sql >> Databasteknik >  >> RDS >> Sqlserver

Kan inte använda UPDATE med OUTPUT-satsen när en trigger finns på bordet

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)
  • 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.



  1. PIVOT-fråga på distinkta poster

  2. Utmaningslösningar för nummerseriegenerator – del 4

  3. Hur man importerar XML-fil till MySQL-databastabell med XML_LOAD(); fungera

  4. Få rekordantal för alla tabeller i MySQL-databasen