Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Não é possível usar UPDATE com cláusula OUTPUT quando um gatilho está na tabela


Aviso de visibilidade :Não a outra resposta. Vai dar valores incorretos. Leia por que está errado.

Dado o kludge necessário para fazer UPDATE com OUTPUT trabalho no SQL Server 2008 R2, alterei minha consulta de:
UPDATE BatchReports  
SET IsProcessed = 1
OUTPUT inserted.BatchFileXml, inserted.ResponseFileXml, deleted.ProcessedDate
WHERE BatchReports.BatchReportGUID = @someGuid

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

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

Basicamente eu parei de usar OUTPUT . Isso não é tão ruim quanto o Entity Framework em si usa esse mesmo hack!

Espero que 2012 2014 2016 2018 2019 2020 terá uma implementação melhor.

Atualização:usar OUTPUT é prejudicial


O problema com o qual começamos foi tentar usar o OUTPUT cláusula para recuperar o "depois" valores em uma tabela:
UPDATE BatchReports
SET IsProcessed = 1
OUTPUT inserted.LastModifiedDate, inserted.RowVersion, inserted.BatchReportID
WHERE BatchReports.BatchReportGUID = @someGuid

Isso atinge a limitação bem conhecida ("não vai corrigir" bug) no SQL Server:

A tabela de destino 'BatchReports' da instrução DML não pode ter nenhum acionador habilitado se a instrução contiver uma cláusula OUTPUT sem cláusula INTO

Tentativa de solução nº 1


Então, tentamos algo em que usaremos uma TABLE intermediária variável para manter o 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

Exceto que falha porque você não tem permissão para inserir um timestamp na tabela (mesmo uma variável de tabela temporária).

Tentativa de solução nº 2


Sabemos secretamente que um timestamp é na verdade um inteiro sem sinal de 64 bits (também conhecido como 8 bytes). Podemos alterar nossa definição de tabela temporária para usar binary(8) em vez 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

E isso funciona, exceto que o valor está errado .

O carimbo de data/hora RowVersion que retornamos não é o valor do timestamp como existia após a conclusão do UPDATE:
  • carimbo de data e hora retornado :0x0000000001B71692
  • carimbo de data e hora real :0x0000000001B71693

Isso porque os valores OUTPUT em nossa tabela não os valores como estavam no final da instrução UPDATE:
  • Instrução UPDATE iniciando
    • modifica a linha
      • o carimbo de data/hora foi atualizado (por exemplo, 2 → 3)
    • OUTPUT recupera o novo carimbo de data/hora (ou seja, 3)
    • execuções de gatilho
      • modifica a linha novamente
        • o carimbo de data/hora foi atualizado (por exemplo, 3 → 4)
  • Instrução UPDATE concluída
  • SAÍDA retorna 3 (o valor errado)

Isso significa:
  • Não recebemos o carimbo de data/hora, pois existe no final da instrução UPDATE (4 )
  • Em vez disso, obtemos o carimbo de data/hora como estava no meio indeterminado da instrução UPDATE (3 )
  • Não recebemos o carimbo de data/hora correto

O mesmo vale para qualquer gatilho que modifica qualquer valor na linha. A OUTPUT não irá OUTPUT o valor no final do UPDATE.

Isso significa que você não pode confiar em OUTPUT para retornar valores corretos.

Esta dolorosa realidade está documentada no BOL:

As colunas retornadas de OUTPUT refletem os dados como estão após a conclusão da instrução INSERT, UPDATE ou DELETE, mas antes que os gatilhos sejam executados.

Como o Entity Framework resolveu isso?


O .NET Entity Framework usa rowversion para simultaneidade otimista. O EF depende de saber o valor do timestamp como existe depois que eles emitem um UPDATE.

Como você não pode usar OUTPUT para quaisquer dados importantes, o Entity Framework da Microsoft usa a mesma solução alternativa que eu:

Solução nº 3 - Final - Não use a cláusula OUTPUT


Para recuperar o depois valores, problemas do 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

Não use OUTPUT .

Sim, sofre de uma condição de corrida, mas isso é o melhor que o SQL Server pode fazer.

E os INSERTOS


Faça o que o Entity Framework faz:
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

Novamente, eles usam um SELECT instrução para ler a linha, em vez de colocar qualquer confiança na cláusula OUTPUT.