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?
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.
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
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).
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:
0x0000000001B71692
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:
Ça signifie:
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.
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:
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.
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
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.
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;