Passare informazioni su chi ha eliminato il record su un trigger Elimina


11

Nella creazione di una pista di controllo non ho problemi a rintracciare chi sta aggiornando o inserendo i record in una tabella, tuttavia, il tracciamento di chi elimina i record sembra più problematico.

Sono in grado di tenere traccia degli inserti / aggiornamenti includendo nel campo Inserisci / Aggiorna il campo "Aggiornato". Ciò consente al trigger INSERT / UPDATE di accedere al campo "UpdatedBy" tramite inserted.UpdatedBy. Tuttavia, con il trigger Elimina non vengono inseriti / aggiornati dati. C'è un modo per passare informazioni sul trigger Elimina in modo che possa sapere chi ha eliminato il record?

Ecco un trigger Inserisci / Aggiorna

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 

Utilizzando SQL Server 2012


1
Vedi questa risposta SUSER_SNAME()è la chiave per ottenere chi ha eliminato il record.
Kin Shah,

1
Grazie Kin, tuttavia non credo SUSER_SNAME()funzionerebbe in una situazione come un'applicazione Web in cui un singolo utente potrebbe essere utilizzato per la comunicazione di database per l'intera applicazione.
webworm

1
Non hai detto che stavi chiamando un'app Web.
Kin Shah,

Mi spiace Kin, avrei dovuto essere più specifico per il tipo di applicazione.
webworm

Risposte:


10

C'è un modo per passare informazioni sul trigger Elimina in modo che possa sapere chi ha eliminato il record?

Sì: usando una funzione molto interessante (e sotto utilizzata) chiamata CONTEXT_INFO. È essenzialmente la memoria di sessione che esiste in tutti gli ambiti e non è vincolata da transazioni. Può essere usato per trasmettere informazioni (qualsiasi informazione - beh, qualsiasi cosa si adatti allo spazio limitato) ai trigger, nonché avanti e indietro tra chiamate sub-proc / EXEC. E l'ho usato prima per questa stessa identica situazione.

Prova con quanto segue per vedere come funziona. Si noti che sto convertendo in CHAR(128)prima del CONVERT(VARBINARY(128), ... Questo per forzare l'imbottitura in bianco per rendere più semplice la riconversione VARCHARquando si esce da CONTEXT_INFO()poiché VARBINARY(128)è imbottito a destra con 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];

risultati:

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

METTERE TUTTO INSIEME:

  1. L'app dovrebbe chiamare una procedura memorizzata "Elimina" che passa nel UserName (o qualunque altra cosa) che sta eliminando il record. Presumo che questo sia già il modello utilizzato poiché sembra che tu stia già monitorando le operazioni di inserimento e aggiornamento.

  2. La procedura memorizzata "Elimina" fa:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Il trigger di controllo fa:

    -- 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. Si noti che, come sottolineato da @SeanGallardy in un commento, a causa di altre procedure e / o query ad hoc che eliminano i record da questa tabella, è possibile che:

    • CONTEXT_INFOnon è stato impostato ed è ancora NULL:

      Per questo motivo ho aggiornato quanto sopra INSERT INTO AuditTableper utilizzare un COALESCEvalore predefinito. Oppure, se non desideri un valore predefinito e richiedi un nome, puoi fare qualcosa di simile a:

      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è stato impostato su un valore che non è un UserName valido e quindi potrebbe superare la dimensione del AuditTable.[UserWhoMadeChanges]campo:

      Per questo motivo ho aggiunto una LEFTfunzione per garantire che tutto ciò che viene estratto CONTEXT_INFOnon interromperà INSERT. Come indicato nel codice, è sufficiente impostare la 50dimensione effettiva del UserWhoMadeChangescampo.


AGGIORNAMENTO PER SQL SERVER 2016 E PIÙ RECENTI

SQL Server 2016 ha aggiunto una versione migliorata di questa memoria per sessione: contesto di sessione. Il nuovo contesto di sessione è essenzialmente una tabella hash di coppie chiave-valore con "chiave" di tipo sysname(ovvero NVARCHAR(128)) e "valore" SQL_VARIANT. Senso:

  1. Ora esiste una separazione di valori, quindi meno probabile che sia in conflitto con altri usi
  2. È possibile memorizzare vari tipi, non è più necessario preoccuparsi del comportamento strano quando si ottiene il valore indietro tramite CONTEXT_INFO()(per i dettagli, consultare il mio post: Perché CONTEXT_INFO () non restituisce il valore esatto impostato da SET CONTEXT_INFO? )
  3. Ottieni molto più spazio: 8000 byte max per "Valore", fino a 256 kb totali su tutte le chiavi (rispetto ai 128 byte max di CONTEXT_INFO)

Per i dettagli, consultare le seguenti pagine di documentazione:


Il problema con questo approccio è che è MOLTO volatile. Qualsiasi sessione può impostare questo, in quanto tale può sovrascrivere qualsiasi elemento precedentemente impostato. Vuoi davvero infrangere la tua candidatura? fai in modo che un singolo sviluppatore sovrascriva ciò che ti aspetti. Consiglio vivamente di NON utilizzare questo e avere un approccio standard che potrebbe richiedere una modifica dell'architettura. Altrimenti, stai giocando con il fuoco.
Sean Gallardy,

@SeanGallardy Puoi per favore fornire un esempio reale di ciò che sta accadendo? Sessione == @@SPID. Questa è la memoria PER-Session / Connection. Una sessione non può sovrascrivere le informazioni sul contesto di un'altra sessione. E quando la sessione si disconnette, il valore scompare. Non esiste un "elemento impostato in precedenza".
Solomon Rutzky,

1
Non ho detto "un'altra sessione". Ho detto che qualsiasi oggetto nell'ambito della sessione può farlo. Quindi, uno sviluppatore scrive uno sproc per conservare le proprie informazioni "contestuali" e ora le tue vengono sovrascritte. C'era un'applicazione con cui ho avuto a che fare con questo stesso schema, l'ho visto accadere ... era un software per le risorse umane. Lascia che ti dica come le persone felici NON dovevano essere pagate in tempo a causa di un "bug" da parte di uno degli sviluppatori che scriveva un nuovo SP che aggiornava erroneamente le informazioni di contesto per la sessione da quello che doveva "essere". Solo facendo un esempio, ho visto perché non usare questo metodo.
Sean Gallardy,

@SeanGallardy Ok, grazie per aver chiarito questo punto. Ma è ancora solo un punto parzialmente valido. Affinché tale situazione si verifichi, quel "altro" proc dovrebbe essere chiamato all'interno di questo. Oppure, se stai parlando di qualche altro proc che potrebbe essere cancellato da questa tabella e dare il via al grilletto, è qualcosa che può essere testato. È una condizione di razza, che è qualcosa di cui tenere conto (così come lo sono in tutte le app multithreading) e non un motivo per non usare questa tecnica. E così farò un piccolo aggiornamento per fare proprio questo. Grazie per aver sollevato questa possibilità.
Solomon Rutzky,

2
Sto dicendo che la sicurezza come ripensamento è il problema principale e questo non è lo strumento per risolverlo. Strutture di promemoria o altri usi che non interrompono l'applicazione, sicuramente non ho problemi. È assolutamente un motivo per NON usarlo. YMMV ma non userei mai qualcosa di così volatile e non strutturato per qualcosa di importante come la sicurezza. L'utilizzo di qualsiasi tipo di spazio di archiviazione scrivibile dall'utente condiviso per la sicurezza è nel complesso un'idea terribile. Una progettazione corretta eliminerebbe la necessità di cose come questa, per la maggior parte.
Sean Gallardy,

5

Non è possibile in questo modo, a meno che non si desideri registrare l'ID utente del server SQL anziché uno a livello di applicazione.

È possibile eseguire una cancellazione soft avendo una colonna chiamata DeletedBy e impostandola come necessario, quindi il trigger di aggiornamento può eseguire la vera cancellazione (o archiviare il record, in genere evito le cancellazioni forzate ove possibile e legale), nonché aggiornando la pista di controllo . Per forzare le eliminazioni da eseguire in questo modo, definire un on deletetrigger che genera un errore. Se non si desidera aggiungere una colonna alla tabella fisica, è possibile definire una vista che aggiunge la colonna e i instead oftrigger per gestire l'aggiornamento della tabella di base, ma potrebbe essere eccessivo.


Vedo il tuo punto. Sarei davvero cercando di registrare l'utente a livello di applicazione.
webworm

David, in realtà puoi trasmettere informazioni ai trigger. Si prega di consultare la mia risposta per i dettagli :).
Solomon Rutzky,

