Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Nie można użyć UPDATE z klauzulą ​​OUTPUT, gdy na stole znajduje się wyzwalacz

Ostrzeżenie dotyczące widoczności :Nie rób drugiej odpowiedzi. Poda nieprawidłowe wartości. Czytaj dalej, dlaczego to jest złe.

Biorąc pod uwagę ilość potrzebną do wykonania UPDATE z OUTPUT pracuję w SQL Server 2008 R2, zmieniłem zapytanie z:

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

do:

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

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

Zasadniczo przestałem używać OUTPUT . Nie jest tak źle, jak sam Entity Framework używa tego samego hacka!

Mam nadzieję, że 2012 2014 2016 2018 2019 2020 będzie miał lepszą realizację.

Aktualizacja:używanie OUTPUT jest szkodliwe

Problem, z którym zaczęliśmy, polegał na próbie użycia OUTPUT klauzula do pobrania "po" wartości w tabeli:

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

To wtedy trafia w dobrze znane ograniczenie („nie naprawi” błąd) w SQL Server:

Tabela docelowa „BatchReports” instrukcji DML nie może mieć żadnych włączonych wyzwalaczy, jeśli instrukcja zawiera klauzulę OUTPUT bez klauzuli INTO

Próba obejścia nr 1

Więc spróbujemy czegoś, w którym użyjemy pośredniej TABLE zmienna do przechowywania OUTPUT wyniki:

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

Tyle że to się nie udaje, ponieważ nie możesz wstawić timestamp do tabeli (nawet tymczasowej zmiennej tabeli).

Próba obejścia #2

Potajemnie wiemy, że timestamp jest w rzeczywistości 64-bitową (czyli 8 bajtową) liczbą całkowitą bez znaku. Możemy zmienić definicję tabeli tymczasowej, aby używała binary(8) zamiast 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

I to działa, poza tym, że wartość jest nieprawidłowa .

Znacznik czasu RowVersion zwracamy nie jest wartością znacznika czasu, która istniała po zakończeniu UPDATE:

  • znacznik czasu zwrotu :0x0000000001B71692
  • rzeczywisty znacznik czasu :0x0000000001B71693

Dzieje się tak, ponieważ wartości OUTPUT w naszym stole nie wartości takie, jakie były na końcu instrukcji UPDATE:

  • Rozpoczęcie instrukcji UPDATE
    • modyfikuje wiersz
      • sygnatura czasowa została zaktualizowana (np. 2 → 3)
    • OUTPUT pobiera nowy znacznik czasu (tj. 3)
    • uruchamianie spustu
      • ponownie modyfikuje wiersz
        • sygnatura czasowa została zaktualizowana (np. 3 → 4)
  • Zakończono instrukcję UPDATE
  • WYJŚCIE zwraca 3 (nieprawidłowa wartość)

Oznacza to:

  • Nie otrzymujemy znacznika czasu, który istnieje na końcu instrukcji UPDATE (4 )
  • Zamiast tego otrzymujemy znacznik czasu, taki jak w nieokreślonym środku instrukcji UPDATE (3 )
  • Nie otrzymujemy poprawnej sygnatury czasowej

To samo dotyczy dowolnych wyzwalacz, który modyfikuje dowolne wartość w wierszu. OUTPUT nie wyprowadzi wartości na koniec AKTUALIZACJI.

Oznacza to, że nie możesz ufać, że OUTPUT kiedykolwiek zwróci jakiekolwiek poprawne wartości.

Ta bolesna rzeczywistość jest udokumentowana w BOL:

Kolumny zwrócone z OUTPUT odzwierciedlają dane po wykonaniu instrukcji INSERT, UPDATE lub DELETE, ale przed wykonaniem wyzwalaczy.

Jak rozwiązał to Entity Framework?

.NET Entity Framework używa rowversion dla optymistycznej współbieżności. EF zależy od znajomości wartości timestamp tak jak istnieje po wydaniu AKTUALIZACJI.

Ponieważ nie możesz użyć OUTPUT w przypadku jakichkolwiek ważnych danych Microsoft Entity Framework wykorzystuje to samo obejście, co ja:

Obejście nr 3 — wersja ostateczna — nie używaj klauzuli OUTPUT

Aby pobrać po wartości, problemy z 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

Nie używaj OUTPUT .

Tak, cierpi z powodu wyścigu, ale to najlepsze, co może zrobić SQL Server.

A co z WSTAWKAMI

Zrób to, co robi 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

Ponownie używają SELECT oświadczenie, aby przeczytać wiersz, zamiast pokładać zaufanie w klauzuli OUTPUT.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak znaleźć nazwę ograniczenia w SQL Server

  2. Jak łatwo i szybko sparametryzować ciąg o wartości null za pomocą DBNull.Value?

  3. Zaimplementuj funkcję stronicowania (pomiń / weź) za pomocą tego zapytania

  4. Jaki jest najlepszy sposób na zaimplementowanie asocjacji polimorficznej w SQL Server?

  5. Symulacja CONNECT BY PRIOR Oracle w SQL Server