背景介绍

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

实现步骤

复写上下文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)特性比较合理

本篇源代码

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. Shell编程-控制结构 | 基础篇

    if-then-else分支结构 if-then-else是一种基于条件测试结果的流程控制结构.如果测试结果为真,则执行控制结构中相应的命令列表:否则将进行另外一个条件测试或者退出该控制结构. if- ...

  2. Java对象引用和对象赋值

    关于对象与引用之间的一些基本概念. 初学Java时,在很长一段时间里,总觉得基本概念很模糊.后来才知道,在许多Java书中,把对象和对象的引用混为一谈.可是,如果我分不清对象与对象引用,那实在没法很好 ...

  3. 启动两个tomcat服务,以及使用nginx代理实现访问

    1.shoudowm.bat\startup.bat\catalina.bat, 将CATALINA_HOME修改为CATALINA_HOME_2 2.server.xml <Server po ...

  4. freebsd 时间校准

    修改 /etc/rc.conf ntpdate_enable='YES'ntpd_enable='YES' 如果这里不指定ntpdate_hosts=参数的话,ntpdate会读取/etc/ntp.c ...

  5. 将普通用户添加到sudo

    将普通用户添加到sudo组 可以编辑/etc/sudoers文件将普通用户加入sudo组.要注意的是修改该文件只能使用visudo命令:1.首先切换到root #su - (注意有 “-” ,这和su ...

  6. It is difficult to the point of impossiblity for sb to image a time when ...

    对sb而言很难想象一段..的时光.

  7. 使用IDEA时跳转到.class的解决办法

    项目背景:jdk1.8 软件环境:IDEA 问题: 1. 两个不同的项目,在A项目中写了一个实体类.B项目中引用.在B项目中CTRL+鼠标左键点击进入,正常情况下是进入了源码文件,也就是.JAVA文件 ...

  8. 《SpringMVC从入门到放肆》十一、SpringMVC注解式开发处理器方法返回值

    上两篇我们对处理器方法的参数进行了分别讲解,今天来学习处理器方法的返回值. 一.返回ModelAndView 若处理器方法处理完后,需要跳转到其它资源,且又要在跳转资源之间传递数据,此时处理器方法返回 ...

  9. Centos服务器上NFS灾备环境及KVM的搭建及使用

    1.概述 由于在单台服务器上搭建灾备环境需要KVM和NFS的支持,下面先列出KVM的搭建流程,再列出使用NFS实现单台服务器灾备的流程. A.搭建KVM环境 1>.主机环境准备 Linux Sy ...

  10. Markdown基础语法笔记

    # 一级标题## 二级标题### 三级标题###### #号之后记得加一个空格 仅支持1-6级标题  ### 列表 - 文本1 - 文本2 - 文本3+ 列表2* 列表2 ### 有序列表1. 有序文 ...