如何通过Entity Framework Core为字段DDL添加ON UPDATE CURRENT_TIMESTAMP
PostgreSQL + EF Core 实现 UpdatedOn 自动更新为 CURRENT_TIMESTAMP
问题背景
需要在PostgreSQL中通过EF Core创建Movie表,要求:
- 新增记录时,
CreatedOn和UpdatedOn默认设为CURRENT_TIMESTAMP - 更新记录时,
UpdatedOn自动更新为最新的CURRENT_TIMESTAMP
已实现的模型和DbContext配置如下,但生成的DDL中UpdatedOn没有自动更新逻辑——这里需要注意,PostgreSQL本身不支持MySQL的ON UPDATE CURRENT_TIMESTAMP语法,需通过其他方式实现。
已尝试代码
Movie模型
using System.ComponentModel.DataAnnotations; namespace ShowMe.Models; public class Movie { [Key] public Guid Id { get; set; } public string Title { get; set; } public string? Description { get; set; } public string Director { get; set; } public ICollection<Show> Shows { get; set; } public TimeSpan Duration { get; set; } public DateOnly ReleaseDate { get; set; } public DateTime CreatedOn { get; set; } public DateTime UpdatedOn { get; set; } }
DbContext配置
using Microsoft.EntityFrameworkCore; using ShowMe.Models; namespace ShowMe.Data; public class DataContext : DbContext { public DataContext(DbContextOptions<DataContext> options) : base(options) { } public DbSet<Movie> Movies { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Movie>() .Property(b => b.Id) .ValueGeneratedOnAdd() .HasDefaultValueSql("gen_random_uuid()"); modelBuilder.Entity<Movie>() .Property(b => b.CreatedOn) .HasDefaultValueSql("CURRENT_TIMESTAMP") .ValueGeneratedOnAdd(); modelBuilder.Entity<Movie>() .Property(b => b.UpdatedOn) .HasDefaultValueSql("CURRENT_TIMESTAMP") .ValueGeneratedOnAddOrUpdate(); } }
解决方案
方法1:数据库触发器(推荐,数据库层面保证一致性)
PostgreSQL通过触发器实现字段自动更新,步骤如下:
- 创建触发器函数:
CREATE OR REPLACE FUNCTION update_movie_updatedon() RETURNS TRIGGER AS $$ BEGIN NEW.UpdatedOn = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql;
- 为
Movies表创建更新触发器:
CREATE TRIGGER trigger_movie_update BEFORE UPDATE ON Movies FOR EACH ROW EXECUTE FUNCTION update_movie_updatedon();
可以通过EF Core迁移脚本添加:
- 生成空迁移:
dotnet ef migrations add AddMovieUpdatedOnTrigger - 打开迁移文件,补充如下代码:
protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(@" CREATE OR REPLACE FUNCTION update_movie_updatedon() RETURNS TRIGGER AS $$ BEGIN NEW.UpdatedOn = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; "); migrationBuilder.Sql(@" CREATE TRIGGER trigger_movie_update BEFORE UPDATE ON Movies FOR EACH ROW EXECUTE FUNCTION update_movie_updatedon(); "); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.Sql("DROP TRIGGER IF EXISTS trigger_movie_update ON Movies;"); migrationBuilder.Sql("DROP FUNCTION IF EXISTS update_movie_updatedon();"); }
方法2:EF Core SaveChanges拦截器(应用层面处理)
通过拦截EF Core的保存操作,在数据入库前自动更新UpdatedOn字段:
- 创建拦截器类:
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using ShowMe.Models; public class UpdateTimestampInterceptor : SaveChangesInterceptor { public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { UpdateTimestamps(eventData.Context); return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { UpdateTimestamps(eventData.Context); return base.SavingChangesAsync(eventData, result, cancellationToken); } private void UpdateTimestamps(DbContext? context) { if (context == null) return; foreach (var entry in context.ChangeTracker.Entries<Movie>()) { if (entry.State == EntityState.Added) { entry.Entity.CreatedOn = DateTime.UtcNow; entry.Entity.UpdatedOn = DateTime.UtcNow; } else if (entry.State == EntityState.Modified) { entry.Entity.UpdatedOn = DateTime.UtcNow; // 锁定CreatedOn字段,避免被修改 entry.Property(nameof(Movie.CreatedOn)).IsModified = false; } } } }
- 在DbContext中注册拦截器:
修改DataContext的配置方法:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.AddInterceptors(new UpdateTimestampInterceptor()); }
此时可以移除DbContext中CreatedOn和UpdatedOn的HasDefaultValueSql配置,也可以保留作为双重保障。
注意事项
- 触发器方法不受应用层限制,无论通过EF Core、直接SQL还是其他工具更新数据,都会自动更新
UpdatedOn。 - 拦截器方法仅对通过EF Core发起的更新生效,若有其他途径修改数据,需额外处理。
内容的提问来源于stack exchange,提问作者Akash Vekariya
相关产品推荐
相关产品推荐

