为了账号安全,请及时绑定邮箱和手机立即绑定

Entitiy Framework Core中使用ChangeTracker持久化实体修改历史

标签:
C#

背景介绍

在我们的日常开发中,有时候需要记录数据库表中值的变化, 这时候我们通常会使用触发器或者使用关系型数据库中临时表(Temporal Table)或数据变更捕获(Change Data Capture)特性来记录数据库表中字段的值变化。原文的作者Gérald Barré讲解了如何使用Entity Freamwork Core上下文中的ChangeTracker来获取并保存实体的变化记录。

原文链接 Entity Framework Core: History / Audit table

ChangeTracker

ChangeTracker是Entity Framework Core记录实体变更的核心对象(这一点和以前版本的Entity Framework一致)。当你使用Entity Framework Core进行获取实体对象、添加实体对象、删除实体对象、更新实体对象、附加实体对象等操作时,ChangeTracker都会记录下来对应的实体引用和对应的实体状态。
我们可以通过ChangeTracker.Entries()方法, 获取到当前上下文中使用的所有实体对象, 以及每个实体对象的状态属性State。

Entity Framework Core中可用的实体状态属性有以下几种

  • Detached

  • Unchanged

  • Deleted

  • Modified

  • Added

所以如果我们要记录实体的变更,只需要从ChangeTracker中取出所有Added, Deleted, Modified状态的实体, 并将其记录到一个日志表中即可。

我们的目标

我们以下面这个例子为例。
当前我们有一个顾客表Customer和一个日志表Audit, 其对应的实体对象及Entity Framework上下文如下:

Audit.cs

    [Table("Audit")]    public class Audit
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]        public int Id { get; set; }        public string TableName { get; set; }        public DateTime DateTime { get; set; }        public string KeyValues { get; set; }        public string OldValues { get; set; }        public string NewValues { get; set; }
    }

Customer.cs

    [Table("Customer")]    public class Customer
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]        public int Id { get; set; }        public string FirstName { get; set; }        public string LastName { get; set; }
    }

SampleContext.cs

    public class SampleContext : DbContext
    {        public SampleContext()        {

        }        public DbSet<Customer> Customers { get; set; }        public DbSet<Audit> Audits { get; set; }
    }

我们希望当执行以下代码之后, 在Audit表中产生如下数据

    class Program
    {        static void Main(string[] args)
        {
            using (var context = new SampleContext())
            {                // Insert a row
                var customer = new Customer();
                customer.FirstName = "John";
                customer.LastName = "doe";                context.Customers.Add(customer);                context.SaveChangesAsync().Wait();                // Update the first customer
                customer.LastName = "Doe";                context.SaveChangesAsync().Wait();                // Delete the customer
                context.Customers.Remove(customer);                context.SaveChangesAsync().Wait();
            }
        }
    }

5ba7bff50001c3e408310098.jpg

实现步骤

复写上下文SaveChangeAsync方法

首先我们添加一个AuditEntry类, 来生成变更记录。

    public class AuditEntry
    {        public AuditEntry(EntityEntry entry)        {
            Entry = entry;
        }        public EntityEntry Entry { get; }        public string TableName { get; set; }        public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();        public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();        public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();        public List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();        public bool HasTemporaryProperties => TemporaryProperties.Any();        public Audit ToAudit()        {            var audit = new Audit();
            audit.TableName = TableName;
            audit.DateTime = DateTime.UtcNow;
            audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
            audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
            audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);            return audit;
        }
    }
代码解释
  • Entry属性表示变更的实体

  • TableName属性表示实体对应的数据库表名

  • KeyValues属性表示所有的主键值

  • OldValues属性表示当前实体所有变更属性的原始值

  • NewValues属性表示当前实体所有变更属性的新值

  • TemporaryProperties属性表示当前实体所有由数据库生成的属性集合

然后我们打开SampleContext.cs, 复写方法SaveChangeAsync代码如下。

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))    {        var auditEntries = OnBeforeSaveChanges();        var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);        await OnAfterSaveChanges(auditEntries);        return result;
    }    
    private List<AuditEntry> OnBeforeSaveChanges()    {        throw new NotImplementedException();
    }    private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)    {        throw new NotImplementedException();
    }
代码解释
  • 这里我们添加了2个方法OnBeforeSaveChange()OnAfterSaveChanges

  • OnBeforeSaveChanges是用来获取所有需要记录的实体

  • OnAfterSaveChanges是为了获得实体中数据库生成列的新值(例如自增列, 计算列)并持久化变更记录, 这一步必须放置在调用父类SaveChangesAsync之后,因为只有持久化之后,才能获取自增列和计算列的新值。

  • OnBeforeSaveChange方法之后,OnAfterSaveChanges方法之前, 我们调用父类的SaveChangesAsync来保存实体变更。

