web-dev-qa-db-fra.com

Pouvons-nous valider la transaction lorsque le déclencheur du serveur SQL échoue?

J'essaie d'éviter la perte de données lorsque le déclencheur d'insertion sur une table échoue. J'essaye ceci avec le scénario suivant et mon code échoue.

Quand une insertion se produit sur la table Clients, je veux insérer les mêmes données dans la table Archive. Lorsque le déclencheur échoue, je ne veux pas annuler la totalité de la transaction, ce qui entraînerait une perte de données sur la table Clients.

Même si le déclencheur échoue, je souhaite que les données soient insérées dans la table Customers, et la procédure stockée doit retourner Customer_ID comme d'habitude.

ALTER TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Customers]
   AFTER INSERT
AS 
BEGIN

BEGIN TRY
    begin transaction;
    set nocount  on;
    SAVE TRANSACTION InsertSaveHere;
    --Simulating error situation
    RAISERROR (N'This is message %s %d.', -- Message text.
       11, -- Severity,
       1, -- State,
       N'number', -- First argument.
       5); -- Second argument.

   Insert into Archive select * from Inserted;
    commit transaction;

  END TRY

  BEGIN CATCH
    ROLLBACK TRANSACTION InsertSaveHere;
  END CATCH
END

Ma question est principalement de savoir comment éviter l'insertion réelle sur la table des clients pour revenir en arrière si le déclencheur échoue. Comment changer mon code pour ça?

7
PushCode

La question mentionne que le "code échoue" mais il n'y a aucune indication de message d'erreur ou de ce qui échoue spécifiquement. L'inclusion d'au moins un, sinon les deux, de ces éléments d'information permet toujours d'obtenir de meilleures réponses.

Pour le moment, je vois quelque chose qui semble être une hypothèse incorrecte sur les déclencheurs et les transactions: vous incrémentez le @@TRANCOUNT en appelant BEGIN TRAN; mais seulement décrémenter @@TRANCOUNT lorsqu'il n'y a pas d'erreur et que le COMMIT TRAN; la ligne s'exécute dans le bloc TRY. En cas d'erreur, le COMMIT est ignoré et un ROLLBACK du point de sauvegarde se produit. Mais l'annulation d'un point de sauvegarde ne diminue pas @@TRANCOUNT auquel cas l'opération INSERT se termine et la transaction est toujours active.

Des déclencheurs existent dans une transaction démarrée en interne qui la lie à l'opération DML qui a déclenché le déclencheur. Voici comment vous pouvez appeler ROLLBACK dans un déclencheur pour annuler cette opération DML.

Option 1A (empêcher les erreurs d'annuler la transaction - PRÉFÉRÉ s'il y a plusieurs instructions DML dans le déclencheur)

Dans cet esprit, vous devriez pouvoir supprimer le BEGIN TRAN; et COMMIT TRAN; lignes pour que cela fonctionne. L'effet net sera que s'il n'y a pas d'erreur, le INSERT dans la table Archive sera validé comme prévu, mais s'il y a une erreur, il fera le ROLLBACK au point de sauvegarde et continuez.

CEPENDANT, après avoir retiré ces deux pièces, vous vous retrouvez toujours avec la situation délicate d'obtenir l'erreur suivante:

Msg 3931, niveau 16, état 1, procédure Customer_Insert_Trigger_Test, ligne 78
La transaction en cours ne peut pas être validée et ne peut pas être annulée vers un point de sauvegarde. Annulez la transaction entière.

La raison de ce comportement semble être un paramètre implicite de XACT_ABORT ON par le système lorsqu'il appelle le déclencheur. L'effet de XACT_ABORT ON consiste à annuler la transaction si une erreur se produit. Le remède? Réglez simplement XACT_ABORT OFF au début du déclencheur.

Par exemple, ce qui suit fonctionne pour moi:

CREATE
--ALTER
TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Inserts]
   AFTER INSERT
AS 
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;

BEGIN TRY
    PRINT '@@TRANCOUNT = ' + CONVERT(VARCHAR(10), @@TRANCOUNT); -- for debug only
    SAVE TRANSACTION InsertSaveHere;

    --Simulating error situation
        RAISERROR (N'This is message %s %d.', -- Message text.
          11, -- Severity,
          1, -- State,
          N'number', -- First argument.
          5); -- Second argument.

   --Insert into Archive select * from Inserted;
END TRY
BEGIN CATCH
    PRINT 'Entering CATCH block...'; -- for debug only
    ROLLBACK TRANSACTION InsertSaveHere;
END CATCH;

END;

Veuillez noter que cette méthode ne pas modifie le comportement attendu des déclencheurs sur ce tableau en ce qui concerne: 1) le COMMIT se produisant au niveau de la couche la plus externe (soit l'instruction DML initiale, soit au-delà si une transaction explicite a été lancée avant cette instruction), 2) la capacité d'autres déclencheurs potentiels de cette table d'émettre un ROLLBACK pour annuler l'opération , et 3) la capacité d'une transaction explicite, commencée avant l'instruction DML sur cette table, d'émettre un ROLLBACK pour annuler toutes les modifications, y compris l'opération DML sur cette table.

Option 1B (empêcher les erreurs d'annuler la transaction - PREFERRED s'il y a une seule instruction DML dans le déclencheur)

Bien sûr, si la seule chose qui pourrait faire une erreur ici est le INSERT dans la table Archive, alors vous pourriez probablement aussi vous débarrasser du SAVE TRAN et ROLLBACK TRANSACTION InsertSaveHere; et faites simplement quelque chose dans le bloc CATCH pour qu'il ne soit pas vide, quelque chose comme DECLARE @Test INT; pourrait fonctionner. Le raisonnement ici est qu'une seule déclaration DML que les erreurs ne se sont jamais vraiment produites, il n'y a donc rien à annuler ;-).


Option 2 (NON PRÉFÉRÉ)

Pour répondre à la question comme indiqué dans le titre: vous devriez pouvoir COMMIT dans le déclencheur, mais je serais eXtreeemely attention à faire une telle chose car elle modifie le comportement attendu du moment où la transaction sera validée ou annulée, ce qui pourrait empêcher le bon fonctionnement des autres déclencheurs de cette table (ils ne pourront pas émettre un ROLLBACK pour annuler l'opération si ce déclencheur s'exécute en premier) et empêcherait le fonctionnement prévu d'une transaction explicite démarrée avant l'opération DML sur cette table.

Pour ce faire (REMARQUE: vous devez avoir lu le paragraphe directement ci-dessus avant de continuer à lire ce paragraphe ), vous devez émettre un COMMIT TRAN; (puisque le déclencheur existe déjà dans une transaction), puis exécutez un BEGIN TRAN;. Le COMMIT TRAN; va valider l'opération DML initiale et BEGIN TRAN; mettra le @@TRANCOUNT retour à 1 afin que lorsque l'exécution des déclencheurs se termine, vous n'obtenez pas l'erreur indiquant que le déclencheur s'est terminé avec un autre @@TRANCOUNT qu'il n'en avait au début.

7
Solomon Rutzky

Un déclencheur fonctionne déjà toujours dans une transaction implicite, voir la question connexe:

Existe-t-il un moyen de garantir qu'un déclencheur SQL Server sera exécuté?

Vous pouvez intercepter une erreur et empêcher un abandon/annulation, mais vous ne pouvez pas "valider" la transaction à partir du déclencheur.