背景介绍

在我们的日常开发中,有时候需要记录数据库表中值的变化, 这时候我们通常会使用触发器或者使用关系型数据库中临时表(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

  1. [Table("Audit")]
  2. public class Audit
  3. {
  4. [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  5. public int Id { get; set; }
  6. public string TableName { get; set; }
  7. public DateTime DateTime { get; set; }
  8. public string KeyValues { get; set; }
  9. public string OldValues { get; set; }
  10. public string NewValues { get; set; }
  11. }

Customer.cs

  1. [Table("Customer")]
  2. public class Customer
  3. {
  4. [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  5. public int Id { get; set; }
  6. public string FirstName { get; set; }
  7. public string LastName { get; set; }
  8. }

SampleContext.cs

  1. public class SampleContext : DbContext
  2. {
  3. public SampleContext()
  4. {
  5. }
  6. public DbSet<Customer> Customers { get; set; }
  7. public DbSet<Audit> Audits { get; set; }
  8. }

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

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. using (var context = new SampleContext())
  6. {
  7. // Insert a row
  8. var customer = new Customer();
  9. customer.FirstName = "John";
  10. customer.LastName = "doe";
  11. context.Customers.Add(customer);
  12. context.SaveChangesAsync().Wait();
  13. // Update the first customer
  14. customer.LastName = "Doe";
  15. context.SaveChangesAsync().Wait();
  16. // Delete the customer
  17. context.Customers.Remove(customer);
  18. context.SaveChangesAsync().Wait();
  19. }
  20. }
  21. }

实现步骤

复写上下文SaveChangeAsync方法

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

  1. public class AuditEntry
  2. {
  3. public AuditEntry(EntityEntry entry)
  4. {
  5. Entry = entry;
  6. }
  7. public EntityEntry Entry { get; }
  8. public string TableName { get; set; }
  9. public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();
  10. public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();
  11. public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();
  12. public List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();
  13. public bool HasTemporaryProperties => TemporaryProperties.Any();
  14. public Audit ToAudit()
  15. {
  16. var audit = new Audit();
  17. audit.TableName = TableName;
  18. audit.DateTime = DateTime.UtcNow;
  19. audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
  20. audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
  21. audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
  22. return audit;
  23. }
  24. }
代码解释
  • Entry属性表示变更的实体
  • TableName属性表示实体对应的数据库表名
  • KeyValues属性表示所有的主键值
  • OldValues属性表示当前实体所有变更属性的原始值
  • NewValues属性表示当前实体所有变更属性的新值
  • TemporaryProperties属性表示当前实体所有由数据库生成的属性集合

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

  1. public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
  2. {
  3. var auditEntries = OnBeforeSaveChanges();
  4. var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
  5. await OnAfterSaveChanges(auditEntries);
  6. return result;
  7. }
  8. private List<AuditEntry> OnBeforeSaveChanges()
  9. {
  10. throw new NotImplementedException();
  11. }
  12. private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
  13. {
  14. throw new NotImplementedException();
  15. }
代码解释
  • 这里我们添加了2个方法OnBeforeSaveChange()OnAfterSaveChanges
  • OnBeforeSaveChanges是用来获取所有需要记录的实体
  • OnAfterSaveChanges是为了获得实体中数据库生成列的新值(例如自增列, 计算列)并持久化变更记录, 这一步必须放置在调用父类SaveChangesAsync之后,因为只有持久化之后,才能获取自增列和计算列的新值。
  • OnBeforeSaveChange方法之后,OnAfterSaveChanges方法之前, 我们调用父类的SaveChangesAsync来保存实体变更。

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

  1. private List<AuditEntry> OnBeforeSaveChanges()
  2. {
  3. ChangeTracker.DetectChanges();
  4. var auditEntries = new List<AuditEntry>();
  5. foreach (var entry in ChangeTracker.Entries())
  6. {
  7. if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
  8. continue;
  9. var auditEntry = new AuditEntry(entry);
  10. auditEntry.TableName = entry.Metadata.Relational().TableName;
  11. auditEntries.Add(auditEntry);
  12. foreach (var property in entry.Properties)
  13. {
  14. if (property.IsTemporary)
  15. {
  16. // value will be generated by the database, get the value after saving
  17. auditEntry.TemporaryProperties.Add(property);
  18. continue;
  19. }
  20. string propertyName = property.Metadata.Name;
  21. if (property.Metadata.IsPrimaryKey())
  22. {
  23. auditEntry.KeyValues[propertyName] = property.CurrentValue;
  24. continue;
  25. }
  26. switch (entry.State)
  27. {
  28. case EntityState.Added:
  29. auditEntry.NewValues[propertyName] = property.CurrentValue;
  30. break;
  31. case EntityState.Deleted:
  32. auditEntry.OldValues[propertyName] = property.OriginalValue;
  33. break;
  34. case EntityState.Modified:
  35. if (property.IsModified)
  36. {
  37. auditEntry.OldValues[propertyName] = property.OriginalValue;
  38. auditEntry.NewValues[propertyName] = property.CurrentValue;
  39. }
  40. break;
  41. }
  42. }
  43. }
  44. }
代码解释
  • 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, 代码如下

  1. private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
  2. {
  3. if (auditEntries == null || auditEntries.Count == 0)
  4. return Task.CompletedTask;
  5. foreach (var auditEntry in auditEntries)
  6. {
  7. // Get the final value of the temporary properties
  8. foreach (var prop in auditEntry.TemporaryProperties)
  9. {
  10. if (prop.Metadata.IsPrimaryKey())
  11. {
  12. auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
  13. }
  14. else
  15. {
  16. auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
  17. }
  18. }
  19. // Save the Audit entry
  20. Audits.Add(auditEntry.ToAudit());
  21. }
  22. return SaveChangesAsync();
  23. }
代码解释
  • OnBeforeSaveChanges中,我们记录下了当前实体所有需要数据库生成的属性。 在调用父类的SaveChangesAsync方法, 我们可以获取通过property的CurrentValue属性获得到这些数据库生成属性的新值
  • 记录下新值,之后我们生成变更实体记录Audit,并添加到上下文中,再次调用SaveChangesAsync方法,将其持久化

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

  • 这个方案中,整个数据库持久化并不在一个原子事务中,我们都知道Entity Framework的SaveChangesAsync方法是自带事务的,但是调用2次SaveChangeAsync就不是一个事务作用域了,可能出现实体保存成功,Audit实体保存失败的情况
  • 由于调用了2次SaveChangeAsync方法,所以Audit实体中的DateTime属性并不能确切的反映保存实体操作的真正时间, 中间间隔了第一次SaveChangeAsync花费的时间(个人认为在OnBeforeSaveChanges中就可以生成这个DateTime让时间更精确一些)
  • 如果所有实体属性值都是预生成的,非数据库生成的,作者这个方案还是非常好的,但是如果有数据库自增列或计算列, 还是使用关系型数据库中临时表(Temporal Table)或数据变更捕获(Change Data Capture)特性比较合理

本篇源代码

Entitiy Framework Core中使用ChangeTracker持久化实体修改历史的更多相关文章

  1. 浅析Entity Framework Core中的并发处理

    前言 Entity Framework Core 2.0更新也已经有一段时间了,园子里也有不少的文章.. 本文主要是浅析一下Entity Framework Core的并发处理方式. 1.常见的并发处 ...

  2. ASP.NET Core Web 应用程序系列(五)- 在ASP.NET Core中使用AutoMapper进行实体映射

    本章主要简单介绍下在ASP.NET Core中如何使用AutoMapper进行实体映射.在正式进入主题之前我们来看下几个概念: 1.数据库持久化对象PO(Persistent Object):顾名思义 ...

  3. 如何处理Entity Framework / Entity Framework Core中的DbUpdateConcurrencyException异常(转载)

    1. Concurrency的作用 场景有个修改用户的页面功能,我们有一条数据User, ID是1的这个User的年龄是20, 性别是female(数据库中的原始数据)正确的该User的年龄是25, ...

  4. 悲观并发 乐观并发 Entity Framework Core中的并发处理

    悲观并发策略 A用户发起一个请求   开启了事务 查询到了某一条数据 进行修改     在A提交事务之前 其他人都不能对这条数据进行修改 这种策略最常见的一个问题就是死锁  比如A修改X记录,B修改Y ...

  5. Entity Framework Core中的数据迁移命令

    使用程序包管理控制台输入命令. 数据迁移命令: Add-Migration  对比当前数据库和模型的差异,生成相应的代码,使数据库和模型匹配的. Remove-Migration 删除上次的迁移 Sc ...

  6. 《浅析Entity Framework Core中的并发处理》引起的思考

    看到一篇关于EF并发处理的文章,http://www.cnblogs.com/GuZhenYin/p/7761352.html,突然觉得为什么常见业务中为什么很少做并发方面的考虑.结合过去的项目,这样 ...

  7. ASP.NET Core中使用GraphQL - 第六章 使用EF Core作为持久化仓储

    ASP.NET Core中使用GraphQL ASP.NET Core中使用GraphQL - 第一章 Hello World ASP.NET Core中使用GraphQL - 第二章 中间件 ASP ...

  8. Entity Framework Core生成的存储过程在MySQL中需要进行处理及PMC中的常用命令

    在使用Entity Framework Core生成MySQL数据库脚本,对于生成的存储过程,在执行的过程中出现错误,需要在存储过程前面添加 delimiter // 附:可以使用Visual Stu ...

  9. 全自动迁移数据库的实现 (Fluent NHibernate, Entity Framework Core)

    在开发涉及到数据库的程序时,常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况,这时就需要进行数据库迁移. 实现数据库迁移有很多种办法,从手动管理各个版本的ddl脚本,到实现自己的mig ...

随机推荐

  1. 在WINDOWS中安装使用GSL(MinGW64+Sublime Text3 & Visual Studio)

    本文介绍在Windows下安装使用GSL库,涉及GSL两个版本(官方最新版及GSL1.8 VC版).msys shell.GCC.G++等内容,最终实现对GSL安装及示例基于MinGW64在Subli ...

  2. Emgucv使用中常用函数总结

    Emgucv常用函数总结: 读取图片 Mat SCr = new Mat(Form1.Path, Emgu.CV.CvEnum.LoadImageType.AnyColor); //根据路径创建指定的 ...

  3. 普通用户添加sudo权限

    1.切换超级用户 su - root 2.编辑配置文件 vim /etc/sudoers ## Allow root to run any commands anywhere root ALL=(AL ...

  4. [数据结构] 用C语言模拟一个简单的队列程序

    #include<stdio.h> #include <stdlib.h> #include<string.h> #include<math.h> // ...

  5. 数据分析 大数据之路 五 pandas 报表

    pandas:  在内存中或对象,会有一套基于对象属性的方法,   可以视为 pandas 是一个存储一维表,二维表,三维表的工具, 主要以二维表为主 一维的表, (系列(Series)) 二维的表, ...

  6. php获取微信基础接口凭证Access_token

    php获取微信基础接口凭证Access_token的具体代码,供大家参考,具体内容如下 access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token.开发者需要进 ...

  7. 关于VB里判断逻辑的说明

    如上图,当进行连续判断的时候,即使第一个已经不符合条件了,后面的依然会计算.这点一定要记住,除非你所有的函数都有必要执行,否则会导致效率降低. 减代码不一定能提高效率,对于IIF和连续判断写法,貌似很 ...

  8. word文档最上面有一条不是页眉的线

    word2013文档最上面有一条不是页眉的线 在编辑Word文档时发现文档上面出现了一条实线,而且并非页眉,这里我采取了一个方式: 找到[设计]---[页面边框] 找到[边框和底纹]----[页面边框 ...

  9. 微信小程序----没有 DOM 对象,一切基于组件化 ---- mpvue

    封装好用的 类库 和 组件,复用且灵活度高 抽取相同的部分放在函数内部(组件内部) 抽取不同的部分放在形参(组件 props 传参,或者插槽) new Promise 运行时 初始化实例对象的状态为 ...

  10. class A<T> where T:new()

    class A<T> where T:new() 这是类型参数约束,where表明了对类型变量T的约束关系.where T:A 表示类型变量是继承于A的,或者是A本身.where T: n ...