Entity Framework Core 捕获数据库变动

时间:2022-07-24
本文章向大家介绍Entity Framework Core 捕获数据库变动,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

在实际项目中我们往往需要记录存储在数据库中数据的变动(例如修改数据前记录下数据的原始值),这样一来在发生误操作时可以将数据恢复到变动前的状态,也可以追溯到数据的修改人。大部分开发人员会自己定义记录数据变动的代码,但是这样不仅费时费力有时还会影响到这个业务的性能。当然,我们也可以利用数据库触发器来记录这些操作,在 SQL Server 数据库 2017 以上版本中给我们提供了跟踪数据库数据更改的功能,利用这个功能可以准确的记录数据库数据的变动。这个功能虽然强大但是某些时候我们使用的数据库并不是 SQL Server 数据库,或者某些情况下我们不适合使用 SQL Server 数据库所提供的这个功能。那么这个时候该怎么办呢?如果你使用的是 Entity Framework Core 2.0 及以上版本来开发项目的话,那这个问题就好解决了。在 Entity Framework Core 中,只要捕获到了数据变更记录,我们就可以将数据随时还原到变更前的状态,在这里数据库变更记录被称为审计数据。那么我们先来看两个问题:

  1. 审计数据是在什么时候产生并写入数据库的呢?
  2. 数据的新旧值是如何获取到的呢? 要解答上述两个问题,那就跟着我一起来看看怎么利用 Entity Framework Core 来捕获审计数据。

零、创建审计模型

捕获审计数据并存入数据库的第一步是创建审计模型,只有具有了审计模型的审计数据才能正确的存入数据库。

public class Audit
{
    public int Id { get; set; }
    public string TableName { get; set; }
    public DateTime DateTime { get; set; }
    [NotMapped]
    public Operation Operation { get; set; }
    public string OperationString
    {
        get { return Operation.ToString(); }
        private set { Operation = (Operation)Enum.Parse(typeof(Operation), value, true); }
    }
    public string Key { get; set; }
    public string Old { get; set; }
    /// <summary>
    /// 操作后的数据
    /// </summary>
    public string New { get; set; }
}
/// <summary>
/// 操作类型
/// </summary>
public enum Operation
{
    Add = 0,
    Delete = 1,
    Modified = 2
}

上述代码创建的审计模型包含被操作表的名称 TableName 、操作的类型 Operation 、被操作数据的主键 Key 、 操作前的数据 Old 以及操作后的数据 New ,其中操作类型包含了增删改。

一、创建审计数据存储

现在我们有了审计模型,但是只有审计模型还不行,我们还需要创建和存储审计数据相关的类,下面我们就来一起创建这个类。

public class AuditDb
{
    public EntityEntry _entityEntry { get; set; }
    public AuditDb(EntityEntry entityEntry)
    {
        this._entityEntry = entityEntry;
    }
    public string TableName { get; set; }
    public Operation Operation { get; set; }
    public Dictionary<string, object> keys { get; } = new Dictionary<string, object>();
    public Dictionary<string, object> olds { get; } = new Dictionary<string, object>();
    public Dictionary<string, object> news { get; } = new Dictionary<string, object>();
    public List<PropertyEntry> propertyEntries { get; } = new List<PropertyEntry>();
    public bool HasPropertyEntries => propertyEntries.Any();
    public Audit ToAudit()
    {
        Audit audit = new Audit
        {
            TableName = TableName,
            Operation = Operation,
            DateTime = DateTime.Now,
            Key = JsonConvert.SerializeObject(keys),
            Old = olds.Count == 0 ? null : JsonConvert.SerializeObject(olds),
            New = news.Count == 0 ? null : JsonConvert.SerializeObject(news)
        };
        return audit;
    }
}

这个类主要是用于存储表名称,被操作数据的主键Id,被操作前的数据和被操作后的数据。在上面的代码中我们看到我们将被操作数据的主键Id、被操作前的数据和被操作后的数据的变量都定义成了字典类型,这是因为我们的程序中有可能出现批量操作的问题。在将上述信息转换成 Audit 时提示我们对被操作前的数据和被操作后的数据进行了一个长度判断,这是因为当我们新增数据的时候是没有旧数据的,当我们对数据没有进行任何更改就提交数据的时候是不存在新数据的。

二、重写 SaveChanges

这个例子重写的是 SaveChanges ,对于 SaveChangesAsync 同样适用。我们需要在 OnBeforSaveBehavior 方法中创建 AuditDb 列表。

public class EFContext : DbContext
{
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        List<AuditDb> auditDbs = OnBeforeSaveBehavior();
        var result = base.SaveChanges(acceptAllChangesOnSuccess);
        return result;
    }
    List<AuditDb> OnBeforeSaveBehavior()
    {
        ChangeTracker.DetectChanges();
        List<AuditDb> auditDbs = new List<AuditDb>();
        foreach (EntityEntry entity in ChangeTracker.Entries())
        {
            if (entity.Entity is Audit || entity.State == EntityState.Detached || entity.State == EntityState.Unchanged)
            {
                continue;
            }
            AuditDb auditDb = new AuditDb(entity)
            {
                TableName = entity.Metadata.Name
            };
            auditDbs.Add(auditDb);
            foreach (var property in entity.Properties)
            {
                if (property.IsTemporary)
                {
                    auditDb.propertyEntries.Add(property);
                    continue;
                }
                var propertName = property.Metadata.Name;
                if (property.Metadata.IsPrimaryKey())
                {
                    auditDb.keys[propertName] = property.CurrentValue;
                    continue;
                }
                switch (entity.State)
                {
                    case EntityState.Deleted:
                        auditDb.Operation = Operation.Delete;
                        auditDb.olds[propertName] = property.OriginalValue;
                        break;
                    case EntityState.Modified:
                        if (property.IsModified)
                        {
                            auditDb.Operation = Operation.Modified;
                            auditDb.olds[propertName] = property.OriginalValue;
                            auditDb.news[propertName] = property.CurrentValue;
                        }
                        break;
                    case EntityState.Added:
                        auditDb.Operation = Operation.Add;
                        auditDb.news[propertName] = property.CurrentValue;
                        break;
                }
            }
        }
        List<Audit> audits = new List<Audit>();
        foreach (var item in auditDbs.Where(p => !p.HasPropertyEntries))
        {
            audits.Add(item.ToAudit());
        }
        return auditDbs.Where(p => p.HasPropertyEntries).ToList();
    }
}

到目前为止,捕获审计数据的所有代码已经完成,这里需要注意的一点是部分实体属性是由数据库生成的,例如当前日期、Id等,这些值需要等待 SaveChanges 方法执行完毕后方可获得,也就是说在这种情况下保存审计数据必须在 SaveChanges 方法之后。

三、总结

通过前面的代码示例和讲解,我们就可以解答前面提出的两个问题了,除了部分数据是由数据库自动生成的情况下,大部分情况下在调用SaveChanges方法之前,我们通过上下文中的ChangeTracker属性来获取旧值和新值并保存。上述代码理解起来比较简单,适用于大部分情况,可以直接放在项目中使用。