Übergeben von Informationen darüber, wer den Datensatz gelöscht hat, an einen Löschauslöser

11

Beim Einrichten eines Prüfpfads habe ich kein Problem damit, zu verfolgen, wer Datensätze aktualisiert oder in eine Tabelle einfügt. Das Verfolgen, wer Datensätze löscht, scheint jedoch problematischer.

Ich kann Einfügungen / Aktualisierungen verfolgen, indem ich in das Feld Einfügen / Aktualisieren das Feld "UpdatedBy" einfüge. Dadurch kann der INSERT / UPDATE-Trigger über auf das Feld "UpdatedBy" zugreifen inserted.UpdatedBy. Mit dem Delete-Trigger werden jedoch keine Daten eingefügt / aktualisiert. Gibt es eine Möglichkeit, Informationen an den Trigger "Löschen" zu übergeben, damit dieser weiß, wer den Datensatz gelöscht hat?

Hier ist ein Einfüge- / Aktualisierungsauslöser

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 

Verwenden von SQL Server 2012

Webworm
quelle
1
Siehe diese Antwort. SUSER_SNAME()ist der Schlüssel, um herauszufinden, wer den Datensatz gelöscht hat.
Kin Shah
1
Vielen Dank an Kin, aber ich glaube nicht, dass SUSER_SNAME()dies in einer Situation wie einer Webanwendung funktionieren würde, in der ein einzelner Benutzer für die Datenbankkommunikation für die gesamte Anwendung verwendet werden könnte.
Webworm
1
Sie haben nicht erwähnt, dass Sie eine Web-App aufrufen.
Kin Shah
Sorry Kin, ich hätte genauer auf den Anwendungstyp eingehen sollen.
Webworm

Antworten:

10

Gibt es eine Möglichkeit, Informationen an den Trigger "Löschen" zu übergeben, damit dieser weiß, wer den Datensatz gelöscht hat?

Ja, indem Sie eine sehr coole (und nicht ausreichend genutzte) Funktion verwenden CONTEXT_INFO. Es ist im Wesentlichen der Sitzungsspeicher, der in allen Bereichen vorhanden ist und nicht an Transaktionen gebunden ist. Es kann verwendet werden, um Informationen (alle Informationen - also alle, die in den begrenzten Raum passen) an Trigger sowie zwischen Sub-Proc / EXEC-Aufrufen hin und her zu übergeben. Und ich habe es schon einmal für genau dieselbe Situation verwendet.

Testen Sie Folgendes, um zu sehen, wie es funktioniert. Beachten Sie, dass ich CHAR(128)vor dem konvertiere CONVERT(VARBINARY(128), ... Dies dient dazu, das Auffüllen von Leerzeichen zu erzwingen, um das Zurückkonvertieren zu erleichtern, VARCHARwenn es herausgenommen wird, CONTEXT_INFO()da VARBINARY(128)es mit 0x00s rechts aufgefüllt ist .

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

Ergebnisse:

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

Alles zusammenfügen:

  1. Die App sollte eine gespeicherte Prozedur "Löschen" aufrufen, die den Benutzernamen (oder was auch immer) übergibt, der den Datensatz löscht. Ich gehe davon aus, dass dies bereits das verwendete Modell ist, da es so klingt, als würden Sie bereits Einfüge- und Aktualisierungsvorgänge verfolgen.

  2. Die gespeicherte Prozedur "Löschen" führt Folgendes aus:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Der Audit-Trigger führt Folgendes aus:

    -- 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. Bitte beachten Sie, dass, wie @SeanGallardy in einem Kommentar hervorhob, aufgrund anderer Verfahren und / oder Ad-hoc-Abfragen beim Löschen von Datensätzen aus dieser Tabelle Folgendes möglich ist:

    • CONTEXT_INFOwurde nicht eingestellt und ist noch NULL:

      Aus diesem Grund habe ich das oben Gesagte aktualisiert INSERT INTO AuditTable, um COALESCEden Wert als Standardwert zu verwenden. Wenn Sie keine Standardeinstellung wünschen und einen Namen benötigen, können Sie Folgendes tun:

      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_INFOwurde auf einen Wert festgelegt, der kein gültiger Benutzername ist und daher möglicherweise die Größe des AuditTable.[UserWhoMadeChanges]Felds überschreitet :

      Aus diesem Grund habe ich eine LEFTFunktion hinzugefügt , um sicherzustellen, dass alles, woraus herausgegriffen CONTEXT_INFOwird, nicht kaputt geht INSERT. Wie im Code angegeben, müssen Sie nur 50die tatsächliche Größe des UserWhoMadeChangesFelds festlegen .


UPDATE FÜR SQL SERVER 2016 UND NEUER

SQL Server 2016 hat eine verbesserte Version dieses Sitzungsspeichers hinzugefügt: Sitzungskontext. Der neue Sitzungskontext ist im Wesentlichen eine Hash-Tabelle von Schlüssel-Wert-Paaren, wobei der "Schlüssel" vom Typ sysname(dh NVARCHAR(128)) und der "Wert" ist SQL_VARIANT. Bedeutung:

  1. Es gibt jetzt eine Wertetrennung, die weniger wahrscheinlich mit anderen Verwendungen in Konflikt steht
  2. Sie können verschiedene Typen speichern, ohne sich um das seltsame Verhalten kümmern zu müssen, wenn Sie den Wert über zurückgeben CONTEXT_INFO()(Details finden Sie in meinem Beitrag: Warum gibt CONTEXT_INFO () den von SET CONTEXT_INFO festgelegten genauen Wert nicht zurück? )
  3. Sie erhalten viel mehr Speicherplatz: maximal 8000 Byte pro "Wert", insgesamt bis zu 256 KB über alle Schlüssel hinweg (im Vergleich zu maximal 128 Byte CONTEXT_INFO)

Weitere Informationen finden Sie auf den folgenden Dokumentationsseiten:

Solomon Rutzky
quelle
Das Problem bei diesem Ansatz ist, dass er SEHR flüchtig ist. Jede Sitzung kann dies festlegen, sodass jedes zuvor festgelegte Element überschrieben werden kann. Möchten Sie Ihre Bewerbung wirklich brechen? Lassen Sie einen einzelnen Entwickler überschreiben, was Sie erwarten. Ich würde dringend empfehlen, dies NICHT zu verwenden und einen Standardansatz zu verwenden, der möglicherweise eine Änderung der Architektur erfordert. Ansonsten spielst du mit dem Feuer.
Sean Gallardy
@ SeanGallardy Können Sie bitte ein aktuelles Beispiel dafür geben? Sitzung == @@SPID. Dies ist der PER-Session / Connection-Speicher. Eine Sitzung kann die Kontextinformationen einer anderen Sitzung nicht überschreiben. Und wenn sich die Sitzung abmeldet, verschwindet der Wert. Es gibt kein "zuvor festgelegtes Element".
Solomon Rutzky
1
Ich habe nicht "eine andere Sitzung" gesagt. Ich habe gesagt, dass jedes Objekt im Sitzungsbereich dies kann. Ein Entwickler schreibt also einen Sproc, um seine eigenen "kontextbezogenen" Informationen zu speichern, und jetzt wird Ihre überschrieben. Es gab eine Anwendung, mit der ich mich befassen musste, die dasselbe Muster verwendete. Ich habe beobachtet, wie es passierte ... es war eine HR-Software. Lassen Sie mich Ihnen sagen, wie glücklich die Leute waren, aufgrund eines "Fehlers" durch einen der Entwickler, der einen neuen SP schrieb, der die Kontextinformationen für die Sitzung fälschlicherweise von dem "aktualisiert" hat, NICHT pünktlich bezahlt zu werden. Ich gebe nur ein Beispiel, warum ich diese Methode nicht anwenden sollte.
Sean Gallardy
@ SeanGallardy Ok, danke, dass du diesen Punkt klargestellt hast. Aber es ist immer noch nur ein teilweise gültiger Punkt. Damit diese Situation eintreten kann, müsste dieser "andere" Prozess in diesem aufgerufen werden. Oder wenn Sie über einen anderen Prozess sprechen, der möglicherweise aus dieser Tabelle gelöscht und den Auslöser ausgelöst wird, kann dies getestet werden. Es ist eine Rennbedingung, die berücksichtigt werden muss (genau wie in allen Multithread-Apps), und kein Grund, diese Technik nicht zu verwenden. Und so werde ich ein kleines Update machen, um genau das zu tun. Vielen Dank, dass Sie diese Möglichkeit angesprochen haben.
Solomon Rutzky
2
Ich sage, Sicherheit als nachträglicher Gedanke ist das Hauptproblem und dies ist nicht das Werkzeug, um es zu lösen. Memostrukturen oder andere Verwendungen, die die Anwendung nicht beschädigen, haben sicher kein Problem. Es ist absolut ein Grund, es NICHT zu benutzen. YMMV, aber ich würde niemals etwas so Flüchtiges und Unstrukturiertes für etwas Wichtiges wie Sicherheit verwenden. Die Verwendung eines beliebigen beschreibbaren Speichers für gemeinsam genutzte Benutzer zur Sicherheit ist insgesamt eine schreckliche Idee. Ein korrektes Design würde solche Dinge zum größten Teil überflüssig machen.
Sean Gallardy
5

Dies ist nur möglich, wenn Sie die SQL Server-Benutzer-ID anstelle einer Anwendungsebene 1 aufzeichnen möchten.

Sie können einen Soft-Löschvorgang durchführen, indem Sie eine Spalte mit dem Namen DeletedBy verwenden und diese nach Bedarf festlegen. Anschließend kann Ihr Update-Trigger den eigentlichen Löschvorgang (oder die Archivierung des Datensatzes, ich vermeide im Allgemeinen harte Löschvorgänge, soweit dies möglich und legal ist) sowie die Aktualisierung Ihres Prüfpfads durchführen . Um das Löschen auf diese Weise zu erzwingen, definieren Sie einen on deleteTrigger, der einen Fehler auslöst. Wenn Sie Ihrer physischen Tabelle keine Spalte hinzufügen möchten, können Sie eine Ansicht definieren, in der die Spalte hinzugefügt und instead ofTrigger für die Aktualisierung der Basistabelle definiert werden. Dies kann jedoch zu viel des Guten sein.

David Spillett
quelle
Ich verstehe dein Argument. Ich würde in der Tat versuchen, den Benutzer auf Anwendungsebene zu protokollieren.
Webworm
David, eigentlich kannst du Informationen an Trigger weitergeben. Bitte siehe meine Antwort für Details :).
Solomon Rutzky
Guter Vorschlag hier, ich mag diese Route wirklich. Tötet zwei Vögel, indem Sie Who im selben Schritt wie das Auslösen des echten Löschvorgangs erfassen. Da diese Spalte für jeden Datensatz in dieser Tabelle NULL sein wird, scheint es eine gute Verwendung der SQL Server- SPARSESpalte zu sein?
Airn5475
2

Gibt es eine Möglichkeit, Informationen an den Trigger "Löschen" zu übergeben, damit dieser weiß, wer den Datensatz gelöscht hat?

Ja, anscheinend gibt es zwei Möglichkeiten ;-). Wenn es irgendwelche Vorbehalte gegen die Verwendung gibt, CONTEXT_INFOwie ich in meiner anderen Antwort hier vorgeschlagen habe , habe ich mir nur einen anderen Weg überlegt, der eine sauberere funktionale Trennung von anderen Codes / Prozessen aufweist: Verwenden Sie eine lokale temporäre Tabelle.

Der temporäre Tabellenname sollte den Tabellennamen enthalten, aus dem gelöscht wird, da dies dazu beiträgt, ihn von jedem anderen Code zu trennen, der möglicherweise in derselben Sitzung ausgeführt wird. Etwas in der Art von:
#<TableName>DeleteAudit

Ein Vorteil einer lokalen temporären Tabelle CONTEXT_INFObesteht darin, dass der Unterprozess a) einen neuen lokalen Namen erstellt, wenn jemand in einem anderen Prozess - der irgendwie von diesem bestimmten "Löschen" -Prozess aufgerufen wird - zufällig denselben temporären Tabellennamen falsch verwendet temporäre Tabelle des angeforderten Namens, die von dieser anfänglichen temporären Tabelle getrennt wird (obwohl sie denselben Namen hat), und b) DML-Anweisungen für die neue lokale temporäre Tabelle im Unterprozess wirken sich nicht auf Daten in der aus Lokale temporäre Tabelle, die hier im übergeordneten Prozess erstellt wurde, daher kein Überschreiben von Daten. Natürlich, wenn eine Subprozess Frage eine DML - Anweisung gegen diesen Namen temporären Tabelle , ohne zuerst eine Ausgabe Tabelle des gleichen Namen erstellen, dann wird diese DML - Anweisungen werden die Daten in dieser Tabelle beeinflussen. ABER an diesem Punkt bekommen wir wirklichEdge-Casey hier, noch mehr als mit der Wahrscheinlichkeit überlappender Verwendungen von CONTEXT_INFO(ja, ich weiß, dass es passiert ist, weshalb ich eher "Edge-Case" als "es wird nie passieren" sage).

  1. Die App sollte eine gespeicherte Prozedur "Löschen" aufrufen, die den Benutzernamen (oder was auch immer) übergibt, der den Datensatz löscht. Ich gehe davon aus, dass dies bereits das verwendete Modell ist, da es so klingt, als würden Sie bereits Einfüge- und Aktualisierungsvorgänge verfolgen.

  2. Die gespeicherte Prozedur "Löschen" führt Folgendes aus:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Der Audit-Trigger führt Folgendes aus:

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

    Ich habe diesen Code in einem Trigger getestet und er funktioniert wie erwartet.

Solomon Rutzky
quelle