How to create CreatedOn and UpdatedOn using EF Core 2.1 and Pomelo

蹲街弑〆低调 提交于 2019-12-07 07:28:41

问题


In Code First approach, how to define my entity so that:

  • CreatedOn NOT NULL - value is generated on insert by the db with the current timestamp
  • Updated NULL - value is generated on update by the db with the current timestamp

Sample Entity:

public class MyEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column(TypeName = "TIMESTAMP")]
    public DateTime CreatedOn { get; set; }
    [Column(TypeName = "TIMESTAMP")]
    public DateTime UpdatedOn { get; set; }
}

DbContext:

public class MyContext : DbContext
{
    public MyContext(DbContextOptions options) : base(options) {}

    public DbSet<MyEntity> Entities { get; set; }
}

End result in the database should be:

  • CreatedOn NOT NULL - Has no Extra - Default could be CURRENT_TIMESTAMP
  • UpdatedOn NULL - Extra on update CURRENT_TIMESTAMP - No Default or Default is NULL

回答1:


Issue:

I've narrowed this down to (what appears to be) a bug in Pomelo. Issue is here:

https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/801

The issue is that Pomelo creates a defaultValue property for DateTime and other structs when generating the migration. If a default value is set on the migration, it overrides the value generation strategy, and the SQL then looks incorrect.

The workaround is to generate the migration, and then manually modify the migrations file to set the defaultValue to null (or remove the entire line).

For example, change this:

migrationBuilder.AddColumn<DateTime>(
                name: "UpdatedTime",
                table: "SomeTable",
                nullable: false,
                defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)))
                .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.ComputedColumn);

To this:

migrationBuilder.AddColumn<DateTime>(
                name: "UpdatedTime",
                table: "SomeTable",
                nullable: false)
                .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.ComputedColumn);

The migration script will then spit out the correct SQL with DEFAULT CURRENT_TIMESTAMP for TIMESTAMP. If you remove the [Column(TypeName = "TIMESTAMP")] attribute, it will use a datetime(6) column and spit out DEFAULT CURRENT_TIMESTAMP(6).

SOLUTION:

I've come up with a workaround that correctly implements Created Time (updated by the database only on INSERT) and Updated time (updated by the database only on INSERT and UPDATE).

First, define your entity like so:

public class SomeEntity
{
    // Other properties here ...

    public DateTime CreatedTime { get; set; }
    public DateTime UpdatedTime { get; set; }
}

Then, add the following to OnModelCreating():

protected override void OnModelCreating(ModelBuilder builder)
{
    // Other model creating stuff here ...

    builder.Entity<SomeEntity>.Property(d => d.CreatedTime).ValueGeneratedOnAdd();
    builder.Entity<SomeEntity>.Property(d => d.UpdatedTime).ValueGeneratedOnAddOrUpdate();

    builder.Entity<SomeEntity>.Property(d => d.CreatedTime).Metadata.BeforeSaveBehavior = PropertySaveBehavior.Ignore;
    builder.Entity<SomeEntity>.Property(d => d.CreatedTime).Metadata.AfterSaveBehavior = PropertySaveBehavior.Ignore;
    builder.Entity<SomeEntity>.Property(d => d.UpdatedTime).Metadata.BeforeSaveBehavior = PropertySaveBehavior.Ignore;
    builder.Entity<SomeEntity>.Property(d => d.UpdatedTime).Metadata.AfterSaveBehavior = PropertySaveBehavior.Ignore;
}

This produces a perfect initial migration (where migrationBuilder.CreateTable is used), and generates the expected SQL:

`created_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),

This should also work on migrations that update existing tables, but do make sure that defaultValue is always null.

The PropertySaveBehavior lines prevent EF from ever trying to overwrite the Created time with a default value. It effectively makes the Created and Updated columns read only from EF's point of view, allowing the database to do all of the work.

You can even extract this into an interface and extension method:

public interface ITimestampedEntity
    {
        DateTime CreatedTime { get; set; }
        DateTime UpdatedTime { get; set; }
    }
public static EntityTypeBuilder<TEntity> UseTimestampedProperty<TEntity>(this EntityTypeBuilder<TEntity> entity) where TEntity : class, ITimestampedEntity
{
    entity.Property(d => d.CreatedTime).ValueGeneratedOnAdd();
    entity.Property(d => d.UpdatedTime).ValueGeneratedOnAddOrUpdate();

    entity.Property(d => d.CreatedTime).Metadata.BeforeSaveBehavior = PropertySaveBehavior.Ignore;
    entity.Property(d => d.CreatedTime).Metadata.AfterSaveBehavior = PropertySaveBehavior.Ignore;
    entity.Property(d => d.UpdatedTime).Metadata.BeforeSaveBehavior = PropertySaveBehavior.Ignore;
    entity.Property(d => d.UpdatedTime).Metadata.AfterSaveBehavior = PropertySaveBehavior.Ignore;

    return entity;
}

Then implement the interface on all of your timestamped entities:

public class SomeEntity : ITimestampedEntity
{
    // Other properties here ...

    public DateTime CreatedTime { get; set; }
    public DateTime UpdatedTime { get; set; }
}

This allows you to set up the Entity from within OnModelCreating() like so:

protected override void OnModelCreating(ModelBuilder builder)
{
    // Other model creating stuff here ...

    builder.Entity<SomeTimestampedEntity>().UseTimestampedProperty();
}

This will also work with DateTimeOffset after issue 799 is fixed.



来源:https://stackoverflow.com/questions/50823809/how-to-create-createdon-and-updatedon-using-ef-core-2-1-and-pomelo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!