sql >> Base de Datos >  >> RDS >> Sqlserver

No se puede usar ACTUALIZAR con la cláusula OUTPUT cuando hay un activador en la tabla

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