原文作者:tim_deschryver
翻譯:沙漠盡頭的狼(Google 翻譯加持)
Entity Framework 7 包含了一些備受期待的熱門功能,其中之一是批次操作。Julie Lerman 的一則推文引起了我的注意,我不得不親自試試看。
推文地址:https://twitter.com/julielerman/status/1557743067691569156

為什麼?
那麼,既然我們已經可以更新和刪除實體,為什麼還需要這個功能呢?關鍵字是效能。這是 EF 新版本中一直高居榜首的主題,這次也不例外。
新增的方法從多個方面提升了效能。不再需要先擷取實體並將所有實體儲存在記憶體中,然後才能對它們執行操作,最後再提交給 SQL。現在我們只需一個操作就能完成,並且只產生一個 SQL 命令。
讓我們看看它在程式碼中的樣子。
設定場景
在深入範例之前,我們先設定 SQL 資料庫並填入 3 個資料表:
- Persons: 人
- Addresses: 地址(一個人有一個地址)
- Pets: 寵物(一個人可以養很多寵物)
using Microsoft.EntityFrameworkCore;
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
}
static void SetupAndPopulate(NewInEFContext context)
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Persons.AddRange(Enumerable.Range(1, 1_000).Select(i =>
{
return new Person
{
FirstName = $"{nameof(Person.FirstName)}-{i}",
LastName = $"{nameof(Person.LastName)}-{i}",
Address = new Address
{
Street = $"{nameof(Address.Street)}-{i}",
},
Pets = Enumerable.Range(1, 3).Select(i2 =>
{
return new Pet
{
Breed = $"{nameof(Pet.Breed)}-{i}-{i2}",
Name = $"{nameof(Pet.Name)}-{i}-{i2}",
};
}).ToList()
};
}));
context.SaveChanges();
}
public class NewInEFContext : DbContext
{
public DbSet<Person> Persons { get; set; }
public DbSet<Pet> Pets { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.UseSqlServer("Connectionstring");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Address>()
.Property<long>("PersonId");
modelBuilder.Entity<Pet>()
.Property<long>("PersonId");
}
}
public class Person
{
public long PersonId { get; set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public Address? Address { get; set; }
public List<Pet> Pets { get; set; } = new List<Pet>();
}
public class Address
{
public long AddressId { get; set; }
public string Street { get; set; } = "";
}
public class Pet
{
public long PetId { get; set; }
public string Breed { get; set; } = "";
public string Name { get; set; } = "";
}
ExecuteDelete 與 ExecuteDeleteAsync
既然我們已經解決了這個問題,讓我們深入探討 ExecuteDelete 與 ExecuteDeleteAsync。
要批次刪除一組實體,請使用 Where 方法篩選出要刪除的實體(與先前類似)。然後,呼叫 ExecuteDelete 方法刪除實體集合。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
context.Pets
.Where(p => p.Name.Contains("1"))
.ExecuteDelete();
}
讓我們也看看它產生的 SQL 陳述式:
DELETE FROM [p]
FROM [Pets] AS [p]
WHERE [p].[Name] LIKE N'%1%'
如您所見,它只產生一條 SQL 陳述式來刪除符合條件的實體。這些實體也不再保存在記憶體中。不錯,簡單,高效!
串聯刪除
讓我們再看一個範例,刪除一些持有地址和寵物參考的人。透過刪除人員,我們也刪除了地址和寵物,因為刪除陳述式串聯到外部資料表。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
context.Persons
.Where(p => p.PersonId <= 500)
.ExecuteDelete();
}
與先前類似,這會產生以下 SQL 陳述式:
DELETE FROM [p]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(500 AS bigint)
受影響的行數
還可以檢視刪除操作影響了多少行,ExecuteDelete 會傳回受影響的行數。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
var personsDeleted =
context.Persons
.Where(p => p.PersonId <= 100)
.ExecuteDelete();
}
在上面的表達式中,personsDeleted 變數等於 100。
ExecuteUpdate 與 ExecuteUpdateAsync
現在我們已經瞭解如何刪除實體,讓我們探索如何更新它們。就像 ExecuteDelete 一樣,我們首先必須篩選出想要更新的實體,然後呼叫 ExecuteUpdate。
要更新實體,我們需要使用新的 SetProperty 方法。SetProperty 的第一個參數是透過 Lambda 選取需要更新的屬性,第二個參數也使用 Lambda 選取該屬性的新值。
例如,讓我們將人員的姓氏設為 "Updated"。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
context.Persons
.Where(p => p.PersonId <= 1_000)
.ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated"));
}
這會產生對應的 SQL 陳述式:
UPDATE [p]
SET [p].[LastName] = N'Updated'
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)
我們還可以存取實體的值並用它來建立新值。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
context.Persons
.Where(p => p.PersonId <= 1_000)
.ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated" + x.LastName));
}
產生以下 SQL 陳述式:
UPDATE [p]
SET [p].[LastName] = N'Updated' + [p].[LastName]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)
一次更新多個值
我們甚至可以透過多次呼叫 SetProperty 來一次更新多個屬性。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
context.Persons
.Where(p => p.PersonId <= 1_000)
.ExecuteUpdate(p =>
p.SetProperty(x => x.LastName, x => "Updated" + x.LastName)
.SetProperty(x => x.FirstName, x => "Updated" + x.FirstName));
}
再一次,對應的 SQL 陳述式:
UPDATE [p]
SET [p].[FirstName] = N'Updated' + [p].[FirstName],
[p].[LastName] = N'Updated' + [p].[LastName]
FROM [Persons] AS [p]
WHERE [p].[PersonId] <= CAST(1000 AS bigint)
受影響的行數
就像 ExecuteDelete 一樣,ExecuteUpdate 也會傳回受影響的行數。
using (var context = new NewInEFContext())
{
SetupAndPopulate(context);
var personsUpdated =
context.Persons
.Where(p => p.PersonId <= 1_000)
.ExecuteUpdate(p => p.SetProperty(x => x.LastName, x => "Updated"));
}
請注意,不支援更新巢狀實體。
Entity Framework 7 中的更多更新
有關新功能的完整清單,請參閱 EF 7 計劃。