Entity Framework Core 捕获数据库变动

举报
喵叔 发表于 2020/01/16 20:25:49 2020/01/16
【摘要】 在实际项目中我们往往需要记录存储在数据库中数据的变动(例如修改数据前记录下数据的原始值),这样一来在发生误操作时可以将数据恢复到变动前的状态,也可以追溯到数据的修改人。大部分开发人员会自己定义记录数据变动的代码,但是这样不仅费时费力有时还会影响到这个业务的性能。当然,我们也可以利用数据库触发器来记录这些操作,在 SQL Server 数据库 2017 以上版本中给我们提供了跟踪数据库数据更改...

在实际项目中我们往往需要记录存储在数据库中数据的变动(例如修改数据前记录下数据的原始值),这样一来在发生误操作时可以将数据恢复到变动前的状态,也可以追溯到数据的修改人。大部分开发人员会自己定义记录数据变动的代码,但是这样不仅费时费力有时还会影响到这个业务的性能。当然,我们也可以利用数据库触发器来记录这些操作,在 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属性来获取旧值和新值并保存。上述代码理解起来比较简单,适用于大部分情况,可以直接放在项目中使用。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。