C#中的自动审计字段(Audit trail)
在系统开发的数据库设计中,有时基于对数据变更的追溯,需要添加几个用于审计的字段。如下:
-
IsValid, 当前是否有效,用于数据行的逻辑删除。 -
CreatedBy, 数据行的创建人。 -
CreatedTime, 数据行创建的时间,默认当前时间。 -
UpdatedBy,数据行的更新人,默认为创建人。 -
UpdatedTime, 数据行的更新时间,默认为第一次创建时间,后变更时候更新。.
对应的数据库SQL脚本如下:
CREATE TABLE TestTable(ID int NOT NULL IDENTITY(1, 1),-- ... (其它 字段)IsValid bit NOT NULL,CreatedBy int NOT NULL,CreatedTime DATETIME NOT NULL,UpdatedBy int NOT NULL,UpdatedTime DATETIME NOT NULL,PRIMARY KEY (ID));
以上几个字段,如果在业务对象的操作中手动去设置相应的值,实际上是比较繁琐的,且是容易遗漏的。例如如下的代码,写多了,其实也没太多的意义:
baseEntity.IsValid = true;// 其它业务字段的赋值。baseEntity.CreatedByUserId = User.UserId;baseEntity.CreatedTime = DateTime.Now;baseEntity.UpdatedByUserId = User.UserId;baseEntity.UpdatedTime = DateTime.Now;
基于这种场景,我们需要思考,是否有更加优雅的方式,来解决这样的一个问题呢? 是的,Entity Framework是内置了一种Change Tracking的机制,它会自动跟踪上下文中已加载的实体,并且提供ChangeTracker类,来方便获取当前实体所有的跟踪信息。所以,我们可以在数据库的SaveChanges()方法时,进行重写,补上我们需要的审计字段信息逻辑。 以下有两种重新的方式,一种是类有父类时,可以直接通过BaseEntity的属性进行赋值;另外一种是EF生成的对象,没有继承父类,那么便通过反射判断字段名的方式进行取属性赋值。同时考虑到EF脚手架DB First在生成实体对象时,会自动生成一个DBContext,所以我们的方法最好通过partial class的方式单独写出来,防止被EF的覆盖。对应类的代码整理如下,以供参考:
public partial class TestDBContext : DbContext{private readonly string _conStr;public TestDBContext(string conStr){_conStr = conStr;}protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){if (!optionsBuilder.IsConfigured){optionsBuilder.UseSqlServer(_conStr);}}// 业务上也只是会逻辑删除,所以只Track新增和变更的情况public int SaveChangesWithAuditInfo1(TokenUser tokenUser){var changeTracker = ChangeTracker.Entries().Where(a => a.State == EntityState.Added || a.State == EntityState.Modified);try{foreach (EntityEntry entry in changeTracker){BaseEntity baseEntity = entry.Entity as BaseEntity;switch (entry.State){case EntityState.Added:baseEntity.IsValid = true;baseEntity.CreatedByUserId = tokenUser.UserId;baseEntity.CreatedTime = DateTime.Now;baseEntity.UpdatedByUserId = tokenUser.UserId;baseEntity.UpdatedTime = DateTime.Now;break;case EntityState.Modified:baseEntity.UpdatedByUserId = tokenUser.UserId;baseEntity.UpdatedTime = DateTime.Now;break;default:break;}}}catch (Exception e){Console.WriteLine(e.Message);Log.Error(e, "Db SaveChanges Error.");throw;}return base.SaveChanges();}// 通过反射, 判断属性,并赋值public int SaveChangesWithAuditInfo2(TokenUser tokenUser){var changeTracker = ChangeTracker.Entries().Where(a =>a.State == EntityState.Added || a.State == EntityState.Modified);try{foreach (EntityEntry entry in changeTracker){var dbModelEntity = entry.Entity;var objectType = dbModelEntity.GetType();var allProperties = objectType.GetProperties();switch (entry.State){case EntityState.Added:if (allProperties.Any(a => a.Name.ToUpper() == "IsValid".ToUpper())){dbModelEntity.GetType().GetProperty("IsValid").SetValue(dbModelEntity, true);}if (allProperties.Any(a => a.Name.ToUpper() == "CreatedByUserId".ToUpper())){dbModelEntity.GetType().GetProperty("CreatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);}if (allProperties.Any(a => a.Name.ToUpper() == "CreatedTime".ToUpper())){dbModelEntity.GetType().GetProperty("CreatedTime").SetValue(dbModelEntity, DateTime.Now);}if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedByUserId".ToUpper())){dbModelEntity.GetType().GetProperty("UpdatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);}if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedTime".ToUpper())){dbModelEntity.GetType().GetProperty("UpdatedTime").SetValue(dbModelEntity, DateTime.Now);}break;case EntityState.Modified:if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedByUserId".ToUpper())){dbModelEntity.GetType().GetProperty("UpdatedByUserId").SetValue(dbModelEntity, tokenUser.UserId);}if (allProperties.Any(a => a.Name.ToUpper() == "UpdatedTime".ToUpper())){dbModelEntity.GetType().GetProperty("UpdatedTime").SetValue(dbModelEntity, DateTime.Now);}break;default:break;}}}catch (Exception e){Console.WriteLine(e.Message);Log.Error(e, "Db SaveChanges Error.");throw;}return base.SaveChanges();}}
最后,在EF ChangeTracker中的EntityState总计有5种,由于我们的数据只会逻辑删除,不会有Deleted的状态,所以在SaveChanges的判断中,只包含了Added和Modified:
public enum EntityState{Detached,Unchanged,Deleted,Modified,Added}
同时再补充一句,在优化EF查询的时候,由于EF的Change Tracking的机制。建议在只查询数据时候,加上AsNoTracking()的方法,可以提高响应的查询效率。