web-dev-qa-db-ja.com

トリガーがテーブルにある場合、OUTPUT句でUPDATEを使用できません

私はUPDATEクエリでOUTPUTを実行しています:

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

この声明は申し分ありません。トリガーがテーブルで定義されるまで。その後、私のUPDATEステートメントはエラーを取得します 4

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

現在、この問題は SQL Serverチームによるブログ投稿 で説明されています。

エラーメッセージは一目瞭然です

また、ソリューションも提供します。

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

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

それで、私の質問を聞かせてください:UPDATEを何に変更すればうまくいきますか?

こちらもご覧ください

54
Ian Boyd

可視性警告最高得票数 を使用しないでください。間違った値が表示されます。それが間違っている方法を読んでください。


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自体がこのまったく同じハックを使用しているので、これはそれほど悪くありません!

うまくいけば 201220142016年 2018年の実装は改善されます。


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

私たちが始めた問題は、OUTPUT句を使用して、テーブル内の "after"値を取得しようとしていました。

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

その後、SQL Serverのよく知られている制限(バグは修正されません)にぶつかります。

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

回避策の試み#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をテーブルに挿入することが許可されていないために失敗します(一時テーブル変数でも)。

回避策の試み#2

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

_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ステートメントの終了時の値notであるためです。

  • UPDATEステートメントの開始
    • 行を変更
    • タイムスタンプが更新されます
    • 新しいタイムスタンプを取得する
    • トリガーの実行
      • 行を変更
      • タイムスタンプが更新されます
  • UPDATEステートメントの完了

これの意味は:

  • UPDATEステートメントの最後に存在するタイムスタンプは取得しません
  • uPDATEステートメントの不確定な中間にあったタイムスタンプを取得します
  • 正しいタイムスタンプが取得されません

同じことは、any行内のany値を変更するトリガーにも当てはまります。 OUTPUTは、UPDATEの終了時点で値を出力しません。

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

この痛みを伴う現実はBOLに文書化されています。

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

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

.NET Entity Frameworkは、オプティミスティック同時実行性にrowversionを使用します。 EFは、UPDATEを発行した後、timestampの値を知ることに依存しています。

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

回避策#3-最終

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
_
34
Ian Boyd

この制限を回避するには、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- ingしている列に影響するように書き戻す場合、結果は有用ですが、これはトリガーのサブセットにすぎません。上記の手法は、監査目的で他のテーブルに記録するトリガーや、元の行がトリガーで書き戻された場合でも挿入されたID値を返すなど、他の場合に正常に機能します。

44
Martin Smith

なぜ必要なすべての列をテーブル変数に入れるのですか?主キーが必要なだけで、UPDATE後にすべてのデータを読み取ることができます。トランザクションを使用する場合、競合はありません。

DECLARE @t TABLE (ID INT PRIMARY KEY);

BEGIN TRAN;

UPDATE BatchReports SET 
    IsProcessed = 1
OUTPUT inserted.ID INTO @t(ID)
WHERE BatchReports.BatchReportGUID = @someGuid;

SELECT b.* 
FROM @t t JOIN BatchReports b ON t.ID = b.ID;

COMMIT;
1