然后我们来修改OnBeforeSaveChanges方法, 代码如下

    private List<AuditEntry> OnBeforeSaveChanges()
    {
        ChangeTracker.DetectChanges();        var auditEntries = new List<AuditEntry>();        foreach (var entry in ChangeTracker.Entries())
        {            if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)                continue;    
            var auditEntry = new AuditEntry(entry);
            auditEntry.TableName = entry.Metadata.Relational().TableName;
            auditEntries.Add(auditEntry);    
            foreach (var property in entry.Properties)
            {                if (property.IsTemporary)
                {                    // value will be generated by the database, get the value after saving
                    auditEntry.TemporaryProperties.Add(property);                    continue;
                }    
                string propertyName = property.Metadata.Name;                if (property.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[propertyName] = property.CurrentValue;                    continue;
                }
    
                switch (entry.State)
                {                    case EntityState.Added:
                        auditEntry.NewValues[propertyName] = property.CurrentValue;                        break;    
                    case EntityState.Deleted:
                        auditEntry.OldValues[propertyName] = property.OriginalValue;                        break;    
                    case EntityState.Modified:                        if (property.IsModified)
                        {
                            auditEntry.OldValues[propertyName] = property.OriginalValue;
                            auditEntry.NewValues[propertyName] = property.CurrentValue;
                        }                        break;
                }
            }
        }
    }
代码解释
  • ChangeTracker.DetectChanges()是强制上下文再做一次变更检查

  • 由于Audit表也在ChangeTracker的管理中, 所以在OnBeforeSaveChanges方法中,我们需要将Audit表的实体排除掉,否则会出现死循环

  • 这里我们只需要操作所有Added, Modified, Deleted状态的实体,所以Detached和Unchanged状态的实体需要排除掉

  • ChangeTracker中记录的每个实体都有一个Properties集合,里面记录的每个实体所有属性的状态, 如果某个属性被修改了,则该属性的IsModified是true.

  • 实体属性Property对象中的IsTemporary属性表明了该字段是不是数据库生成的。 我们将所有数据库生成的属性放到了TemplateProperties集合中,供OnAfterSaveChanges方法遍历

  • 我们可以通过Property对象的Metadata.IsPrimaryKey()方法来获得当前字段是不是主键字段

  • Property对象的CurrentValue属性表示当前字段的新值,OriginalValue属性表示当前字段的原始值

最后我们修改一下OnAfterSaveChanges, 代码如下

    private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)    {        if (auditEntries == null || auditEntries.Count == 0)            return Task.CompletedTask;        foreach (var auditEntry in auditEntries)
        {            // Get the final value of the temporary properties
            foreach (var prop in auditEntry.TemporaryProperties)
            {                if (prop.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
                }                else
                {
                    auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
                }
            }            // Save the Audit entry
            Audits.Add(auditEntry.ToAudit());
        }        return SaveChangesAsync();
    }
代码解释
  • OnBeforeSaveChanges中,我们记录下了当前实体所有需要数据库生成的属性。 在调用父类的SaveChangesAsync方法, 我们可以获取通过property的CurrentValue属性获得到这些数据库生成属性的新值

  • 记录下新值,之后我们生成变更实体记录Audit,并添加到上下文中,再次调用SaveChangesAsync方法,将其持久化

当前方案的问题和适合的场景

  • 这个方案中,整个数据库持久化并不在一个原子事务中,我们都知道Entity Framework的SaveChangesAsync方法是自带事务的,但是调用2次SaveChangeAsync就不是一个事务作用域了,可能出现实体保存成功,Audit实体保存失败的情况

  • 由于调用了2次SaveChangeAsync方法,所以Audit实体中的DateTime属性并不能确切的反映保存实体操作的真正时间, 中间间隔了第一次SaveChangeAsync花费的时间(个人认为在OnBeforeSaveChanges中就可以生成这个DateTime让时间更精确一些)

  • 如果所有实体属性值都是预生成的,非数据库生成的,作者这个方案还是非常好的,但是如果有数据库自增列或计算列, 还是使用关系型数据库中临时表(Temporal Table)或数据变更捕获(Change Data Capture)特性比较合理

本篇源代码

 

作者:Lamond Lu

出处:https://www.cnblogs.com/lwqlun/p/9693970.html


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消