EF Core 大量資料處理 for EFCore.BulkExtensions

以往,大量資料異動我們都知道要使用 Bulk 系列的 API,EFCore.BulkExtensions 除了提供大量資料異動之外,還有查詢後異動、BulkRead(Where In),這裡我將先記錄初步的使用方式,後續有其他心得再補上

 GitHub:https://github.com/borisdj/EFCore.BulkExtensions

EntityFrameworkCore 擴充方法提供以下方法(出自官網):

-Bulk operations (Insert, Update, Delete, Read, Upsert, Sync, SaveChanges)
-Batch ops (Delete, Update) and Truncate.

支援以下幾種資料庫,並針對不同的資料庫實作批量異動(出自官網)

At the moment supports Microsoft SQLServer(2012+) or SqlAzure, PostgreSQL(9.5+) and SQLite.
-SQLServer under the hood uses SqlBulkCopy for Insert, for Update/Delete combines BulkInsert with raw Sql MERGE.
-PostgreSQL is using COPY BINARY combined with ON CONFLICT for Update.
-For SQLite there is no Copy tool, instead library uses plain SQL combined with UPSERT.

 

開發環境

  • Windows 11
  • Rider 2021.3.2
  • EF Core 6
  • docker sqlserver 2019 container

前置準備

準備資料庫 SQL Server 2019 ,Rider 已經把 docker 整合得很好了,第一次需要花費比較久的下載時間

 

sample.dotblog/docker-compose.yml at master · yaochangyu/sample.dotblog (github.com)

 

為節省篇幅,我將 EF Core 的 DbContext / POCO 放在 github

如果你對 DbContext 的使用不熟悉的話可以參考

[EF Core][SQLite]如何使用 EF Core DbContext 以 Microsoft.EntityFrameworkCore.Sqlite 為例 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[EF Core 3] 安裝 EF Core 3 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

接下來,為了說明將使用測試專案來呈現,要記住,這不是一個正確測試案例的寫法

Batch

Batch 擴充方法針對 IQueryable DbSet 進行擴充,它們使用很純的 Sql 完成,所以並沒有使用追蹤;以往,我們要先用 用 EF 用把需要異動的資料挑出來,再進行更新或是刪除,這需要執行兩段 SQL 指令,一段 Select 查詢,另外一段則是 Where Update,Batch 擴充方法,只需要一段 SQL 指令,節省了一段網路開銷

BatchUpdate

查詢後更新

[TestMethod]
public void BatchUpdate()
{
    var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();
    var toDb = GetEmployees(10000);
    var update = new Employee
    {
        Id = Guid.NewGuid(),
        Age = 10,
        CreateBy = "yao",
        CreateAt = DateTimeOffset.Now,
        Name = "yao",
        Remark = "等待更新"
    };
    toDb.Add(update);
    var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true };
    db.BulkInsert(toDb, config);

    var watch = new Stopwatch();
    watch.Restart();

    db.Employees
      .Where(p => p.Id == update.Id)
      .BatchUpdate(new Employee { Remark = "Updated" });

    watch.Stop();

    var count = db.Employees.Count();
    Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}");
}

 

BatchDelete

查詢後刪除

[TestMethod]
public void BatchDelete()
{
    var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();
    var toDb = GetEmployees(10000);
    var update = new Employee
    {
        Id = Guid.NewGuid(),
        Age = 10,
        CreateBy = "yao",
        CreateAt = DateTimeOffset.Now,
        Name = "yao",
        Remark = "等待更新"
    };
    toDb.Add(update);
    var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true };
    db.BulkInsert(toDb, config);

    var watch = new Stopwatch();
    watch.Restart();

    db.Employees
      .Where(p => p.Id == update.Id)
      .BatchDelete();

    watch.Stop();

    var count = db.Employees.Count();
    var isExist = db.Employees.Any(p => p.Id == update.Id);
    Assert.AreEqual(false, isExist);
    Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed},{update.Id} 資料不存在");
}

 

Truncate

使用上沒甚麼難度,指定要刪掉的表(Entity) 就可以了,如下,不過,如果 Entity 有被關聯的話會失敗

