ExecuteUpdate と ExecuteDelete

Note

この機能は EF Core 7.0 で導入されました。

ExecuteUpdateExecuteDelete は、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

現在、ExecuteUpdateSetProperty ラムダ内の参照ナビゲーションをサポートしていません。 たとえば、各ブログの新しい評価がすべての投稿の評価の平均になるように、すべてのブログの評価を更新するとします。 次のように、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 変更と追跡対象でない変更の両方を混在させないようにすることをお勧めします。

トランザクション

上記に続いて、ExecuteUpdateExecuteDelete は自身が呼び出されたトランザクションを暗黙的に開始しないことを理解することが重要です。 次のコードがあるとします。

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 には、行を読み込んだ時点からそれに対する変更を保存するまでの間に行が変更されなかったことを確認するためにコンカレンシー トークンを使用する自動コンカレンシー制御が用意されています。 ExecuteUpdateExecuteDelete は変更トラッカーと対話しないため、コンカレンシー制御を自動的に適用することができません。

ただし、これらのメソッドはいずれも、操作の影響を受けた行の数を返します。これは、コンカレンシー制御を自分で実装する場合に特に便利です。

// (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>.AddSaveChanges() を使用して行う必要があります。
  • SQL UPDATE ステートメントと DELETE ステートメントを使用すると、影響を受ける行の元の列値を取得できますが、現在これは ExecuteUpdateExecuteDelete ではサポートされていません。
  • これらのメソッドの複数の呼び出しをバッチ処理することはできません。 各呼び出しでは、データベースへの独自のラウンドトリップが実行されます。
  • 通常、データベースでは UPDATE または DELETE を使用して 1 つのテーブルのみを変更できます。
  • 現在、これらのメソッドはリレーショナル データベース プロバイダーでのみ機能します。

その他のリソース