You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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通过触发器实现字段自动更新,步骤如下:

  1. 创建触发器函数:
CREATE OR REPLACE FUNCTION update_movie_updatedon()
RETURNS TRIGGER AS $$
BEGIN
    NEW.UpdatedOn = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
  1. 为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字段:

  1. 创建拦截器类:
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;
            }
        }
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 11:15:38