Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Impossibile utilizzare UPDATE con la clausola OUTPUT quando un trigger è sul tavolo

Avviso di visibilità :Non rispondere all'altra. Darà valori errati. Continua a leggere perché è sbagliato.

Data la fatica necessaria per fare UPDATE con OUTPUT lavoro in SQL Server 2008 R2, ho cambiato la mia query da:

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

a:

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

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

Fondamentalmente ho smesso di usare OUTPUT . Questo non è così male come Entity Framework stesso usa lo stesso trucco!

Si spera 2012 2014 2016 2018 2019 Il 2020 avrà una migliore attuazione.

Aggiornamento:l'utilizzo di OUTPUT è dannoso

Il problema con cui abbiamo iniziato è stato provare a utilizzare OUTPUT clausola per recuperare il "dopo" valori in una tabella:

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

Ciò raggiunge quindi il ben noto limite ("non risolverà" bug) in SQL Server:

La tabella di destinazione 'BatchReports' dell'istruzione DML non può avere trigger abilitati se l'istruzione contiene una clausola OUTPUT senza clausola INTO

Tentativo di soluzione alternativa n. 1

Quindi proviamo qualcosa in cui useremo una TABLE intermedia variabile per contenere il OUTPUT risultati:

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

Tranne che non riesce perché non sei autorizzato a inserire un timestamp nella tabella (anche una variabile di tabella temporanea).

Tentativo di soluzione alternativa n. 2

Sappiamo segretamente che un timestamp è in realtà un intero senza segno a 64 bit (aka 8 byte). Possiamo modificare la nostra definizione di tabella temporanea per utilizzare binary(8) anziché 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

E funziona, tranne che il valore è sbagliato .

Il timestamp RowVersion restituiamo non è il valore del timestamp come esisteva dopo il completamento dell'AGGIORNAMENTO:

  • marca temporale restituita :0x0000000001B71692
  • marca temporale effettiva :0x0000000001B71693

Questo perché i valori OUTPUT nella nostra tabella sono non i valori come erano alla fine dell'istruzione UPDATE:

  • istruzione UPDATE che inizia
    • modifica riga
      • Il timestamp è aggiornato (ad es. 2 → 3)
    • OUTPUT recupera il nuovo timestamp (cioè 3)
    • il trigger viene eseguito
      • modifica di nuovo la riga
        • Il timestamp è aggiornato (es. 3 → 4)
  • Istruzione UPDATE completa
  • OUTPUT restituisce 3 (il valore sbagliato)

Ciò significa:

  • Non otteniamo il timestamp poiché esiste alla fine dell'istruzione UPDATE (4 )
  • Invece otteniamo il timestamp com'era nel mezzo indeterminato dell'istruzione UPDATE (3 )
  • Non otteniamo il timestamp corretto

Lo stesso vale per qualsiasi trigger che modifica qualsiasi valore nella riga. Il OUTPUT non produrrà il valore alla fine dell'AGGIORNAMENTO.

Ciò significa che non puoi fidarti che OUTPUT restituisca mai valori corretti.

Questa realtà dolorosa è documentata nel BOL:

Le colonne restituite da OUTPUT riflettono i dati così come sono dopo il completamento dell'istruzione INSERT, UPDATE o DELETE ma prima dell'esecuzione dei trigger.

Come ha risolto Entity Framework?

.NET Entity Framework usa rowversion per la concorrenza ottimistica. L'EF dipende dalla conoscenza del valore del timestamp come esiste dopo che hanno emesso un AGGIORNAMENTO.

Dal momento che non puoi usare OUTPUT per tutti i dati importanti, Entity Framework di Microsoft utilizza la stessa soluzione alternativa che faccio io:

Soluzione alternativa n. 3 - Finale - Non utilizzare la clausola OUTPUT

Per recuperare il dopo valori, problemi di 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

Non utilizzare OUTPUT .

Sì, soffre di una race condition, ma è quanto di meglio può fare SQL Server.

E gli INSERTI

Fai ciò che fa 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

Ancora una volta, usano un SELECT per leggere la riga, piuttosto che riporre fiducia nella clausola OUTPUT.