87

UPDATEwithOUTPUTクエリを実行しています:

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

このステートメントは問題ありません。テーブルでトリガーが定義されるまで。次に、私のUPDATEステートメントはエラー334を取得します。

ステートメントに INTO 句のない OUTPUT 句が含まれている場合、DML ステートメントのターゲット テーブル 'BatchReports' は有効なトリガーを持つことはできません

現在、この問題は、SQL Server チームによるブログ投稿 ( UPDATE with OUTPUT 句 – Triggers – および SQLMoreResults ) で説明されています。

エラーメッセージは自明です

また、解決策も提供します。

アプリケーションは、INTO 句を使用するように変更されました

ただし、ブログ投稿全体の頭も尻尾も作ることはできません。

では、私の質問をさせてください:UPDATE動作させるには、何に変更すればよいでしょうか?

こちらもご覧ください

4

3 に答える 3

63

可視性に関する警告:他の回答をしないでください。誤った値が返されます。なぜそれが間違っているのかを読んでください。


SQL Server 2008 R2 での作業に必要な手間を考慮して、クエリを次のように変更しましUPDATEた。OUTPUT

UPDATE BatchReports  
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

に:

SELECT BatchFileXml, ResponseFileXml, ProcessedDate FROM BatchReports
WHERE BatchReports.BatchReportGUID = @someGuid

UPDATE BatchReports
SET IsProcessed = 1
WHERE BatchReports.BatchReportGUID = @someGuid

基本的に使用をやめOUTPUTました。Entity Framework 自体がこれとまったく同じハックを使用するため、これはそれほど悪くはありません。

うまくいけば、 2012年、 2014年、 2016年、 2018年、 2019年、2020年がより良い実装になるでしょう。


更新: OUTPUT の使用は有害です

最初の問題は、OUTPUT句を使用してテーブルの「後」の値を取得しようとしたことでした。

UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid

次に、SQL Serverのよく知られた制限 ( 「修正しない」バグ) にぶつかります。

DML ステートメントのターゲット テーブル 'BatchReports' は、ステートメントに INTO 句のない OUTPUT 句が含まれている場合、有効なトリガーを持つことはできません

回避策の試み #1

TABLEそのため、中間変数を使用してOUTPUT結果を保持する何かを試します。

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion timestamp, 
   BatchReportID int
)
  
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

timestampただし、テーブルに a を挿入することは許可されていないため(一時テーブル変数であっても)、失敗します。

回避策の試み #2

timestampaが実際には 64 ビット (別名 8 バイト) の符号なし整数であることは密かに知っています。一時テーブルの定義を次のbinary(8)代わりに使用するように変更できtimestampます。

DECLARE @t TABLE (
   LastModifiedDate datetime,
   RowVersion binary(8), 
   BatchReportID int
)
  
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
INTO @t
WHERE BatchReports.BatchReportGUID = @someGuid

SELECT * FROM @t

そして、値が間違っていることを除いて、それは機能します。

返されるタイムスタンプRowVersionは、UPDATE が完了した後に存在していたタイムスタンプの値ではありません。

  • 返されたタイムスタンプ:0x0000000001B71692
  • 実際のタイムスタンプ:0x0000000001B71693

これはOUTPUT、テーブルへの値が UPDATE ステートメントの最後にあった値ではないためです。

  • UPDATE ステートメントの開始
    • 行を変更します
      • タイムスタンプが更新されます(例: 2 → 3)
    • OUTPUT は新しいタイムスタンプ(つまり 3)を取得します
    • トリガーラン
      • 行を再度変更します
        • タイムスタンプが更新されます(例: 3 → 4)
  • UPDATE ステートメントの完了
  • OUTPUT は3を返します (間違った値)

これの意味は:

  • UPDATE ステートメントの最後に存在するため、タイムスタンプを取得しません ( 4 )
  • 代わりに、UPDATE ステートメントの不確定な途中にあったタイムスタンプを取得します ( 3 )
  • 正しいタイムスタンプが得られない

行の任意の値を変更する任意のトリガーについても同じことが言えます。は、UPDATE の終了時点で値を出力しません。OUTPUT

これは、OUTPUT が正しい値を返すことを信頼できないことを意味します。

この痛ましい現実は、BOL に記載されています。

OUTPUT から返される列には、INSERT、UPDATE、または DELETE ステートメントが完了した後、トリガーが実行される前のデータがそのまま反映されます。

Entity Framework はどのように解決しましたか?

.NET Entity Framework は、オプティミスティック コンカレンシーに rowversion を使用します。timestampEF は、UPDATE を発行した後に存在するの値を知ることに依存します。

重要なデータには使用できないためOUTPUT、Microsoft の Entity Framework は私と同じ回避策を使用します。

回避策 #3 - 最終 - OUTPUT 句を使用しない

after値を取得するために、Entity Framework は以下を発行します。

UPDATE [dbo].[BatchReports]
SET [IsProcessed] = @0
WHERE (([BatchReportGUID] = @1) AND ([RowVersion] = @2))

SELECT [RowVersion], [LastModifiedDate]
FROM [dbo].[BatchReports]
WHERE @@ROWCOUNT > 0 AND [BatchReportGUID] = @1

使用しないでくださいOUTPUT

はい、競合状態に苦しんでいますが、それは SQL Server ができる最善のことです。

INSERT について

Entity Framework が行うことを行います。

SET NOCOUNT ON;

DECLARE @generated_keys table([CustomerID] int)

INSERT Customers (FirstName, LastName)
OUTPUT inserted.[CustomerID] INTO @generated_keys
VALUES ('Steve', 'Brown')

SELECT t.[CustomerID], t.[CustomerGuid], t.[RowVersion], t.[CreatedDate]
FROM @generated_keys AS g
   INNER JOIN Customers AS t
   ON g.[CustomerGUID] = t.[CustomerGUID]
WHERE @@ROWCOUNT > 0

ここでも、SELECTOUTPUT 句に信頼を置くのではなく、ステートメントを使用して行を読み取ります。

于 2012-11-02T16:04:06.790 に答える
54

この制限を回避するには、 OUTPUT INTO ...何かが必要です。たとえば、中間テーブル変数をターゲットとして宣言し、そこSELECTからターゲットにします。

DECLARE @T TABLE (
  BatchFileXml    XML,
  ResponseFileXml XML,
  ProcessedDate   DATE,
  RowVersion      BINARY(8) )

UPDATE BatchReports
SET    IsProcessed = 1
OUTPUT inserted.BatchFileXml,
       inserted.ResponseFileXml,
       deleted.ProcessedDate,
       inserted.Timestamp
INTO @T
WHERE  BatchReports.BatchReportGUID = @someGuid

SELECT *
FROM   @T 

他の回答で警告されているように、UPDATEステートメント自体によって変更された行にトリガーが書き戻して、実行中の列に影響を与えるOUTPUT場合、結果が役に立たない場合がありますが、これはトリガーのサブセットにすぎません。上記の手法は、監査目的でトリガーが他のテーブルに記録する場合や、元の行がトリガーで書き戻された場合でも挿入された ID 値を返す場合など、他の場合にうまく機能します。

于 2012-11-02T15:43:06.207 に答える