Un buon suggerimento qui, mi piace molto questo itinerario. Uccide due uccelli catturando Chi nello stesso passaggio in cui si avvia la vera eliminazione. Dal momento che questa colonna sarà NULL per ogni record in questa tabella, sembra che sarebbe un buon uso della SPARSEcolonna di SQL Server ?
Airn5475,

2

C'è un modo per passare informazioni sul trigger Elimina in modo che possa sapere chi ha eliminato il record?

Sì, apparentemente ci sono due modi ;-). Se ci sono delle riserve sull'uso CONTEXT_INFOcome ho suggerito nella mia altra risposta qui , ho appena pensato a un altro modo che ha una separazione funzionale più pulita da altri codici / processi: utilizzare una tabella temporanea locale.

Il nome della tabella temporanea deve includere il nome della tabella da cui viene eliminato poiché ciò contribuirà a tenerlo separato da qualsiasi altro codice che potrebbe capitare di essere eseguito nella stessa sessione. Qualcosa sulla falsariga di:
#<TableName>DeleteAudit

Un vantaggio di una tabella temporanea locale CONTEXT_INFOè che se qualcuno in un altro proc - che è in qualche modo chiamato da questo particolare proc "Elimina" - capita semplicemente di usare lo stesso nome di tabella temporanea, il sottoprocesso a) creerà un nuovo locale tabella temporanea del nome richiesto che sarà separata da questa tabella temporanea iniziale (anche se ha lo stesso nome) eb) eventuali istruzioni DML rispetto alla nuova tabella temporanea locale nel processo secondario non influiranno su alcun dato nel tabella temporanea locale creata qui nel processo padre, quindi nessuna sovrascrittura dei dati. Naturalmente, se una questioni di processo parziali un'istruzione DML contro questo nome tabella temporanea senza prima emissione di creare la tabella dello stesso nome, quindi quelle istruzioni DML avranno effetto sui dati di questa tabella. MA, a questo punto, stiamo davvero diventandoedge-casey qui, anche più che con la probabilità di usi sovrapposti di CONTEXT_INFO(sì, lo so che è successo, motivo per cui dico "edge-case" piuttosto che "non accadrà mai").

  1. L'app dovrebbe chiamare una procedura memorizzata "Elimina" che passa nel UserName (o qualunque altra cosa) che sta eliminando il record. Presumo che questo sia già il modello utilizzato poiché sembra che tu stia già monitorando le operazioni di inserimento e aggiornamento.

  2. La procedura memorizzata "Elimina" fa:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Il trigger di controllo fa:

    -- 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;

    Ho testato questo codice in un trigger e funziona come previsto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.