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


11

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, cependant, suivre 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 de suppression 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


1
Voir cette réponse. SUSER_SNAME()est la clé pour savoir qui a supprimé l'enregistrement.
Kin Shah

1
Merci Kin, mais je ne pense pas que SUSER_SNAME()cela fonctionnerait dans une situation comme une application Web où un seul utilisateur pourrait être utilisé pour la communication de base de données pour l'ensemble de l'application.
webworm

1
Vous n'avez pas mentionné que vous appeliez une application Web.
Kin Shah

Désolé Kin, j'aurais dû être plus spécifique au type d'application.
webworm

Réponses:


10

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 une fonction très cool (et sous-utilisée) appelée 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 me convertis en CHAR(128)avant le CONVERT(VARBINARY(128), ... Il s'agit de forcer le remplissage pour faciliter la reconversion VARCHARlorsque vous le retirez CONTEXT_INFO()car il VARBINARY(128)est rempli à droite avec 0x00s.

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!

METTRE TOUS 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_INFOn'a pas été défini et est toujours NULL:

      Pour cette raison, j'ai mis à jour ce qui précède INSERT INTO AuditTablepour utiliser un COALESCEpar défaut la valeur. Ou, si vous ne voulez pas de valeur par défaut et 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_INFOa été défini sur une valeur qui n'est pas un nom d'utilisateur valide et peut donc dépasser la taille du AuditTable.[UserWhoMadeChanges]champ:

      Pour cette raison, j'ai ajouté une LEFTfonction pour garantir que tout ce qui est récupéré CONTEXT_INFOne cassera pas le INSERT. Comme indiqué dans le code, il vous suffit de définir la 50à la taille réelle du UserWhoMadeChangeschamp.


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 () ne renvoie-t-il 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 CONTEXT_INFO)

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


Le problème avec cette approche est qu'elle est TRÈS volatile. N'importe quelle session peut définir cela, en tant que telle, elle peut remplacer tout élément précédemment défini. Vous voulez vraiment casser votre application? avoir un seul dev écraser ce que vous attendez. Je conseillerais fortement de NE PAS l'utiliser et d'avoir une approche standard qui peut nécessiter un changement d'architecture. Sinon, vous jouez avec le feu.
Sean Gallardy

@SeanGallardy Pouvez-vous s'il vous plaît fournir un exemple réel de ce qui se passe? Session == @@SPID. Il s'agit de la mémoire PER-Session / Connection. Une session ne peut pas remplacer les informations de contexte d'une autre session. Et lorsque la session se déconnecte, la valeur disparaît. Il n'y a rien de tel qu'un "élément précédemment défini".
Solomon Rutzky

1
Je n'ai pas dit "une autre session", j'ai dit que n'importe quel objet dans la portée de la session peut faire cela. Ainsi, un développeur écrit un sproc pour contenir ses propres informations "contextuelles" et maintenant les vôtres sont écrasées. Il y avait une application avec laquelle je devais faire face qui utilisait ce même schéma, je l'ai vu se produire ... c'était un logiciel RH. Permettez-moi de vous dire à quel point les gens heureux ne devaient PAS être payés à temps en raison d'un "bug" par l'un des développeurs écrivant un nouveau SP qui a mis à jour à tort les informations de contexte de la session à partir de ce qu'elles étaient "supposées" être. Donnant juste un exemple, j'ai été témoin de pourquoi ne pas utiliser cette méthode.
Sean Gallardy

@SeanGallardy Ok, merci d'avoir clarifié ce point. Mais ce n'est encore qu'un point partiellement valable. Pour que cette situation se produise, cet "autre" processus devrait être appelé à l'intérieur de celui-ci. Ou, si vous parlez d'un autre processus qui pourrait être supprimé de ce tableau et déclencher le déclencheur, c'est quelque chose qui peut être testé. C'est une condition de concurrence, qui doit être prise en compte (tout comme elles le sont dans toutes les applications multithreads), et non une raison de ne pas utiliser cette technique. Et donc je ferai une mise à jour mineure pour faire exactement cela. Merci d'avoir évoqué cette possibilité.
Solomon Rutzky

2
Je dis que la sécurité après coup est le principal problème et ce n'est pas l'outil pour le résoudre. Structures de mémo ou autres utilisations qui ne cassent pas l'application, je n'ai aucun problème. C'est absolument une raison de NE PAS l'utiliser. YMMV mais je n'utiliserais jamais quelque chose d'aussi volatile et non structuré pour quelque chose d'important comme la sécurité. L'utilisation de tout type de stockage accessible en écriture pour la sécurité est une terrible idée dans l'ensemble. Une conception appropriée éliminerait la nécessité de choses comme celle-ci, pour la plupart.
Sean Gallardy

5

Vous ne pouvez pas procéder ainsi, 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 définissant selon vos besoins, 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 deletedéclencheur qui déclenche une erreur. Si vous ne souhaitez pas ajouter une colonne à votre table physique, vous pouvez définir une vue qui ajoute la colonne et définir des instead ofdéclencheurs pour gérer la mise à jour de la table de base, mais cela peut être excessif.


Je vois ce que tu veux dire. Je chercherais en effet à connecter l'utilisateur au niveau application.
webworm

David, en fait, vous pouvez transmettre des informations aux déclencheurs. Veuillez voir ma réponse pour plus de détails :).
Solomon Rutzky

Bonne suggestion ici, j'aime vraiment cet itinéraire. Tue deux oiseaux en capturant Who au même moment que le déclenchement de la suppression réelle. Étant donné que cette colonne va être NULL pour chaque enregistrement de cette table, il semble que ce serait une bonne utilisation de la SPARSEcolonne SQL Server ?
Airn5475

2

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 ;-). S'il y a des réserves sur l'utilisation CONTEXT_INFOcomme je l'ai suggéré dans mon autre réponse ici , j'ai juste pensé à une autre manière 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 l'aidera à être séparé 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 CONTEXT_INFOest que si quelqu'un dans un autre processus - qui est en quelque sorte appelé à partir de ce processus "Supprimer" particulier - arrive juste d'utiliser incorrectement le même nom de table temporaire, le sous-processus a) créera un nouveau local table temporaire du nom demandé qui sera distinct de cette table temporaire initiale (même si elle a 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 des problèmes de sous - processus une déclaration de DML contre ce nom de table temporaire sans émettre d' abord une CREATE TABLE de ce même nom, ces déclarations DML auront une incidence sur les données de ce tableau. MAIS, à ce stade, nous obtenons vraimentbord-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 "bord-cas" 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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.