ExecuteUpdate と ExecuteDelete
Note
この機能は EF Core 7.0 で導入されました。
ExecuteUpdate と ExecuteDelete は、EF の従来の変更追跡と SaveChanges() メソッドを使用することなくデータをデータベースに保存するための方法の 1 つです。 これら 2 つの手法の紹介比較については、データの保存に関する概要ページを参照してください。
ExecuteDelete
評価が特定のしきい値を下回るすべてのブログを削除する必要があるとします。 従来の SaveChanges() の方法では、次のことを行う必要があります。
foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
context.Blogs.Remove(blog);
}
context.SaveChanges();
これは、このタスクを実行するには非効率的な方法です。フィルターに一致するすべてのブログについてデータベースに対してクエリを実行し、それらのインスタンスをすべて照会、具体化、追跡します。一致するエンティティの数は膨大になる可能性があります。 次に、EF の変更トラッカーに、各ブログを削除する必要があることを伝え、SaveChanges() を呼び出してこれらの変更を適用します。これにより、それらすべてに対してそれぞれ DELETE
ステートメントが生成されます。
以下は、同じタスクを ExecuteDelete API を介して実行したものです。
context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();
これは、使い慣れた LINQ 演算子を使用して、どのブログに影響を及ぼすかを決定し (それらにクエリを実行する場合と同様に)、データベースに対して SQL DELETE
を実行するように EF に指示します。
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
このため、よりシンプルで簡潔になるだけでなく、データベースでの実行が非常に効率的になり、データベースからデータを読み込んだり、EF の変更トラッカーを使用したりする必要がありません。 任意の LINQ 演算子を使用して、削除するブログを選択できることに注意してください。これらは、これらのブログにクエリを実行する場合と同様に、データベースで実行するために SQL に変換されます。
ExecuteUpdate
これらのブログを削除するのではなく、代わりに非表示にする必要があることを示すようにプロパティを変更したい場合はどうすればよいでしょうか。 ExecuteUpdate では、同様の方法で SQL UPDATE
ステートメントを表現できます。
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));
ExecuteDelete
と同様に、まず LINQ を使用して影響を受けるブログを決定しますが、ExecuteUpdate
では、一致するブログに適用される変更を表現する必要もあります。 これを行うには、ExecuteUpdate
呼び出し内で SetProperty
を呼び出し、それに次の 2 つの引数を指定します: 変更されるプロパティ (IsVisible
) と持つべき新しい値 (false
)。 その結果、次の SQL が実行されます。
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
複数のプロパティの更新
ExecuteUpdate
では、1 回の呼び出しで複数のプロパティを更新できます。 たとえば、IsVisible
を false に設定し、Rating
をゼロに設定するには、追加の SetProperty
呼び出しを連結するだけです。
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));
これにより、次の SQL が実行されます。
UPDATE [b]
SET [b].[Rating] = 0,
[b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
既存のプロパティ値の参照
上記の例では、プロパティが新しい定数値に更新されました。 また、ExecuteUpdate
では、新しい値を計算するときに既存のプロパティ値を参照することもできます。たとえば、一致するすべてのブログの評価を 1 つ上げるには、以下を使用します。
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
SetProperty
に対する 2 番目の引数は今度はラムダ関数になり、以前のように定数ではないことに注意してください。 その b
パラメータは更新されるブログを表します。したがって、そのラムダ内では、 b.Rating
には変更が発生する前の評価が含まれます。 これにより、次の SQL が実行されます。
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
ナビゲーションと関連エンティティ
現在、ExecuteUpdate
は SetProperty
ラムダ内の参照ナビゲーションをサポートしていません。 たとえば、各ブログの新しい評価がすべての投稿の評価の平均になるように、すべてのブログの評価を更新するとします。 次のように、ExecuteUpdate
の使用を試行できます。
context.Blogs.ExecuteUpdate(
setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));
ただし、EF では、最初に Select
を使用して平均評価を計算し、それを匿名型に射影し、それに対して ExecuteUpdate
を使用することで、この操作を実行できます。
context.Blogs
.Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
.ExecuteUpdate(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));
これにより、次の SQL が実行されます。
UPDATE [b]
SET [b].[Rating] = CAST((
SELECT AVG(CAST([p].[Rating] AS float))
FROM [Post] AS [p]
WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]
変更の追跡
SaveChanges
を使い慣れたユーザーは、複数の変更を実行し、SaveChanges
を呼び出してこれらのすべての変更をデータベースに適用することに慣れています。これは、これらの変更を累積 (または追跡) する EF の変更トラッカーによって可能になります。
ExecuteUpdate
および ExecuteDelete
の動作はまったく異なります。これらは、呼び出された時点ですぐに有効になります。 つまり、1 つの ExecuteUpdate
または ExecuteDelete
操作が複数の行に影響を与えることができる一方で、SaveChanges
を呼び出すときなど、このような操作を複数蓄積してそれらを一度に適用することはできません。 実際、関数は EF の変更トラッカーを完全に認識せず、やり取りも一切行われません。 これには、いくつかの重要な結果があります。
次のコードがあるとします。
// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;
// 4. Persist tracked changes to the database.
context.SaveChanges();
重要なのは、ExecuteUpdate
が呼び出され、データベース内のすべてのブログが更新されるとき、EF の変更トラッカーは更新されず、追跡される .NET インスタンスには、クエリが実行された時点の元の評価値が残ります。 ブログの評価がもともと 5 であったとします。3 番目の行の実行後、データベース内の評価は 6 になりますが (ExecuteUpdate
が原因)、追跡対象の .NET インスタンスの評価は 7 になります。 SaveChanges
が呼び出されると、EF により新しい値 7 が元の値 5 と異なっていることが検出され、その変更が保持されます。 ExecuteUpdate
によって実行される変更は上書きされ、考慮されません。
その結果、通常は、ExecuteUpdate
/ExecuteDelete
を使用して追跡対象の SaveChanges
変更と追跡対象でない変更の両方を混在させないようにすることをお勧めします。
トランザクション
上記に続いて、ExecuteUpdate
と ExecuteDelete
は自身が呼び出されたトランザクションを暗黙的に開始しないことを理解することが重要です。 次のコードがあるとします。
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();
各 ExecuteUpdate
呼び出しにより、1 つの SQL UPDATE
がデータベースに送信されます。 トランザクションは作成されないので、何らかの障害によって 2 つ目の ExecuteUpdate
が正常に完了されない場合でも、1 つ目のものの影響が引き続きデータベースに保持されます。 実際、上記の 4 つの操作 (ExecuteUpdate
の 2 つの呼び出し、クエリ、および SaveChanges
) では、各操作はそれぞれ独自のトランザクション内で実行されます。 1 つのトランザクションで複数の操作をラップするには、DatabaseFacade を使用して 1 つのトランザクションを明示的に開始します。
using (var transaction = context.Database.BeginTransaction())
{
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
...
}
トランザクション処理の詳細については、「トランザクションの使用」を参照してください。
コンカレンシー制御と影響を受ける行
SaveChanges
には、行を読み込んだ時点からそれに対する変更を保存するまでの間に行が変更されなかったことを確認するためにコンカレンシー トークンを使用する自動コンカレンシー制御が用意されています。 ExecuteUpdate
と ExecuteDelete
は変更トラッカーと対話しないため、コンカレンシー制御を自動的に適用することができません。
ただし、これらのメソッドはいずれも、操作の影響を受けた行の数を返します。これは、コンカレンシー制御を自分で実装する場合に特に便利です。
// (load the ID and concurrency token for a Blog in the database)
var numUpdated = context.Blogs
.Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
.ExecuteUpdate(/* ... */);
if (numUpdated == 0)
{
throw new Exception("Update failed!");
}
このコードでは、LINQ Where
演算子を使用して、特定のブログに更新プログラムを適用します。そのコンカレンシー トークンに特定の値がある場合に限ります (データベースからブログに対してクエリを実行したときに表示されたものなど)。 その後、ExecuteUpdate
によって実際に更新された行の数を確認します。結果がゼロの場合、行は更新されておらず、同時更新の結果としてコンカレンシー トークンが変更されている可能性があります。
制限事項
- 現在、更新と削除のみがサポートされています。挿入は DbSet<TEntity>.Add と SaveChanges() を使用して行う必要があります。
- SQL UPDATE ステートメントと DELETE ステートメントを使用すると、影響を受ける行の元の列値を取得できますが、現在これは
ExecuteUpdate
とExecuteDelete
ではサポートされていません。 - これらのメソッドの複数の呼び出しをバッチ処理することはできません。 各呼び出しでは、データベースへの独自のラウンドトリップが実行されます。
- 通常、データベースでは UPDATE または DELETE を使用して 1 つのテーブルのみを変更できます。
- 現在、これらのメソッドはリレーショナル データベース プロバイダーでのみ機能します。
その他のリソース
ExecuteUpdate
とExecuteDelete
については、.NET Data Access Community Standup セッションを参照してください。
.NET