Advertencia de visibilidad :No la otra respuesta. Dará valores incorrectos. Siga leyendo para saber por qué está mal.
Dada la torpeza necesaria para hacer UPDATE
con OUTPUT
trabajo en SQL Server 2008 R2, cambié mi consulta de:
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
Básicamente dejé de usar OUTPUT
. Esto no es tan malo como Entity Framework usa este mismo truco!
Ojalá 2012 2014 2016 2018 2019 2020 tendrá una mejor implementación.
Actualización:usar OUTPUT es dañino
El problema con el que comenzamos fue intentar usar OUTPUT
cláusula para recuperar el "después" valores en una tabla:
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid
Eso luego alcanza la limitación conocida ("no arreglará" error) en SQL Server:
La tabla de destino 'BatchReports' de la declaración DML no puede tener activadores habilitados si la declaración contiene una cláusula OUTPUT sin cláusula INTO
Intento de solución n.º 1
Así que intentamos algo donde usaremos una TABLE
intermedia variable para contener la OUTPUT
resultados:
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
Excepto que falla porque no se le permite insertar una timestamp
en la tabla (incluso una variable de tabla temporal).
Intento de solución n.º 2
Secretamente sabemos que una timestamp
es en realidad un entero sin signo de 64 bits (también conocido como 8 bytes). Podemos cambiar nuestra definición de tabla temporal para usar binary(8)
en lugar de 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
Y eso funciona, excepto que el valor es incorrecto .
La marca de tiempo RowVersion
lo que devolvemos no es el valor de la marca de tiempo tal como existía después de que se completó la ACTUALIZACIÓN:
- marca de tiempo devuelta :
0x0000000001B71692
- marca de tiempo real :
0x0000000001B71693
Eso es porque los valores OUTPUT
en nuestra mesa no los valores como estaban al final de la instrucción UPDATE:
- instrucción UPDATE que comienza
- modifica fila
- la marca de tiempo se actualiza (por ejemplo, 2 → 3)
- OUTPUT recupera la nueva marca de tiempo (es decir, 3)
- desencadenar ejecuciones
- modifica la fila de nuevo
- la marca de tiempo se actualiza (por ejemplo, 3 → 4)
- modifica la fila de nuevo
- modifica fila
- Instrucción UPDATE completa
- SALIDA devuelve 3 (el valor incorrecto)
Esto significa:
- No obtenemos la marca de tiempo tal como existe al final de la instrucción UPDATE (4 )
- En cambio, obtenemos la marca de tiempo tal como estaba en el medio indeterminado de la instrucción UPDATE (3 )
- No obtenemos la marca de tiempo correcta
Lo mismo ocurre con cualquier disparador que modifica cualquiera valor en la fila. La OUTPUT
no SALDRÁ el valor al final de la ACTUALIZACIÓN.
Esto significa que no puede confiar en OUTPUT para devolver valores correctos nunca.
Esta dolorosa realidad está documentada en el BOL:
Las columnas devueltas por OUTPUT reflejan los datos tal como están después de que se haya completado la declaración INSERT, UPDATE o DELETE, pero antes de que se ejecuten los disparadores.
¿Cómo lo resolvió Entity Framework?
.NET Entity Framework usa la versión de fila para la simultaneidad optimista. El EF depende de conocer el valor de la timestamp
tal como existe después de que emitan una ACTUALIZACIÓN.
Ya que no puedes usar OUTPUT
para cualquier dato importante, Entity Framework de Microsoft usa la misma solución que yo:
Solución alternativa n.º 3:final:no utilizar la cláusula OUTPUT
Para recuperar el después valores, problemas de 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
No use OUTPUT
.
Sí, sufre de una condición de carrera, pero eso es lo mejor que SQL Server puede hacer.
¿Qué pasa con los INSERTOS?
Haz lo que hace 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
Nuevamente, usan un SELECT
instrucción para leer la fila, en lugar de confiar en la cláusula OUTPUT.