如何使用 ChangeTracking 追蹤物件變化再透過 EFCore.BulkExtensions 存到資料庫

當我們需要異動資料庫某一筆資料的某些欄位,可以通過追蹤機制來記錄那些欄位被改變了,除了自己開發之外,還可以考慮 ChangeTracking,它是一套可以輕易的幫我們追蹤物件、集合的利器,不過,有一點可惜的是他最後一次更新是在 2019

接下來,我想要利用這個追蹤機制幫我完成資料表的部分更新,有異動的欄位才更新。

追蹤異動變化使用 ChangeTracking ,消化異動並存放到操作資料庫則使用 EF / EF Core,當然,這不受限,你可以挑選妳喜歡的控制方式,接著,來看看怎麼實現它吧。

當我們需要用 Model (DTO) 異動資料庫某一筆資料的某些欄位,有幾種策略:

  1. 整筆更新,最簡單無腦,也最花費效能
  2. 使用 EF 的追蹤,更新前必須先從資料庫讀取一次,然後再把狀態蓋過去,由 EF 會幫你產生異動語法
  3. 根據追蹤紀錄組合 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,需要一些基礎背景,如果不懂的可以先參考我之前寫的文章

這裡我利用 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 的查詢追蹤再幫我們產生出差異語法,似乎是比較好的解法。

專案位置

sample.dotblog/Property Change Tracking/ChangeTracking at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo