當我們需要異動資料庫某一筆資料的某些欄位,可以通過追蹤機制來記錄那些欄位被改變了,除了自己開發之外,還可以考慮 ChangeTracking,它是一套可以輕易的幫我們追蹤物件、集合的利器,不過,有一點可惜的是他最後一次更新是在 2019
接下來,我想要利用這個追蹤機制幫我完成資料表的部分更新,有異動的欄位才更新。
追蹤異動變化使用 ChangeTracking ,消化異動並存放到操作資料庫則使用 EF / EF Core,當然,這不受限,你可以挑選妳喜歡的控制方式,接著,來看看怎麼實現它吧。
當我們需要用 Model (DTO) 異動資料庫某一筆資料的某些欄位,有幾種策略:
- 整筆更新,最簡單無腦,也最花費效能
- 使用 EF 的追蹤,更新前必須先從資料庫讀取一次,然後再把狀態蓋過去,由 EF 會幫你產生異動語法
- 根據追蹤紀錄組合 SQL 更新語法
這次我的實作,想要透過 ChangeTracking 的機制來完成部分更新,Layer 的職責如下:
- Domain Layer 的職責:處理狀態的追蹤
- Repository Layer 的職責:根據狀態變化異動到資料庫
開發環境
- Windows 11
- .NET 6
- Rider 2021.3.3
- ChangeTracking 2.2.17
- EFCore.BulkExtensions 6.2.6
- Microsoft.EntityFrameworkCore 6.0.1
ChangeTracking
使用步驟
- 屬性加上 virtual
- 實例化物件並呼叫 AsTrackable() 方法,針對它進行操作
Order order = new Order(){}
Order trackedOrder = order.AsTrackable();
- 異動屬性
trackedOrder.Name="yao";
- 取出追蹤物件
var trackable = (IChangeTrackable<Order>)trackedOrder;
// same as
var trackable = trackedOrder.CastToIChangeTrackable();
- 取出異動欄位
var changedProperties = trackable.ChangedProperties;
附帶一提,除了 class,record 也可以適用
這裡有一個簡單的範例,依照上述的步驟,最後我把異動的欄位印出來
[TestMethod]
public void 追蹤()
{
var source = new EmployeeEntity
{
Id = Guid.NewGuid(),
Name = "yao",
Age = 12,
Identity = new IdentityEntity { Account = "G1234" },
};
var trackable = source.AsTrackable();
trackable.Name = "小章";
var employTrackable = trackable.CastToIChangeTrackable();
var employeeChangedProperties = employTrackable.ChangedProperties;
Console.WriteLine($"{nameof(this.追蹤)}:追蹤 Employee 欄位");
Console.WriteLine(ToJson(employeeChangedProperties));
}
為了節省篇幅,資料結構請參考
執行結果如下,得到異動欄位 Name
追蹤複雜型別的範例如下
[TestMethod]
public void 追蹤複雜型別()
{
var source = new EmployeeEntity
{
Id = Guid.NewGuid(),
Name = "yao",
Age = 12,
Identity = new IdentityEntity { Account = "G1234" },
};
var trackable = source.AsTrackable();
trackable.Name = "小章";
trackable.Identity.Account = "yao";
var employTrackable = trackable.CastToIChangeTrackable();
var identityTrackable = trackable.Identity.CastToIChangeTrackable();
var employeeChangedProperties = employTrackable.ChangedProperties;
var identityChangedProperties = identityTrackable.ChangedProperties;
Console.WriteLine($"{nameof(this.追蹤複雜型別)}:追蹤欄位");
Console.WriteLine(ToJson(employeeChangedProperties));
Console.WriteLine(ToJson(identityChangedProperties));
}
執行結果如下
追蹤集合就複雜了許多,但是還是可以把異動資訊取出來
public void 追蹤集合()
{
var source = new EmployeeEntity
{
Id = Guid.NewGuid(),
Name = "yao",
Age = 12,
Identity = new IdentityEntity { Account = "G1234" },
Profiles = new List<ProfileEntity>
{
new() { FirstName = "第一筆" },
new() { FirstName = "將被刪掉" },
}
};
var trackable = source.AsTrackable();
trackable.Profiles[0].FirstName = "變更";
trackable.Profiles.Add(new ProfileEntity { FirstName = "新增" });
trackable.Profiles.RemoveAt(1);
var profileTrackable = trackable.Profiles.CastToIChangeTrackableCollection();
var unchangedItems = profileTrackable.UnchangedItems;
var addedItems = profileTrackable.AddedItems;
var changedItems = profileTrackable.ChangedItems;
var deleteItems = profileTrackable.DeletedItems;
Console.WriteLine($"{nameof(this.追蹤集合)}:追蹤 Profiles 集合");
Console.WriteLine($"UnchangedItems:{ToJson(unchangedItems)}");
Console.WriteLine($"AddItem:{ToJson(addedItems)}");
Console.WriteLine($"ChangedItems:{ToJson(changedItems)}");
Console.WriteLine($"DeleteItems:{ToJson(deleteItems)}");
Console.WriteLine($"{nameof(this.追蹤集合)}:追蹤 Profiles[0] 變更屬性");
var changeTrackable = trackable.Profiles[0].CastToIChangeTrackable();
Console.WriteLine($"Profiles[0] 變更欄位:{ToJson(changeTrackable.ChangedProperties)}");
}
執行結果如下:
儲存異動資訊到資料庫
接下來,我會使用 EF Core,需要一些基礎背景,如果不懂的可以先參考我之前寫的文章
- [EF Core][SQLite]如何使用 EF Core DbContext 以 Microsoft.EntityFrameworkCore.Sqlite 為例 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- EF Core 大量資料處理 for EFCore.BulkExtensions | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- [EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- [EF Core 3] 如何使用 Code First 定義資料庫結構 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
這裡我利用 borisdj/EFCore.BulkExtensions 完成部分更新,把需要更新的欄位傳給 BatchUpdate 方法就可以了
這裡我想要呈現,追蹤物件跨方法的傳遞
@EmployeeAggregate.cs
這裡啟用追蹤,並異動資訊,最後傳給 SaveChangeAsync
public async Task<EmployeeEntity> ModifyFlowAsync(EmployeeEntity srcEmployee, CancellationToken cancel = default)
{
var memberTrackable = srcEmployee.AsTrackable();
memberTrackable.Name = "小章";
memberTrackable.Identity.Password = "9527";
var changeCount = await this._repository.SaveChangeAsync(memberTrackable, cancel);
return memberTrackable;
}
完整代碼請參考:sample.dotblog/EmployeeAggregate.cs at master · yaochangyu/sample.dotblog (github.com)
@EmployeeRepository.cs
這裡取出追蹤物件並拿到異動欄位,最後再調用 BatchUpdate
public async Task<int> SaveChangeAsync(EmployeeEntity srcEmployee,
CancellationToken cancel = default)
{
var employeeTrackable = srcEmployee.CastToIChangeTrackable();
var identityTrackable = srcEmployee.Identity.CastToIChangeTrackable();
var memberChangeProperties = employeeTrackable.ChangedProperties.ToList();
var identityChangeProperties = identityTrackable.ChangedProperties.ToList();
await using var dbContext = await this._memberContextFactory.CreateDbContextAsync(cancel);
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancel);
try
{
var destEmployee = this.To(srcEmployee);
var memberChangeCount = await dbContext.Employees
.Where(a => a.Id == srcEmployee.Id)
.BatchUpdateAsync(destEmployee,
memberChangeProperties, cancel);
var identityChangeCount = await dbContext.Identities
.Where(a => a.Employee_Id == srcEmployee.Id)
.BatchUpdateAsync(destEmployee.Identity,
identityChangeProperties,
cancel);
await transaction.CommitAsync(cancel);
return memberChangeCount + identityChangeCount;
}
catch (Exception e)
{
await transaction.RollbackAsync(cancel);
throw new Exception("存檔失敗");
}
return 0;
}
完整代碼請參考:sample.dotblog/EmployeeRepository.cs at master · yaochangyu/sample.dotblog (github.com)
調用端呼叫 ModifyFlowAsync 方法並驗證資料庫的內容
[TestMethod]
public void 異動追蹤後存檔()
{
var toDB = Insert();
var source = new EmployeeEntity
{
Id = toDB.Id,
Name = "yao",
Age = 12,
Identity = new IdentityEntity(),
};
var employeeEntity = this._employeeAggregate.ModifyFlowAsync(source).Result;
this.DataShouldOk(source);
}
private void DataShouldOk(EmployeeEntity source)
{
var dbContext = this._employeeDbContextFactory.CreateDbContext();
var actual = dbContext.Employees
.Where(p => p.Id == source.Id)
.Include(p => p.Identity)
.First()
;
Assert.AreEqual("小章", actual.Name);
Assert.AreEqual("9527", actual.Identity.Password);
}
private static Employee Insert()
{
using var dbContext = TestAssistants.EmployeeDbContextFactory.CreateDbContext();
var toDB = new Employee
{
Id = Guid.NewGuid(),
Age = 18,
Name = "yao",
CreateAt = DateTimeOffset.Now,
CreateBy = "TEST",
Identity = new Identity
{
Account = "yao",
Password = "123456",
CreateAt = DateTimeOffset.Now,
CreateBy = "TEST",
}
};
dbContext.Employees.Add(toDB);
dbContext.SaveChanges();
return toDB;
}
完整代碼請參考:sample.dotblog/ChangeTrackingUnitTest.cs at master · yaochangyu/sample.dotblog (github.com)
結論
BatchUpdate 沒有支援複雜結構,沒有辦法一次把異動資訊傳給資料庫,要異動的資料表越多,就需要多次的網路開銷;反而,在異動資料表比較多的場景下,先透過 EF /EF Core 的查詢追蹤再幫我們產生出差異語法,似乎是比較好的解法。
專案位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET