以往,大量資料異動我們都知道要使用 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
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