web-dev-qa-db-fra.com

Transmettre des informations sur qui a supprimé l'enregistrement sur un déclencheur de suppression

Lors de la configuration d'une piste d'audit, je n'ai aucun problème à suivre qui met à jour ou insérer des enregistrements dans une table, mais le suivi qui supprime les enregistrements semble plus problématique.

Je peux suivre les insertions/mises à jour en incluant dans l'insertion/mise à jour le champ "UpdatedBy". Cela permet au déclencheur INSERT/UPDATE d'avoir accès au champ "UpdatedBy" via inserted.UpdatedBy. Cependant, avec le déclencheur Supprimer, aucune donnée n'est insérée/mise à jour. Existe-t-il un moyen de transmettre des informations au déclencheur Supprimer afin qu'il sache qui a supprimé l'enregistrement?

Voici un déclencheur d'insertion/mise à jour

ALTER TRIGGER [dbo].[trg_MyTable_InsertUpdate] 
ON [dbo].[MyTable]
FOR INSERT, UPDATE
AS  

INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
VALUES (inserted.ID, inserted.LastUpdatedBy)
FROM inserted 

Utilisation de SQL Server 2012

11
webworm

Existe-t-il un moyen de transmettre des informations au déclencheur de suppression afin qu'il sache qui a supprimé l'enregistrement?

Oui: en utilisant un très cool (et fonctionnalité sous-utilisée) appelé CONTEXT_INFO. Il s'agit essentiellement de mémoire de session qui existe dans toutes les étendues et n'est pas liée par des transactions. Il peut être utilisé pour transmettre des informations (toutes les informations - enfin, toutes celles qui tiennent dans l'espace limité) aux déclencheurs ainsi que des allers-retours entre les appels sous-proc/EXEC. Et je l'ai déjà utilisé pour cette même situation.

Testez avec ce qui suit pour voir comment cela fonctionne. Notez que je convertis en CHAR(128) avant la CONVERT(VARBINARY(128), ... Il s'agit de forcer le remplissage pour faciliter la reconversion en VARCHAR lors de sa sortie de CONTEXT_INFO() puisque VARBINARY(128) est rempli à droite avec 0x00 s.

SELECT CONTEXT_INFO();
-- Initially = NULL

DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
                            CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
                          );
SET CONTEXT_INFO @EncodedUser;

SELECT CONTEXT_INFO() AS [RawContextInfo],
       RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];

Résultats:

0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!

TOUT ENSEMBLE:

  1. L'application doit appeler une procédure stockée "Supprimer" qui transmet le nom d'utilisateur (ou autre) qui supprime l'enregistrement. Je suppose que c'est déjà le modèle utilisé car il semble que vous suivez déjà les opérations d'insertion et de mise à jour.

  2. La procédure stockée "Supprimer" permet:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
    
  3. Le déclencheur d'audit:

    -- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, COALESCE(
                         LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
                         '<unknown>')
       FROM DELETED del;
    
  4. Veuillez noter que, comme @SeanGallardy l'a souligné dans un commentaire, en raison d'autres procédures et/ou requêtes ad hoc supprimant des enregistrements de ce tableau, il est possible que:

    • CONTEXT_INFO N'a pas été défini et est toujours NULL:

      Pour cette raison, j'ai mis à jour le INSERT INTO AuditTable Ci-dessus pour utiliser un COALESCE par défaut la valeur. Ou, si vous ne voulez pas de valeur par défaut et que vous avez besoin d'un nom, vous pouvez faire quelque chose de similaire à:

      DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
      SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
      
      IF (@UserName IS NULL)
      BEGIN
         ROLLBACK TRAN; -- cancel the DELETE operation
         RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
      END;
      
      -- use @UserName in the INSERT...SELECT
      
    • CONTEXT_INFO A été défini sur une valeur qui est pas un nom d'utilisateur valide, et peut donc dépasser la taille du champ AuditTable.[UserWhoMadeChanges]:

      Pour cette raison, j'ai ajouté une fonction LEFT pour m'assurer que tout ce qui est récupéré de CONTEXT_INFO Ne cassera pas le INSERT. Comme indiqué dans le code, il vous suffit de définir le 50 À la taille réelle du champ UserWhoMadeChanges.


MISE À JOUR POUR SQL SERVER 2016 ET PLUS RÉCENT

