ExecuteUpdate over projection uses derived-table copy of updated column, causing possible lost updates

オープン
#38,505 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
静か
技術スタック
csharp, postgresql
領域
databases

調査の方向性

まず、提供された ExecuteUpdateAsync LINQ クエリを再現し、生成された PostgreSQL SQL を調査します。ExecuteUpdate の変換経路を追跡して、派生テーブルが更新対象の列を提供する理由を特定します。完了の条件は、SET 式が更新対象のエイリアスを参照し、同時実行されたインクリメントが合算された合計を維持することです。

索引モデルが issue の本文から書いたものです。

説明

area-bulkupdate customer-reported
Bug description

ExecuteUpdate over a projection can generate SQL where an increment expression uses a value read into a derived table instead of referencing the actual update target alias.

This can cause lost updates under concurrent executions.

The LINQ expression represents an atomic increment:

target.TotalQuantity = target.TotalQuantity + delta

But the generated SQL uses:

SET "TotalQuantity" = s2."TotalQuantity" + s2."QuantityDelta"

where s2."TotalQuantity" is read inside a derived table before the update.

For concurrent counter updates, the generated SQL should reference the update target alias:

SET "TotalQuantity" = s1."TotalQuantity" + s2."QuantityDelta"

because s1 is the actual row being updated.

Example lost-update scenario:

Initial TotalQuantity = 100

Transaction 1 delta = 10
Transaction 2 delta = 20

Both transactions can read s2.TotalQuantity = 100.

Transaction 1 updates row to 110.
Transaction 2 waits for the same row, then updates row to 100 + 20 = 120.

Expected final value: 130.
Possible final value: 120.
Your code
await context.ShortageOrderLines
    .Where(line => line.ShortageOrderId == orderId)
    .GroupBy(line => line.ShortageArticleTask!)
    .Select(g => new
    {
        Task = g.Key,
        QuantityDelta = g.Sum(l => l.Quantity)
    })
    .ExecuteUpdateAsync(setters => setters.SetProperty(
        t => t.Task.TotalQuantity,
        t => t.Task.TotalQuantity + t.QuantityDelta));

Generated SQL:

UPDATE shortage."ShortageArticleTasks" AS s1
SET "TotalQuantity" = s2."TotalQuantity" + s2."QuantityDelta"
FROM (
    SELECT s0."Id", s0."TotalQuantity", COALESCE(sum(s."Quantity"), 0.0) AS "QuantityDelta"
    FROM shortage."ShortageOrderLines" AS s
    LEFT JOIN shortage."ShortageArticleTasks" AS s0 ON s."ShortageArticleTaskId" = s0."Id"
    WHERE s."ShortageOrderId" = @orderId
    GROUP BY s0."Id", s0."ArticleId", s0."LogisticVariantId", s0."TotalQuantity"
) AS s2
WHERE s1."Id" = s2."Id"

Expected SQL shape:

UPDATE shortage."ShortageArticleTasks" AS s1
SET "TotalQuantity" = s1."TotalQuantity" + s2."QuantityDelta"
FROM (
    SELECT
        s."ShortageArticleTaskId",
        COALESCE(sum(s."Quantity"), 0.0) AS "QuantityDelta"
    FROM shortage."ShortageOrderLines" AS s
    WHERE s."ShortageOrderId" = @orderId
    GROUP BY s."ShortageArticleTaskId"
) AS s2
WHERE s1."Id" = s2."ShortageArticleTaskId"
Stack traces

No exception is thrown. The generated SQL executes successfully, but it may have incorrect concurrency semantics for counter increments.

EF Core version

10.0.8

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

net10.0

Operating system

Amazon Linux

IDE

VS 2026

主要言語
C#
スター
14.8k
フォーク
3.4k
平均マージ
3日 18時間
マージ済み PR(30日)
159

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/efcore のほかの issue

dotnet/efcore の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。