我們在開發系統的時候,經常會遇到這種需求數據庫表中的行被更新時需要自動更新某些列。
數據庫
比如下面的Person表有一列UpdateTime,這列數據要求在行被更新后自動更新為系統的當前時間。
Person表:
CREATE TABLE [dbo].[Person]( [ID] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](50) NULL, [Age] [int] NULL, [CreateTime] [datetime] NULL, [UpdateTime] [datetime] NULL, CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Person] ADD CONSTRAINT [DF_Person_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO
我們還有一個Book表,它沒有UpdateTime列,那么這個表的數據在行更新時不要求自動更新任何列
Book表:
CREATE TABLE [dbo].[Book]( [ID] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](50) NULL, [BookDescription] [nvarchar](100) NULL, [ISBN] [nvarchar](50) NULL, [CreateTime] [datetime] NULL, CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Book] ADD CONSTRAINT [DF_Book_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO
那么Person表的UpdateTime列如果映射到了EF Core的實體上的話,有辦法在Person實體被Update的時候自動設置為系統當前時間嗎?答案是當然有!
EF Core 實體
首先我們將這兩張表映射到EF Core的實體對象上:
Person實體:
public partial class Person { public int Id { get; set; } public string Name { get; set; } public int? Age { get; set; } public DateTime? CreateTime { get; set; } public DateTime? UpdateTime { get; set; } }
Book實體:
public partial class Book { public int Id { get; set; } public string Name { get; set; } public string BookDescription { get; set; } public string Isbn { get; set; } public DateTime? CreateTime { get; set; } }
EF Core的DB First生成的DbContext類EFDemoContext
public partial class EFDemoContext : DbContext { public EFDemoContext() { } public EFDemoContext(DbContextOptions<EFDemoContext> options) : base(options) { } public virtual DbSet<Book> Book { get; set; } public virtual DbSet<Person> Person { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("Server=localhost;User Id=sa;Password=1qaz!QAZ;Database=EFDemo"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>(entity => { entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.BookDescription).HasMaxLength(100); entity.Property(e => e.CreateTime) .HasColumnType("datetime") .HasDefaultValueSql("(getdate())"); entity.Property(e => e.Isbn) .HasColumnName("ISBN") .HasMaxLength(50); entity.Property(e => e.Name).HasMaxLength(50); }); modelBuilder.Entity<Person>(entity => { entity.Property(e => e.Id).HasColumnName("ID"); entity.Property(e => e.CreateTime) .HasColumnType("datetime") .HasDefaultValueSql("(getdate())"); entity.Property(e => e.Name).HasMaxLength(50); entity.Property(e => e.UpdateTime).HasColumnType("datetime"); }); } }
DbContext.ChangeTracker.StateChanged事件
之后最關鍵的一點到了,我們需要用到DbContext.ChangeTracker.StateChanged這個事件,這個事件會在DbContext中被Track的實體對象的EntityState狀態發生變化時被觸發,有多少個實體的EntityState狀態變化了,它就會被觸發多少次。
為此,我們需要再定義一個自定義的DbContext類EFDbContext,來繼承DB First自動生成的EFDemoContext類:
//EFDbContext繼承自EFDemoContext,EFDemoContext又繼承自DbContext public class EFDbContext: EFDemoContext { public EFDbContext() { //設置數據庫Command永不超時 this.Database.SetCommandTimeout(0); //DbContext.ChangeTracker.StateChanged事件,會在DbContext中被Track的實體其EntityState狀態值發生變化時被觸發 this.ChangeTracker.StateChanged += (sender, entityStateChangedEventArgs) => { //如果實體狀態變為了EntityState.Modified,那么就嘗試設置其UpdateTime屬性為當前系統時間DateTime.Now,如果實體沒有UpdateTime屬性,會拋出InvalidOperationException異常,所以下面要用try catch來捕獲異常避免系統報錯 if (entityStateChangedEventArgs.NewState == EntityState.Modified) { try { //如果是Person表的實體那么下面的Entry.Property("UpdateTime")就不會拋出異常 entityStateChangedEventArgs.Entry.Property("UpdateTime").CurrentValue = DateTime.Now; } catch(InvalidOperationException) { //如果上面try中拋出InvalidOperationException,就是實體沒有屬性UpdateTime,應該是表Book的實體 } } //如果要自動更新多列,比如還要自動更新實體的UpdateUser屬性值到數據庫,可以像下面這樣再加一個try catch來更新UpdateUser屬性 //if (entityStateChangedEventArgs.NewState == EntityState.Modified) //{ // try // { // entityStateChangedEventArgs.Entry.Property("UpdateUser").CurrentValue = currentUser; // } // catch (InvalidOperationException) // { // } //} }; } }
然后我們在Program.cs的Main方法中(我在本例建立的是一個.Net Core控制台程序)先初始化Person表和Book表的數據,然后再修改Person表和Book表的數據,看看被修改的Person表數據其列UpdateTime的值是否設置為了系統當前時間:
class Program { //初始化Person表和Book表的數據 static void InitializeDataToDB() { var personJim = new Person() { Name="Jim", Age=20 }; var personTom= new Person() { Name = "Tom", Age = 30 }; var personSam = new Person() { Name = "Sam", Age = 25 }; var personJerry = new Person() { Name = "Jerry", Age = 35 }; var personHenry = new Person() { Name = "Henry ", Age = 26 }; var bookScience = new Book() { Name = "Science", BookDescription= "Science", Isbn="0001" }; var bookMath = new Book() { Name = "Math", BookDescription = "Math", Isbn = "0002" }; var bookPhysics = new Book() { Name = "Physics", BookDescription = "Physics", Isbn = "0003" }; var bookComputer = new Book() { Name = "Computer", BookDescription = "Computer", Isbn = "0004" }; var bookEnglish = new Book() { Name = "English", BookDescription = "English", Isbn = "0005" }; using (var efDbContext = new EFDbContext()) { efDbContext.Person.Add(personJim); efDbContext.Person.Add(personTom); efDbContext.Person.Add(personSam); efDbContext.Person.Add(personJerry); efDbContext.Person.Add(personHenry); efDbContext.Book.Add(bookScience); efDbContext.Book.Add(bookMath); efDbContext.Book.Add(bookPhysics); efDbContext.Book.Add(bookComputer); efDbContext.Book.Add(bookEnglish); efDbContext.SaveChanges(); } } static void Main(string[] args) { Console.WriteLine("Testing start!"); //初始化Person表和Book表的數據 InitializeDataToDB(); //修改Person表和Book表的數據 using (var efDbContext = new EFDbContext()) { //更改Person.Name為Tom的實體的Age屬性值,這會導致personTom這個Person實體的EntityState變為Modified Expression<Func<Person, bool>> expressionTom = p => p.Name == "Tom"; var personTom = efDbContext.Person.First(expressionTom); personTom.Age = 50; //更改Book.Name為Computer的實體的Isbn屬性值,這會導致bookComputer這個Book實體的EntityState變為Modified Expression<Func<Book, bool>> expressionComputer = b => b.Name == "Computer"; var bookComputer = efDbContext.Book.First(expressionComputer); bookComputer.Isbn = "1000"; //由於上面DbContext中有兩個實體的EntityState改變了,下面的SaveChanges方法會觸發兩次DbContext.ChangeTracker.StateChanged事件,在實體數據保存到數據庫之前,自動更新personTom這個Person實體的UpdateTime屬性值為系統當前時間 efDbContext.SaveChanges(); } Console.WriteLine("Testing end!"); Console.ReadLine(); } }
當執行完InitializeDataToDB方法后,數據庫兩張表的值:
Person表:
Book表:
當Program.cs的Main方法運行完畢后,數據庫兩張表的值:
Person表:
Book表:
我們可以看到Person表中列Name為Tom的行,其UpdateTime也被自動更新為了系統當前時間。這樣數據庫中所有帶UpdateTime列的表,其UpdateTime列的值都會在EF Core中自動被更新,省去了很多冗余的代碼。