it-swarm.com.de

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

Ich führe eine UPDATE mit OUTPUT Abfrage durch:

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

Diese Aussage ist gut und schön; bis ein Trigger auf dem Tisch definiert ist. Dann erhält meine UPDATE -Anweisung den Fehler 334 :

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

Dieses Problem wird nun in einem Blogeintrag des SQL Server-Teams erläutert :

Die Fehlermeldung ist selbsterklärend

Und sie geben auch Lösungen:

Die Anwendung wurde geändert, um die INTO-Klausel zu verwenden

Es sei denn, ich kann den gesamten Blogbeitrag nicht auf den Kopf stellen.

Lassen Sie mich also meine Frage stellen: Was soll ich in UPDATE ändern, damit es funktioniert?

Siehe auch

54
Ian Boyd

Sichtbarkeitswarnung : Verwenden Sie nicht die am höchsten bewertete Antwort . Es werden falsche Werte angegeben. Lesen Sie weiter, wie es falsch ist.


Angesichts der Tatsache, dass UPDATE mit OUTPUT in SQL Server 2008 R2 funktionieren muss, habe ich 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. Dies ist nicht so schlimm, da Entity Framework selbst denselben Hack verwendet!

Hoffnungsvoll 201220142016 2018 wird eine bessere Umsetzung haben.


Update: Die Verwendung von OUTPUT ist schädlich

Das Problem, mit dem wir angefangen haben, bestand darin, die OUTPUT -Klausel zu verwenden, um die "after" -Werte in einer Tabelle abzurufen:

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

Das trifft dann die bekannte Einschränkung (wird den Fehler nicht beheben) in SQL Server:

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

Umgehungsversuch Nr. 1

Also probieren wir etwas aus, bei dem wir eine Zwischenvariable TABLE verwenden, um die Ergebnisse von OUTPUT zu speichern:

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

Dies schlägt jedoch fehl, da Sie kein timestamp in die Tabelle einfügen dürfen (auch keine temporäre Tabellenvariable).

Umgehungsversuch Nr. 2

Wir wissen insgeheim, dass ein timestamp eine 64-Bit-Ganzzahl (auch bekannt als 8-Byte-Ganzzahl) ohne Vorzeichen ist. Wir können unsere temporäre Tabellendefinition ändern, um binary(8) anstelle von timestamp zu verwenden:

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, nur dass der Wert falsch ist.

Der von uns zurückgegebene Zeitstempel RowVersion entspricht nicht dem Wert des Zeitstempels, wie er nach Abschluss des UPDATE vorhanden war:

  • zurückgegebener Zeitstempel : 0x0000000001B71692
  • aktueller Zeitstempel : 0x0000000001B71693

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

  • UPDATE-Anweisung ab
    • zeile ändern
    • zeitstempel wird aktualisiert
    • neuen Zeitstempel abrufen
    • auslöser läuft
      • zeile ändern
      • zeitstempel wird aktualisiert
  • UPDATE-Anweisung abgeschlossen

Das heisst:

  • Der am Ende der UPDATE-Anweisung vorhandene Zeitstempel wird nicht angezeigt
  • wir erhalten den Zeitstempel, wie er sich in der unbestimmten Mitte der UPDATE-Anweisung befand
  • wir erhalten nicht den richtigen Zeitstempel

Dasselbe gilt für jeden Trigger, der any Wert in der Zeile ändert. Der OUTPUT gibt den Wert am Ende des UPDATE nicht mehr aus.

Dies bedeutet, dass Sie OUTPUT nicht vertrauen, wenn es darum geht, korrekte Werte zurückzugeben

Diese schmerzhafte Realität ist in der BOL dokumentiert:

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

Wie hat Entity Framework das Problem gelöst?

Das .NET Entity Framework verwendet die Zeilenversion für Optimistic Concurrency. Die EF hängt davon ab, den Wert von timestamp zu kennen, nachdem sie ein UPDATE ausgegeben hat.

Da Sie OUTPUT nicht für wichtige Daten verwenden können, verwendet Microsoft Entity Framework dieselbe Problemumgehung wie ich:

Problemumgehung Nr. 3 - Final

Um die after Werte abzurufen, gibt Entity Framework Folgendes aus:

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 leisten kann.

Was ist mit INSERTs?

Machen Sie, was Entity Framework macht:

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

Um diese Einschränkung zu umgehen, müssen Sie OUTPUT INTO ... etwas. z.B. Deklarieren Sie eine Zwischentabellenvariable als Ziel, dann SELECT davon.

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 

Wie in der anderen Antwort bereits erwähnt, werden Sie möglicherweise nicht fündig, wenn Ihr Trigger in die Zeilen zurückschreibt, die von der UPDATE -Anweisung selbst so geändert wurden, dass sie sich auf die Spalten auswirken, die Sie OUTPUT- bearbeiten Die Ergebnisse sind nützlich, aber dies ist nur eine Teilmenge der Auslöser. Die obige Technik funktioniert auch in anderen Fällen, z. B. wenn die Aufzeichnung zu Prüfzwecken in anderen Tabellen ausgelöst wird oder eingefügte Identitätswerte zurückgegeben werden, selbst wenn die ursprüngliche Zeile in den Auslöser zurückgeschrieben wird.

44
Martin Smith

Warum alle benötigten Spalten in die Tabellenvariable einfügen? Wir brauchen nur einen Primärschlüssel und können alle Daten nach dem UPDATE lesen. Es gibt kein Rennen, wenn Sie die Transaktion verwenden:

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