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)
- modifica di nuovo la riga
- modifica riga
- 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.