我们在项目开发中,通常会有数据审计的项目需求。即业务数据中要包含创建日期,修改日期,修改人等信息等。有些业务数据需要物理删除,有些数据需要逻辑删除。
通常审计数据并不大量参与业务运算,只是为审计提供技术支持。如果我们在项目开发中,花费大量时间在这些审计数据的处理上,显然得不偿失。
本文提出了一个简单的审计数据处理模型,通过Entityframework加以实现。
审计数据的需求总结- 核心所有业务数据都要包括数据的创建人,创建时间,修改时间和修改人。
- 大部分核心数据要支持逻辑删除,所以要包括删除人,删除时间和删除标记位。
- 部分非核心业务数据需要支持物理删除。
在现实开发中,为了实现审计数据功能,我们需要为核心的业务数据类增加相应的审计数据栏位。但是没有必要每个类逐一定义这些栏位,我们可以使用OOP的继承,多态等特性,简化开发。
本文以一个银行分行的管理系统为例,来说明我们的审计数据实现模型。银行分行的业务员数据包括分行(Branch),ATM机和系统的用户User。
审计数据模型的类图- 上图分为两部分,一部分是审计模型,另一部分是该模型在业务数据上的应用。
- IEntity是一个接口,包括创建人,创建时间,修改时间和修改人。
- Entity是一个抽象类,实现IEnitty接口,因此也包含创建人,创建时间,修改时间和修改人四个栏位。
- IDeletable是一个接口,继承IEntity接口,新增了删除人,删除时间和删除标记位。以支持逻辑删除。
- DeletableEntity是一个抽象类,继承Entity类并实现IDeletable接口。
- ATM机器升级换代很快,因此需求是直接物理删除数据就可以,每次维护的数据可以记录到数据中。
- 分行数据需要逻辑删除,数据本身需要被追溯,可以直接继承DeletableEntity 类
- User数据需求和分行数据一样,但是User类已经继承了IdentityUser类,因此只能实现IDeletableEntity接口。
通过继承关系和接口的使用,我们避免了为每个数据类都人工定义审计相关的栏位。
但是审计数据的存取依然是一个问题,我们如果在每个类的存取代码中都增加审计数据的处理,显然做就过于繁琐了。
本文的解决方法是在Entityframework中,定义一个拦截器去实现。而我们模型中定义的类和接口,在拦截器中成了我们重要的路标。
核心代码如下:
public class BankContext : DbContext { private void AddAuditData() => this.ChangeTracker .Entries() .ToList() .ForEach(entry => { if (entry.State == EntityState.Deleted) { if (entry.Entity is IDeletableEntity deletableEnitty) { deletableEnitty.DeletedBy = 2; deletableEnitty.DeletedOn = DateTime.Now; deletableEnitty.IsDeleted = true; entry.State = EntityState.Modified; return; } } if (entry.Entity is IEntity entity) { if (entry.State == EntityState.Added) { entity.CreatedBy = 2; entity.CreatedOn = DateTime.Now; } else if (entry.State == EntityState.Modified) { entity.ModifiedBy = 2; entity.ModifiedOn = DateTime.Now; } } }); public override int SaveChanges() { AddAuditData(); return base.SaveChanges(); } }
- 使用DBContext中的ChangeTracker对象,找到我们存取数据的对象。
- 如果用户要删除该对象,但是该类实现了IDeletableEntity 接口,我们将对象状态改为Modified,填充删除人,删除时间和删除标记位。
A. 删除人即为当前用户,不同系统,获取方式不同,因此本文将其略去,默认写成一个Id为2的用户。
B. 这样用户上层代码就不用再区分物理删除和逻辑删除。所有删除代码都按照物理删除的方式写就行。 - 如果当前存取对象实现了IEntity 接口:
A. 如果是新建数据,则填充创建人和创建时间的栏位
B. 如果是修改数据,则填充修改人和修改时间的栏位 - 复写SaveChanges方法,在数据存取前调用我们定义的方法AddAuditData
- SaveChanges还有其他重载方法,因为修改方式类似,所以不再赘述。
执行EF的migration指令,生成数据Branch,ATM和User数据表。完整类型定义请参考附录。
执行Migration指令:
dotnet ef migrations add InitialCreate dotnet ef database update
执行结果:
从执行结果中我们可以看出,生成的SQL语句中已经包含的审计相关的栏位,我们不希望每个类都定义审计栏位的需求已经实现。
using (var context = new BankContext()){ var u = new User(){ Name = "Tom", }; context.Useres.Add(u); context.SaveChanges(); }
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext(connectionString)){ var u = context.Useres.Where( b=>b.Id == 1).FirstOrDefault(); u.Name = "abc"; context.Update(u); context.SaveChanges(); }
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){ var u = context.Useres.Where( b=>b.Id == 1).FirstOrDefault(); context.Useres.Remove(u); context.SaveChanges(); }
物理删除被自动转化为逻辑删除,删除的审计数据栏位被自动填充
using (var context = new BankContext()){ var branch = new Branch(){ Name = "天津新天地支行", Address = "天津市滨海新区南海路12号新天地华庭A座1门103号", }; context.Branches.Add(branch); context.SaveChanges(); }
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){ var branch = context.Branches.Where( b=>b.Id == 1).FirstOrDefault(); context.Update(branch); context.SaveChanges(); }
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){ var branch = context.Branches.Where( b=>b.Id == 1).FirstOrDefault(); context.Branches.Remove(branch); context.SaveChanges(); }
物理删除被自动转化为逻辑删除,删除的审计数据栏位被自动填充
using (var context = new BankContext()){ var atm = new ATM(){ Name = "取款机-1", }; context.ATMs.Add(atm); context.SaveChanges(); }
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){ var atm = context.ATMs.Where( b=>b.Id == 1).FirstOrDefault(); atm.Name = "abc"; context.Update(atm); context.SaveChanges(); }
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){ var atm = context.ATMs.Where( b=>b.Id == 1).FirstOrDefault(); context.ATMs.Remove(atm); context.SaveChanges(); }
根据需求,ATM数据需要物理删除,因此ATM类没有继承DeletableEntity或实现IDeletableEntity接口,因此直接删除
附录ATM类
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace IQueryableIEnumerable { [Table("t_atm")] public class ATM : Entity{ [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)] public int Id { get; set; } [Required, Column(Order = 1)] public string Name { get; set; } [Timestamp, Column(Order = 20)] public byte[] RowVersion { get; set; } } }
Branch类
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace IQueryableIEnumerable { [Table("t_branch")] public class Branch : DeletableEnitty{ [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)] public int Id { get; set; } [Required, Column(Order = 1)] public string Name { get; set; } [Required, Column("Addr", Order = 2)] public string Address { get; set; } [Timestamp, Column(Order = 20)] public byte[] RowVersion { get; set; } } }
User类
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System; namespace IQueryableIEnumerable { [Table("t_user")] public class User : IDeletableEntity { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)] public int Id { get; set; } [Required, Column(Order = 1)] public string Name { get; set; } public DateTime? DeletedOn { get; set; } public int? DeletedBy { get; set; } public bool IsDeleted { get; set; } = false; public int CreatedBy { get; set; } public DateTime CreatedOn { get; set; } public int? ModifiedBy { get; set; } public DateTime? ModifiedOn { get; set; } [Timestamp, Column(Order = 20)] public byte[] RowVersion { get; set; } } }
BankContext类:
using System; using System.Linq; using Microsoft.EntityframeworkCore; using Microsoft.Extensions.Logging; namespace IQueryableIEnumerable { public class BankContext : DbContext { private readonly ILoggerFactory loggerFactory = LoggerFactory.Create(ConventionForeignKeyExtensions => ConventionForeignKeyExtensions.AddConsole()); private readonly string ConnectionString; public DbSetBranches { get; set; } public DbSet Useres { get; set; } public DbSet ATMs { get; set; } public BankContext() : base() { ConnectionString = @"your connection string"; } public BankContext(DbContextOptions options) : base(options) { ConnectionString = @"your connection string"; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLoggerFactory(loggerFactory); optionsBuilder.UseSqlServer(ConnectionString); } public override int SaveChanges() { AddAuditData(); return base.SaveChanges(); } private void AddAuditData() => this.ChangeTracker .Entries() .ToList() .ForEach(entry => { if (entry.State == EntityState.Deleted) { if (entry.Entity is IDeletableEntity deletableEnitty) { deletableEnitty.DeletedBy = 2; deletableEnitty.DeletedOn = DateTime.Now; deletableEnitty.IsDeleted = true; entry.State = EntityState.Modified; return; } } if (entry.Entity is IEntity entity) { if (entry.State == EntityState.Added) { entity.CreatedBy = 2; entity.CreatedOn = DateTime.Now; } else if (entry.State == EntityState.Modified) { entity.ModifiedBy = 2; entity.ModifiedOn = DateTime.Now; } } }); } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)