背景介紹
在我們的日常開發中,有時候需要記錄數據庫表中值的變化, 這時候我們通常會使用觸發器或者使用關系型數據庫中臨時表(Temporal Table)或數據變更捕獲(Change Data Capture)特性來記錄數據庫表中字段的值變化。原文的作者Gérald Barré講解了如何使用Entity Freamwork Core上下文中的ChangeTracker來獲取並保存實體的變化記錄。
ChangeTracker
ChangeTracker是Entity Framework Core記錄實體變更的核心對象(這一點和以前版本的Entity Framework一致)。當你使用Entity Framework Core進行獲取實體對象、添加實體對象、刪除實體對象、更新實體對象、附加實體對象等操作時,ChangeTracker都會記錄下來對應的實體引用和對應的實體狀態。
我們可以通過ChangeTracker.Entries()方法, 獲取到當前上下文中使用的所有實體對象, 以及每個實體對象的狀態屬性State。
Entity Framework Core中可用的實體狀態屬性有以下幾種
- Detached
- Unchanged
- Deleted
- Modified
- Added
所以如果我們要記錄實體的變更,只需要從ChangeTracker中取出所有Added, Deleted, Modified狀態的實體, 並將其記錄到一個日志表中即可。
我們的目標
我們以下面這個例子為例。
當前我們有一個顧客表Customer和一個日志表Audit, 其對應的實體對象及Entity Framework上下文如下:
Audit.cs
[Table("Audit")]
public class Audit
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string TableName { get; set; }
public DateTime DateTime { get; set; }
public string KeyValues { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
}
Customer.cs
[Table("Customer")]
public class Customer
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
SampleContext.cs
public class SampleContext : DbContext
{
public SampleContext()
{
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Audit> Audits { get; set; }
}
我們希望當執行以下代碼之后, 在Audit表中產生如下數據
class Program
{
static void Main(string[] args)
{
using (var context = new SampleContext())
{
// Insert a row
var customer = new Customer();
customer.FirstName = "John";
customer.LastName = "doe";
context.Customers.Add(customer);
context.SaveChangesAsync().Wait();
// Update the first customer
customer.LastName = "Doe";
context.SaveChangesAsync().Wait();
// Delete the customer
context.Customers.Remove(customer);
context.SaveChangesAsync().Wait();
}
}
}

實現步驟
復寫上下文SaveChangeAsync方法
首先我們添加一個AuditEntry類, 來生成變更記錄。
public class AuditEntry
{
public AuditEntry(EntityEntry entry)
{
Entry = entry;
}
public EntityEntry Entry { get; }
public string TableName { get; set; }
public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();
public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();
public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();
public List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();
public bool HasTemporaryProperties => TemporaryProperties.Any();
public Audit ToAudit()
{
var audit = new Audit();
audit.TableName = TableName;
audit.DateTime = DateTime.UtcNow;
audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
return audit;
}
}
代碼解釋
- Entry屬性表示變更的實體
- TableName屬性表示實體對應的數據庫表名
- KeyValues屬性表示所有的主鍵值
- OldValues屬性表示當前實體所有變更屬性的原始值
- NewValues屬性表示當前實體所有變更屬性的新值
- TemporaryProperties屬性表示當前實體所有由數據庫生成的屬性集合
然后我們打開SampleContext.cs, 復寫方法SaveChangeAsync代碼如下。
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
var auditEntries = OnBeforeSaveChanges();
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
await OnAfterSaveChanges(auditEntries);
return result;
}
private List<AuditEntry> OnBeforeSaveChanges()
{
throw new NotImplementedException();
}
private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
{
throw new NotImplementedException();
}
代碼解釋
- 這里我們添加了2個方法
OnBeforeSaveChange()和OnAfterSaveChanges。 OnBeforeSaveChanges是用來獲取所有需要記錄的實體OnAfterSaveChanges是為了獲得實體中數據庫生成列的新值(例如自增列, 計算列)並持久化變更記錄, 這一步必須放置在調用父類SaveChangesAsync之后,因為只有持久化之后,才能獲取自增列和計算列的新值。- 在
OnBeforeSaveChange方法之后,OnAfterSaveChanges方法之前, 我們調用父類的SaveChangesAsync來保存實體變更。
然后我們來修改OnBeforeSaveChanges方法, 代碼如下
private List<AuditEntry> OnBeforeSaveChanges()
{
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
continue;
var auditEntry = new AuditEntry(entry);
auditEntry.TableName = entry.Metadata.Relational().TableName;
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
if (property.IsTemporary)
{
// value will be generated by the database, get the value after saving
auditEntry.TemporaryProperties.Add(property);
continue;
}
string propertyName = property.Metadata.Name;
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
switch (entry.State)
{
case EntityState.Added:
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
case EntityState.Deleted:
auditEntry.OldValues[propertyName] = property.OriginalValue;
break;
case EntityState.Modified:
if (property.IsModified)
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
break;
}
}
}
}
代碼解釋
ChangeTracker.DetectChanges()是強制上下文再做一次變更檢查- 由於Audit表也在ChangeTracker的管理中, 所以在
OnBeforeSaveChanges方法中,我們需要將Audit表的實體排除掉,否則會出現死循環 - 這里我們只需要操作所有Added, Modified, Deleted狀態的實體,所以Detached和Unchanged狀態的實體需要排除掉
- ChangeTracker中記錄的每個實體都有一個
Properties集合,里面記錄的每個實體所有屬性的狀態, 如果某個屬性被修改了,則該屬性的IsModified是true. - 實體屬性Property對象中的
IsTemporary屬性表明了該字段是不是數據庫生成的。 我們將所有數據庫生成的屬性放到了TemplateProperties集合中,供OnAfterSaveChanges方法遍歷 - 我們可以通過Property對象的
Metadata.IsPrimaryKey()方法來獲得當前字段是不是主鍵字段 - Property對象的CurrentValue屬性表示當前字段的新值,OriginalValue屬性表示當前字段的原始值
最后我們修改一下OnAfterSaveChanges, 代碼如下
private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
{
if (auditEntries == null || auditEntries.Count == 0)
return Task.CompletedTask;
foreach (var auditEntry in auditEntries)
{
// Get the final value of the temporary properties
foreach (var prop in auditEntry.TemporaryProperties)
{
if (prop.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
}
else
{
auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
}
}
// Save the Audit entry
Audits.Add(auditEntry.ToAudit());
}
return SaveChangesAsync();
}
代碼解釋
- 在
OnBeforeSaveChanges中,我們記錄下了當前實體所有需要數據庫生成的屬性。 在調用父類的SaveChangesAsync方法, 我們可以獲取通過property的CurrentValue屬性獲得到這些數據庫生成屬性的新值 - 記錄下新值,之后我們生成變更實體記錄Audit,並添加到上下文中,再次調用SaveChangesAsync方法,將其持久化
當前方案的問題和適合的場景
- 這個方案中,整個數據庫持久化並不在一個原子事務中,我們都知道Entity Framework的SaveChangesAsync方法是自帶事務的,但是調用2次SaveChangeAsync就不是一個事務作用域了,可能出現實體保存成功,Audit實體保存失敗的情況
- 由於調用了2次SaveChangeAsync方法,所以Audit實體中的DateTime屬性並不能確切的反映保存實體操作的真正時間, 中間間隔了第一次SaveChangeAsync花費的時間(個人認為在
OnBeforeSaveChanges中就可以生成這個DateTime讓時間更精確一些) - 如果所有實體屬性值都是預生成的,非數據庫生成的,作者這個方案還是非常好的,但是如果有數據庫自增列或計算列, 還是使用關系型數據庫中臨時表(Temporal Table)或數據變更捕獲(Change Data Capture)特性比較合理
