Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Impossible d'utiliser UPDATE avec la clause OUTPUT lorsqu'un déclencheur est sur la table

Avertissement de visibilité :Ne répondez pas à l'autre. Cela donnera des valeurs incorrectes. Lisez la suite pour savoir pourquoi c'est faux.

Étant donné le travail nécessaire pour faire UPDATE avec OUTPUT travail dans SQL Server 2008 R2, j'ai changé ma requête de :

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

à :

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

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

En gros, j'ai arrêté d'utiliser OUTPUT . Ce n'est pas si mal que Entity Framework lui-même utilise ce même hack !

Espérons 2012 2014 2016 2018 2019 2020 aura une meilleure mise en œuvre.

Mise à jour :l'utilisation de OUTPUT est nuisible

Le problème avec lequel nous avons commencé était d'essayer d'utiliser le OUTPUT clause pour récupérer le "après" valeurs dans un tableau :

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

Cela atteint alors la limitation bien connue ("won't-fix" bogue) dans SQL Server :

La table cible 'BatchReports' de l'instruction DML ne peut pas avoir de déclencheurs activés si l'instruction contient une clause OUTPUT sans clause INTO

Tentative de contournement #1

Nous essayons donc quelque chose où nous utiliserons une TABLE intermédiaire variable pour contenir le OUTPUT résultats :

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

Sauf que cela échoue car vous n'êtes pas autorisé à insérer un timestamp dans la table (même une variable de table temporaire).

Tentative de contournement #2

Nous savons secrètement qu'un timestamp est en fait un entier non signé de 64 bits (alias 8 octets). Nous pouvons modifier notre définition de table temporaire pour utiliser binary(8) plutôt que 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

Et ça marche, sauf que la valeur est fausse .

L'horodatage RowVersion nous renvoyons n'est pas la valeur de l'horodatage tel qu'il existait après la MISE À JOUR :

  • horodatage renvoyé :0x0000000001B71692
  • horodatage réel :0x0000000001B71693

C'est parce que les valeurs OUTPUT dans notre table ne sont pas les valeurs telles qu'elles étaient à la fin de l'instruction UPDATE :

  • Début de l'instruction UPDATE
    • modifie la ligne
      • l'horodatage est mis à jour (par exemple 2 → 3)
    • OUTPUT récupère le nouvel horodatage (c'est-à-dire 3)
    • le déclencheur s'exécute
      • modifie à nouveau la ligne
        • l'horodatage est mis à jour (par exemple 3 → 4)
  • Instruction UPDATE terminée
  • OUTPUT renvoie 3 (la mauvaise valeur)

Cela signifie :

  • Nous n'obtenons pas l'horodatage tel qu'il existe à la fin de l'instruction UPDATE (4 )
  • Au lieu de cela, nous obtenons l'horodatage tel qu'il se trouvait au milieu indéterminé de l'instruction UPDATE (3 )
  • Nous n'obtenons pas le bon horodatage

Il en va de même pour tout déclencheur qui modifie tout valeur dans la ligne. Le OUTPUT ne SORTIRA PAS la valeur à la fin de la MISE À JOUR.

Cela signifie que vous ne pouvez pas faire confiance à OUTPUT pour renvoyer des valeurs correctes.

Cette douloureuse réalité est documentée dans le BOL :

Les colonnes renvoyées par OUTPUT reflètent les données telles qu'elles sont après l'exécution de l'instruction INSERT, UPDATE ou DELETE, mais avant l'exécution des déclencheurs.

Comment Entity Framework a-t-il résolu le problème ?

Le .NET Entity Framework utilise rowversion pour Optimistic Concurrency. L'EF dépend de la connaissance de la valeur du timestamp tel qu'il existe après la publication d'une MISE À JOUR.

Puisque vous ne pouvez pas utiliser OUTPUT pour toutes les données importantes, Entity Framework de Microsoft utilise la même solution de contournement que moi :

Solution #3 - Finale - Ne pas utiliser la clause OUTPUT

Afin de récupérer l'après valeurs, problèmes d'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'utilisez pas OUTPUT .

Oui, il souffre d'une condition de concurrence, mais c'est le mieux que SQL Server puisse faire.

Qu'en est-il des INSERTS

Faites ce que fait 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

Encore une fois, ils utilisent un SELECT pour lire la ligne, plutôt que de faire confiance à la clause OUTPUT.