context.Truncate<Entity>()

 

Bulk

Bulk 擴充方法針對 DbContext 進行擴充並透過 BulkConfig 設定相關參數

用法

Bulk 擴充了 DbContex 類別,支援非同步,下圖出自官網

 

BulkConfig

borisdj/EFCore.BulkExtensions: Entity Framework Core Bulk Batch Extensions for Insert Update Delete Read (CRUD), Truncate and SaveChanges operations on SQL Server, PostgreSQL, SQLite (github.com)

 

BulkInsert

大量新增,把集合物件丟給 BulkInsert 方法就可以了,這應該沒甚麼太大的難度

[TestMethod]
public void BulkInsert()
{
    var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();
    var toDb = GetEmployees(1000000);

    var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true };

    var watch = new Stopwatch();
    watch.Restart();

    db.BulkInsert(toDb, config);

    watch.Stop();

    var count = db.Employees.Count();
    Console.WriteLine($"資料庫存在筆數={count},共花費={watch.Elapsed}");
}

 

BulkRead

第一次用這方法,看了一下官方文件的說明,才知道原來這是跟 sql where In/ LINQ Contains 的用法效果一樣,但是內部使用了 temp table 實作,理論上會比 LINQ Contains 效能還要好,用法如下

  • items 建立兩個物件 Name="yao1",Name="yao2"
  • UpdateByProperties 描述要關聯那些欄位
[TestMethod]
public void BulkRead()
{
    var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();
    var toDb = GetEmployees(100);
    {
        var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true };
        db.BulkInsert(toDb, config);
    }

    var watch = new Stopwatch();
    watch.Restart();
    {
        var items = new List<Employee>
        {
            new() { Name = "yao1" },
            new() { Name = "yao2" }
        };
        var config = new BulkConfig
        {
            UpdateByProperties = new List<string>
            {
                nameof(Employee.Name),
            },
            UseTempDB = true
        };
        db.BulkRead(items, config);
    }

    watch.Stop();

    Console.WriteLine($"共花費={watch.Elapsed}");
}

 

等效方法如下

[TestMethod]
public void Contains()
{
    var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();
    var toDb = GetEmployees(100);
    {
        var config = new BulkConfig { SetOutputIdentity = false, BatchSize = 4000, UseTempDB = true };
        db.BulkInsert(toDb, config);
    }

    var watch = new Stopwatch();
    watch.Restart();

    var items = new List<string> { "yao1", "yao2" };
    var employees = db.Employees.Where(a => items.Contains(a.Name)).AsNoTracking().ToList(); //SQL IN operator

    watch.Stop();

    Console.WriteLine($"共花費={watch.Elapsed}");
}

 

Transaction

有 using 時發生例外會 Rollback

private static void CleanData()
{
    using var db = TestInstanceManager.EmployeeDbContextFactory.CreateDbContext();

    // db.Truncate<OrderHistory>();
    // db.Truncate<Identity>();
    using var transaction = db.Database.BeginTransaction();

    db.OrderHistories
      .BatchDelete();

    db.Identities
      .BatchDelete();

    // db.Truncate<Employee>();
    db.Employees
      .BatchDelete();

    transaction.Commit();

    // db.Employees
    //   .Where(p => p.Id != Guid.Empty)
    //   .BatchDelete();
    //
    // while (db.Employees.Any())
    // {
    //     var deletedCount = db.Employees
    //                          .Where(p => p.Id != Guid.Empty)
    //                          .Take(1000000)
    //                          .BatchDelete();
    //     var count = db.Employees.Count();
    //     Console.WriteLine($"已刪除 {deletedCount} 筆,剩下 {count} 筆");
    // }
}

 

結論

EFCore.BulkExtensions 套件,提供了微軟所沒有提供的 Batch (查詢後異動)和 Bulk(大量資料異動、Where) 兩個系列的方法,讓我們在使用 ORM 控制資料庫時可以更有效,未來在使用這套件若有碰到狀況再紀錄下來

 

範例位置

sample.dotblog/ORM/EFCore/Lab.EFCoreBulk 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