SQL Server 2016 a ajouté une version améliorée de cette mémoire par session: Contexte de session. Le nouveau contexte de session est essentiellement une table de hachage de paires clé-valeur, la "clé" étant de type sysname (c'est-à-dire NVARCHAR(128)) et la "valeur" étant SQL_VARIANT . Sens:

  1. Il y a maintenant une séparation des valeurs donc moins susceptibles d'entrer en conflit avec d'autres utilisations
  2. Vous pouvez stocker différents types, sans avoir à vous soucier du comportement étrange lors de la récupération de la valeur via CONTEXT_INFO() (pour plus de détails, veuillez consulter mon article: Pourquoi CONTEXT_INFO () Retourne pas la valeur exacte définie par SET CONTEXT_INFO? )
  3. Vous obtenez beaucoup plus d'espace: 8000 octets max par "Value", jusqu'à 256 Ko au total sur toutes les clés (par rapport aux 128 octets max de CONTEXT_INFO)

Pour plus de détails, veuillez consulter les pages de documentation suivantes:

10
Solomon Rutzky

Vous ne pouvez pas le faire de cette façon, sauf si vous cherchez à enregistrer l'ID utilisateur du serveur SQL plutôt qu'un niveau d'application.

Vous pouvez effectuer une suppression logicielle en ayant une colonne appelée DeletedBy et en la configurant au besoin, puis votre déclencheur de mise à jour peut effectuer la véritable suppression (ou archiver l'enregistrement, j'évite généralement les suppressions matérielles lorsque cela est possible et légal) ainsi que la mise à jour de votre piste d'audit . Pour forcer les suppressions à effectuer de cette façon, définissez un on delete déclencheur qui déclenche une erreur. Si vous ne souhaitez pas ajouter de colonne à votre table physique, vous pouvez définir une vue qui ajoute la colonne et définir instead of déclencheurs pour gérer la mise à jour de la table de base, mais cela peut être exagéré.

5
David Spillett

Existe-t-il un moyen de transmettre des informations au déclencheur de suppression afin qu'il sache qui a supprimé l'enregistrement?

Oui, apparemment il y a deux façons ;-). En cas de réserves sur l'utilisation de CONTEXT_INFO comme je l'ai suggéré dans mon autre réponse ici , je viens de penser à une autre façon qui a une séparation fonctionnelle plus nette des autres codes/processus: utiliser une table temporaire locale.

Le nom de la table temporaire doit inclure le nom de la table en cours de suppression car cela aidera à le séparer de tout autre code susceptible de s'exécuter dans la même session. Quelque chose dans le sens de:
#<TableName>DeleteAudit

Un avantage pour une table temporaire locale sur CONTEXT_INFO est que si quelqu'un dans un autre proc - qui est en quelque sorte appelé à partir de ce proc "Delete" particulier - arrive juste d'utiliser incorrectement le même nom de table temporaire, le sous-processus a) créera une nouvelle table temporaire locale du nom demandé qui sera distincte de cette table temporaire initiale (même si elle porte le même nom), et b) toute instruction DML par rapport à la nouvelle table temporaire locale dans le sous-processus n'affectera aucune donnée de la table temporaire locale créée ici dans le processus parent, donc pas d'écrasement des données. Bien sûr, si un sous-processus émet une instruction DML par rapport à ce nom de table temporaire sans d'abord émettre une CREATE TABLE de ce même nom, alors ces instructions DML affecteront les données de ce tableau. MAIS, à ce stade, nous obtenons vraiment Edge-casey ici, encore plus qu'avec la probabilité de chevauchement des utilisations de CONTEXT_INFO (oui, je sais que c'est arrivé, c'est pourquoi je dis "Edge-case" plutôt que "ça n'arrivera jamais").

  1. L'application doit appeler une procédure stockée "Supprimer" qui transmet le nom d'utilisateur (ou autre) qui supprime l'enregistrement. Je suppose que c'est déjà le modèle utilisé car il semble que vous suivez déjà les opérations d'insertion et de mise à jour.

  2. La procédure stockée "Supprimer" permet:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
    
  3. Le déclencheur d'audit:

    -- Set the datatype and length to be the same as the [UserWhoMadeChanges] field
    DECLARE @UserName VARCHAR(50);
    IF (OBJECT_ID(N'tempdb..#TriggerTestDeleteAudit') IS NOT NULL)
    BEGIN
       SELECT @UserName = UserName
       FROM #TriggerTestDeleteAudit;
    END;
    
    -- catch the following conditions: missing table, no rows in table, or empty row
    IF (@UserName IS NULL OR @UserName NOT LIKE '%[a-z]%')
    BEGIN
      /* -- uncomment if undefined UserName == badness
       ROLLBACK TRAN; -- cancel the DELETE operation
       RAISERROR('Please set UserName via #TriggerTestDeleteAudit and try again.', 16 ,1);
       RETURN; -- exit
      */
      /* -- uncomment if undefined UserName gets default value
       SET @UserName = '<unknown>';
      */
    END;
    
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, @UserName
       FROM DELETED del;
    

    J'ai testé ce code dans un déclencheur et il fonctionne comme prévu.

2
Solomon Rutzky