Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

UPDATE kann nicht mit der OUTPUT-Klausel verwendet werden, wenn sich ein Trigger in der Tabelle befindet

Sichtbarkeitswarnung :Nicht die andere Antwort. Es werden falsche Werte ausgegeben. Lesen Sie weiter, warum es falsch ist.

Angesichts des Klodges, der benötigt wird, um UPDATE zu erstellen mit OUTPUT Arbeit in SQL Server 2008 R2, ich habe meine Abfrage geändert von:

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

zu:

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

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

Grundsätzlich habe ich aufgehört, OUTPUT zu verwenden . Das ist nicht so schlimm wie Entity Framework selbst verwendet genau diesen Hack!

Hoffentlich 2012 2014 2016 2018 2019 2020 wird es eine bessere Umsetzung geben.

Aktualisierung:Die Verwendung von OUTPUT ist schädlich

Das Problem, mit dem wir begonnen haben, war der Versuch, OUTPUT zu verwenden -Klausel, um das "after" abzurufen Werte in einer Tabelle:

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

Das trifft dann auf die bekannte Einschränkung ("wird nicht behoben"). Fehler) in SQL Server:

Die Zieltabelle 'BatchReports' der DML-Anweisung kann keine aktivierten Trigger haben, wenn die Anweisung eine OUTPUT-Klausel ohne INTO-Klausel enthält

Umgehungsversuch Nr. 1

Also versuchen wir etwas, wo wir eine Zwischen-TABLE verwenden Variable, die OUTPUT enthält Ergebnisse:

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

Nur dass das fehlschlägt, weil Sie keinen timestamp einfügen dürfen in die Tabelle (sogar eine temporäre Tabellenvariable).

Umgehungsversuch Nr. 2

Wir wissen insgeheim, dass ein timestamp ist eigentlich eine vorzeichenlose 64-Bit-Ganzzahl (auch bekannt als 8 Byte). Wir können unsere temporäre Tabellendefinition ändern, um binary(8) zu verwenden statt 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

Und das funktioniert, außer dass der Wert falsch ist .

Der Zeitstempel RowVersion Wir geben nicht den Wert des Zeitstempels zurück, wie er nach Abschluss des UPDATE bestand:

  • zurückgegebener Zeitstempel :0x0000000001B71692
  • tatsächlicher Zeitstempel :0x0000000001B71693

Das liegt daran, dass die Werte OUTPUT in unsere Tabelle sind nicht die Werte, wie sie am Ende der UPDATE-Anweisung standen:

  • UPDATE-Anweisung ab
    • ändert Zeile
      • Zeitstempel wird aktualisiert (z. B. 2 → 3)
    • OUTPUT ruft den neuen Zeitstempel (z. B. 3) ab
    • Auslöser läuft
      • Ändert die Zeile erneut
        • Zeitstempel wird aktualisiert (z. B. 3 → 4)
  • UPDATE-Anweisung abgeschlossen
  • AUSGABE gibt 3 zurück (der falsche Wert)

Das bedeutet:

  • Wir erhalten den Zeitstempel nicht so, wie er am Ende der UPDATE-Anweisung steht (4 )
  • Stattdessen erhalten wir den Zeitstempel, wie er in der unbestimmten Mitte der UPDATE-Anweisung stand (3 )
  • Wir erhalten nicht den korrekten Zeitstempel

Dasselbe gilt für any Auslöser, der alle modifiziert Wert in der Zeile. Die OUTPUT wird den Wert am Ende des UPDATE nicht AUSGEBEN.

Das bedeutet, dass Sie sich nicht darauf verlassen können, dass OUTPUT jemals korrekte Werte zurückgibt.

Diese schmerzhafte Realität ist im BOL dokumentiert:

Von OUTPUT zurückgegebene Spalten geben die Daten wieder, wie sie sind, nachdem die INSERT-, UPDATE- oder DELETE-Anweisung abgeschlossen wurde, aber bevor Trigger ausgeführt werden.

Wie hat Entity Framework es gelöst?

Das .NET Entity Framework verwendet rowversion für optimistische Parallelität. Die EF hängt davon ab, den Wert des timestamp zu kennen wie es existiert, nachdem sie ein UPDATE herausgegeben haben.

Da Sie OUTPUT nicht verwenden können Für alle wichtigen Daten verwendet Microsofts Entity Framework die gleiche Problemumgehung wie ich:

Problemumgehung Nr. 3 - Endgültig - Verwenden Sie keine OUTPUT-Klausel

Um das after abzurufen Werte, Entity Framework-Probleme:

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

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

Verwenden Sie nicht OUTPUT .

Ja, es leidet unter einer Racebedingung, aber das ist das Beste, was SQL Server tun kann.

Was ist mit INSERTs

Tun Sie, was Entity Framework tut:

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

Auch hier verwenden sie ein SELECT -Anweisung, die Zeile zu lesen, anstatt der OUTPUT-Klausel zu vertrauen.