Synlighetsvarning :Svara inte den andre. Det kommer att ge felaktiga värden. Läs vidare för varför det är fel.
Med tanke på den kludge som behövs för att göra UPDATE
med OUTPUT
arbetade i SQL Server 2008 R2, ändrade jag min fråga från:
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid
till:
SELECT BatchFileXml, ResponseFileXml, ProcessedDate FROM BatchReports
WHERE BatchReports.BatchReportGUID = @someGuid
UPDATE BatchReports
SET IsProcessed = 1
WHERE BatchReports.BatchReportGUID = @someGuid
I princip slutade jag använda OUTPUT
. Det här är inte så illa som Entity Framework i sig använder samma hack!
Förhoppningsvis 2012 2014 2016 2018 2019 2020 kommer att ha ett bättre genomförande.
Uppdatering:att använda OUTPUT är skadligt
Problemet vi började med var att försöka använda OUTPUT
klausul för att hämta "efter" värden i en tabell:
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid
Det träffar sedan den välkända begränsningen ("kommer inte att fixa" bug) i SQL Server:
Måltabellen 'BatchReports' för DML-satsen kan inte ha några aktiverade triggers om satsen innehåller en OUTPUT-sats utan INTO-sats
Lösningsförsök #1
Så vi försöker något där vi kommer att använda en mellanliggande TABLE
variabel för att hålla OUTPUT
resultat:
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
Förutom att det misslyckas eftersom du inte får infoga en timestamp
in i tabellen (även en temporär tabellvariabel).
Lösningsförsök #2
Vi vet i hemlighet att en timestamp
är faktiskt ett 64-bitars (aka 8 byte) heltal utan tecken. Vi kan ändra vår temporära tabelldefinition till att använda binary(8)
istället för 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
Och det fungerar, förutom att värdet är fel .
Tidsstämpeln RowVersion
we return är inte värdet på tidsstämpeln som den fanns efter att UPPDATERING slutfördes:
- returnerad tidsstämpel :
0x0000000001B71692
- faktisk tidsstämpel :
0x0000000001B71693
Det beror på att värdena OUTPUT
i vår tabell är inte värdena som de var i slutet av UPDATE-satsen:
- UPDATE-sats börjar
- ändrar rad
- tidsstämpeln uppdateras (t.ex. 2 → 3)
- OUTPUT hämtar ny tidsstämpel (dvs. 3)
- utlösaren körs
- ändrar raden igen
- tidsstämpeln uppdateras (t.ex. 3 → 4)
- ändrar raden igen
- ändrar rad
- Uppdatera uttalande slutfört
- OUTPUT returnerar 3 (fel värde)
Det betyder:
- Vi får inte tidsstämpeln eftersom den finns i slutet av UPDATE-satsen (4 )
- Istället får vi tidsstämpeln som den var i den obestämda mitten av UPDATE-satsen (3 )
- Vi får inte rätt tidsstämpel
Detsamma gäller alla trigger som ändrar alla värde i raden. OUTPUT
kommer inte UT UT värdet vid slutet av UPPDATERING.
Detta betyder att du aldrig kan lita på att OUTPUT returnerar några korrekta värden.
Denna smärtsamma verklighet finns dokumenterad i BOL:
Kolumner som returneras från OUTPUT återspeglar data som de är efter att INSERT-, UPDATE- eller DELETE-satsen har slutförts men innan triggers exekveras.
Hur löste Entity Framework det?
.NET Entity Framework använder rowversion för Optimistisk samtidighet. EF är beroende av att känna till värdet på timestamp
som det finns efter att de har utfärdat en UPPDATERING.
Eftersom du inte kan använda OUTPUT
för all viktig data använder Microsofts Entity Framework samma lösning som jag gör:
Lösning #3 - Slutlig - Använd inte OUTPUT-satsen
För att hämta efter värden, Entity Framework-frågor:
UPDATE [dbo].[BatchReports]
SET [IsProcessed] = @0
WHERE (([BatchReportGUID] = @1) AND ([RowVersion] = @2))
SELECT [RowVersion], [LastModifiedDate]
FROM [dbo].[BatchReports]
WHERE @@ROWCOUNT > 0 AND [BatchReportGUID] = @1
Använd inte OUTPUT
.
Ja, den lider av ett rastillstånd, men det är det bästa SQL Server kan göra.
Vad sägs om INSERT
Gör vad Entity Framework gör:
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
Återigen använder de en SELECT
uttalande för att läsa raden, snarare än att lita på OUTPUT-satsen.