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)
- ponownie modyfikuje wiersz
- modyfikuje wiersz
- 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.