web-dev-qa-db-fra.com

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

J'effectue une requête UPDATE avec OUTPUT:

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

Cette déclaration est bel et bien; jusqu'à ce qu'un déclencheur soit défini sur la table. Alors mon UPDATE instruction obtiendra l'erreur 4 :

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

Maintenant, ce problème est expliqué dans n article de blog de l'équipe SQL Server :

Le message d'erreur est explicite

Et ils donnent également des solutions:

L'application a été modifiée pour utiliser la clause INTO

Sauf que je ne peux pas faire des têtes ou des queues de l'intégralité de l'article de blog.

Alors permettez-moi de poser ma question: à quoi dois-je changer mon UPDATE pour qu'il fonctionne?

Voir également

54
Ian Boyd

Avertissement de visibilité : N'utilisez pas la réponse la plus votée . Il donnera des valeurs incorrectes. Lisez la suite pour la façon dont c'est mal.


Étant donné le kludge nécessaire pour faire fonctionner UPDATE avec OUTPUT 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

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

J'espère 201220142016 2018 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 la clause OUTPUT pour récupérer les valeurs "après" dans une table:

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

Cela atteint alors la limitation bien connue (bogue ne résoudra pas) 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 n ° 1

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

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 n ° 2

Nous savons secrètement qu'un timestamp est en fait un entier non signé 64 bits (alias 8 octets). Nous pouvons changer 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 cela fonctionne, sauf que la valeur est fausse.

L'horodatage RowVersion que nous renvoyons n'est pas la valeur de l'horodatage telle qu'elle existait après la fin de la MISE À JOUR:

  • horodatage retourné : 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:

  • Instruction UPDATE commençant le
    • modifier la ligne
    • l'horodatage est mis à jour
    • récupérer un nouvel horodatage
    • le déclencheur s'exécute
      • modifier la ligne
      • l'horodatage est mis à jour
  • Instruction UPDATE terminée

Ça signifie:

  • Nous n'obtenons pas l'horodatage tel qu'il existe à la fin de l'instruction UPDATE
  • nous obtenons l'horodatage tel qu'il était au milieu indéterminé de l'instruction UPDATE
  • nous n'obtenons pas le bon horodatage

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

Cela signifie que vous ne faites pas 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 la fin de l'instruction INSERT, UPDATE ou DELETE mais avant l'exécution des déclencheurs.

Comment Entity Framework l'a-t-il résolu?

Le .NET Entity Framework utilise rowversion pour Optimistic Concurrency. L'EF dépend de la connaissance de la valeur de timestamp comme après l'émission d'une MISE À JOUR.

Comme vous ne pouvez pas utiliser OUTPUT pour des données importantes, Entity Framework de Microsoft utilise la même solution de contournement que je fais:

Solution de contournement n ° 3 - Final

Afin de récupérer les valeurs - après, Entity Framework émet:

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 critique, mais c'est ce que SQL Server peut faire de mieux.

Qu'en est-il des INSERT

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
34
Ian Boyd

Pour contourner cette restriction, vous devez OUTPUT INTO ... quelque chose. par exemple. déclarer une variable de table intermédiaire comme cible puis SELECT à partir de cela.

DECLARE @T TABLE (
  BatchFileXml    XML,
  ResponseFileXml XML,
  ProcessedDate   DATE,
  RowVersion      BINARY(8) )

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

SELECT *
FROM   @T 

Comme mis en garde dans l'autre réponse si votre déclencheur réécrit dans les lignes modifiées par l'instruction UPDATE elle-même de telle manière qu'elle affecte les colonnes que vous êtes OUTPUT- alors vous ne trouverez peut-être pas les résultats sont utiles mais ce n'est qu'un sous-ensemble de déclencheurs. La technique ci-dessus fonctionne bien dans d'autres cas, tels que les déclencheurs enregistrant sur d'autres tables à des fins d'audit, ou renvoyant des valeurs d'identité insérées même si la ligne d'origine est réécrite dans le déclencheur.

44
Martin Smith

Pourquoi mettre toutes les colonnes nécessaires dans une variable de table? Nous avons juste besoin d'une clé primaire et nous pouvons lire toutes les données après la MISE À JOUR. Il n'y a pas de course lorsque vous utilisez la transaction:

DECLARE @t TABLE (ID INT PRIMARY KEY);

BEGIN TRAN;

UPDATE BatchReports SET 
    IsProcessed = 1
OUTPUT inserted.ID INTO @t(ID)
WHERE BatchReports.BatchReportGUID = @someGuid;

SELECT b.* 
FROM @t t JOIN BatchReports b ON t.ID = b.ID;

COMMIT;
1
Marcin Hlibowski