Access
 sql >> Datenbank >  >> RDS >> Access

Entwerfen eines Microsoft T-SQL-Triggers

Entwerfen eines Microsoft T-SQL-Triggers

Gelegentlich beim Erstellen eines Projekts mit einem Access-Front-End und einem SQL Server-Back-End sind wir auf diese Frage gestoßen. Sollten wir einen Auslöser für etwas verwenden? Das Entwerfen eines SQL Server-Triggers für Access-Anwendungen kann eine Lösung sein, aber nur nach sorgfältiger Überlegung. Manchmal wird dies vorgeschlagen, um die Geschäftslogik in der Datenbank und nicht in der Anwendung zu halten. Normalerweise möchte ich, dass die Geschäftslogik so nah wie möglich an der Datenbank definiert wird. Ist Trigger also die Lösung, die wir für unser Access-Front-End wollen?

Ich habe festgestellt, dass das Codieren eines SQL-Triggers zusätzliche Überlegungen erfordert, und wenn wir nicht aufpassen, können wir mit einem größeren Durcheinander enden, als wir begonnen haben. Der Artikel zielt darauf ab, alle Fallstricke und Techniken abzudecken, die wir verwenden können, um sicherzustellen, dass sie beim Aufbau einer Datenbank mit Triggern zu unserem Vorteil funktionieren, anstatt nur der Komplexität wegen Komplexität hinzuzufügen.

Betrachten wir die Regeln…

Regel Nr. 1:Verwenden Sie keinen Trigger!

Ernsthaft. Wenn Sie morgens als erstes nach dem Auslöser greifen, werden Sie es nachts bereuen. Das größte Problem mit Triggern im Allgemeinen besteht darin, dass sie Ihre Geschäftslogik effektiv verschleiern und Prozesse stören können, die keinen Trigger benötigen sollten. Ich habe einige Vorschläge zum Deaktivieren von Triggern gesehen, wenn Sie eine Massenladung oder ähnliches durchführen. Ich behaupte, dass dies ein großer Code-Geruch ist. Sie sollten keinen Trigger verwenden, wenn er bedingt ein- oder ausgeschaltet werden muss.

Standardmäßig sollten wir zuerst gespeicherte Prozeduren oder Ansichten schreiben. Für die meisten Szenarien werden sie die Arbeit gut machen. Fügen wir hier keine Magie hinzu.

Warum dann der Artikel über Trigger?

Denn Trigger haben ihren Nutzen. Wir müssen erkennen, wann wir Trigger verwenden sollten. Wir müssen sie auch so schreiben, dass sie uns mehr helfen als schaden.

Regel Nr. 2:Brauche ich wirklich einen Trigger?

Theoretisch klingt Trigger gut. Sie stellen uns ein ereignisbasiertes Modell zur Verfügung, um Änderungen zu verwalten, sobald sie geändert werden. Aber wenn Sie nur einige Daten validieren oder sicherstellen müssen, dass einige versteckte Spalten oder Protokollierungstabellen gefüllt sind …. Ich denke, Sie werden feststellen, dass eine gespeicherte Prozedur die Arbeit effizienter erledigt und den magischen Aspekt entfernt. Darüber hinaus ist das Schreiben einer gespeicherten Prozedur einfach zu testen; Richten Sie einfach einige Scheindaten ein und führen Sie die gespeicherte Prozedur aus. Überprüfen Sie, ob die Ergebnisse Ihren Erwartungen entsprechen. Ich hoffe, Sie verwenden ein Test-Framework wie tSQLt.

Und es ist wichtig zu beachten, dass es normalerweise effizienter ist, Datenbankeinschränkungen als einen Trigger zu verwenden. Wenn Sie also nur überprüfen müssen, ob ein Wert in einer anderen Tabelle gültig ist, verwenden Sie eine Fremdschlüsseleinschränkung. Die Überprüfung, ob ein Wert innerhalb eines bestimmten Bereichs liegt, erfordert eine Check-Einschränkung. Dies sollte Ihre Standardauswahl für diese Art von Validierungen sein.

Wann brauchen wir also tatsächlich einen Trigger?

Es läuft auf Fälle hinaus, in denen Sie wirklich möchten, dass sich die Geschäftslogik in der SQL-Schicht befindet. Vielleicht, weil Sie mehrere Clients in verschiedenen Programmiersprachen haben, die Einfügungen/Aktualisierungen an einer Tabelle vornehmen. Es wäre sehr umständlich, die Geschäftslogik für jeden Client in seiner jeweiligen Programmiersprache zu duplizieren, und dies bedeutet auch mehr Fehler. In Szenarien, in denen es nicht praktikabel ist, eine Ebene der mittleren Ebene zu erstellen, sind Trigger die beste Vorgehensweise, um die Geschäftsregel durchzusetzen, die nicht als Einschränkung ausgedrückt werden kann.

So verwenden Sie ein Access-spezifisches Beispiel. Angenommen, wir möchten Geschäftslogik erzwingen, wenn Daten über die Anwendung geändert werden. Vielleicht haben wir mehrere Dateneingabeformulare, die an dieselbe Tabelle gebunden sind, oder vielleicht müssen wir komplexe Dateneingabeformulare unterstützen, bei denen mehrere Basistabellen an der Bearbeitung teilnehmen müssen. Vielleicht muss das Dateneingabeformular nicht normalisierte Eingaben unterstützen, die wir dann wieder in normalisierte Daten zusammensetzen. In all diesen Fällen könnten wir einfach VBA-Code schreiben, aber das kann für alle Fälle schwierig zu warten und zu validieren sein. Trigger helfen uns, die Logik aus VBA in T-SQL zu verschieben. Die datenzentrische Geschäftslogik wird im Allgemeinen am besten so nah wie möglich an den Daten platziert.

Regel Nr. 3:Trigger muss satzbasiert sein, nicht zeilenbasiert

Der bei weitem häufigste Fehler, der bei einem Trigger gemacht wird, besteht darin, ihn auf Zeilen laufen zu lassen. Oft sehen wir Code ähnlich diesem:

--Bad code! Do not use!
CREATE TRIGGER dbo.SomeTrigger
ON dbo.SomeTable AFTER INSERT
AS
BEGIN
  DECLARE @NewTotal money;
  DECLARE @NewID int;

  SELECT TOP 1
    @NewID = SalesOrderID,
    @NewTotal = SalesAmount
  FROM inserted;

  UPDATE dbo.SalesOrder
  SET OrderTotal = OrderTotal + @NewTotal
  WHERE SalesOrderID = @SalesOrderID
END;

Das Werbegeschenk sollte die bloße Tatsache sein, dass es ein SELECT TOP 1 von einem Tisch gab eingefügt. Dies funktioniert nur, solange wir nur eine Zeile einfügen. Aber wenn es mehr als eine Reihe ist, was passiert dann mit den unglücklichen Reihen, die an zweiter Stelle und danach kamen? Wir können dies verbessern, indem wir etwas Ähnliches tun:

--Still bad code! Do not use!
CREATE TRIGGER dbo.SomeTrigger
ON dbo.SomeTable AFTER INSERT
AS
BEGIN
  MERGE INTO dbo.SalesOrder AS s
  USING inserted AS i
  ON s.SalesOrderID = i.SalesOrderID
  WHEN MATCHED THEN UPDATE SET
    OrderTotal = OrderTotal + @NewTotal
  ;
END;

Dies ist jetzt satzbasiert und daher stark verbessert, aber es hat noch andere Probleme, die wir in den nächsten Regeln sehen werden …

Regel 4:Verwenden Sie stattdessen eine Ansicht.

An eine Ansicht kann ein Trigger angehängt werden. Dies gibt uns den Vorteil, Probleme im Zusammenhang mit Tabellentriggern zu vermeiden. Wir könnten problemlos saubere Daten in die Tabelle importieren, ohne Trigger deaktivieren zu müssen. Darüber hinaus macht ein Trigger on View es zu einer expliziten Opt-in-Wahl. Wenn Sie sicherheitsrelevante Funktionen oder Geschäftsregeln haben, die das Ausführen von Triggern erfordern, können Sie die Berechtigungen für die Tabelle einfach direkt widerrufen und sie stattdessen in die neue Ansicht leiten. Dadurch wird sichergestellt, dass Sie das Projekt durchgehen und notieren, wo Aktualisierungen an der Tabelle erforderlich sind, damit Sie sie dann auf mögliche Fehler oder Probleme verfolgen können.

Der Nachteil ist, dass einer Ansicht nur ein INSTEAD OF-Trigger zugeordnet werden kann, was bedeutet, dass Sie die entsprechenden Änderungen an der Basistabelle explizit selbst innerhalb des Triggers durchführen müssen. Ich denke jedoch, dass es so besser ist, weil es auch sicherstellt, dass Sie genau wissen, was die Änderung sein wird, und Ihnen somit das gleiche Maß an Kontrolle gibt, das Sie normalerweise innerhalb einer gespeicherten Prozedur haben.

Regel Nr. 5:Der Auslöser sollte dumm einfach sein.

Erinnern Sie sich an den Kommentar zum Debuggen und Testen einer gespeicherten Prozedur? Der beste Gefallen, den wir uns selbst tun können, besteht darin, die Geschäftslogik in einer gespeicherten Prozedur zu belassen und sie stattdessen vom Trigger aufrufen zu lassen. Sie sollten niemals Geschäftslogik direkt in den Trigger schreiben; das gießt effektiv Beton auf die Datenbank. Es ist jetzt in der Form eingefroren und es kann problematisch sein, die Logik angemessen zu testen. Ihr Testgeschirr muss jetzt einige Änderungen an der Basistabelle beinhalten. Das ist nicht gut, um einfache und wiederholbare Tests zu schreiben. Dies sollte am kompliziertesten sein, da Ihr Trigger erlaubt sein sollte:

CREATE TRIGGER [dbo].[SomeTrigger]
ON [dbo].[SomeView] INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
  DECLARE @SomeIDs AS SomeIDTableType

  --Perform the merge into the base table
  MERGE INTO dbo.SomeTable AS t
  USING inserted AS i
  ON t.SomeID = i.SomeID
  WHEN MATCHED THEN UPDATE SET
    t.SomeStuff = i.SomeStuff,
    t.OtherStuff = i.OtherStuff
  WHEN NOT MATCHED THEN INSERT (
    SomeStuff,
    OtherStuff
  ) VALUES (
    i.SomeStuff,
    i.OtherStuff
  )
  OUTPUT inserted.SomeID 
  INTO @SomeIDs(SomeID);

  DELETE FROM dbo.SomeTable
  OUTPUT deleted.SomeID 
  INTO @SomeIDs(SomeID)
  WHERE EXISTS (
    SELECT NULL
    FROM deleted AS d
    WHERE d.SomeID = SomeTable.SomeID
  ) AND NOT EXISTS (
    SELECT NULL
    FROM inserted AS i
    WHERE i.SomeID = SomeTable.SomeID
  );

  EXEC dbo.uspUpdateSomeStuff @SomeIDs;
END;

Der erste Teil des Triggers besteht im Wesentlichen darin, die eigentlichen Änderungen an der Basistabelle durchzuführen, da es sich um einen INSTEAD OF-Trigger handelt, also müssen wir alle Änderungen durchführen, die je nach den Tabellen, die wir verwalten müssen, unterschiedlich sein werden. Es ist hervorzuheben, dass Änderungen hauptsächlich wörtlich vorgenommen werden sollten. Wir berechnen oder transformieren keine der Daten neu. Wir sparen uns all diese zusätzliche Arbeit am Ende, wo alles, was wir innerhalb des Triggers tun, darin besteht, eine Liste von Datensätzen aufzufüllen, die durch den Trigger geändert wurden, und eine gespeicherte Prozedur mithilfe eines Tabellenwertparameters bereitzustellen. Beachten Sie, dass wir nicht einmal berücksichtigen, welche Datensätze geändert wurden oder wie sie geändert wurden. All das kann innerhalb der gespeicherten Prozedur erledigt werden.

Regel Nr. 6:Der Trigger sollte nach Möglichkeit idempotent sein.

Generell gilt:Die Trigger MÜSSEN idempotent sein. Dies gilt unabhängig davon, ob es sich um einen tabellenbasierten oder einen ansichtsbasierten Trigger handelt. Dies gilt insbesondere für diejenigen, die die Daten in den Basistabellen ändern müssen, von denen aus der Trigger überwacht wird. Wieso den? Denn wenn Menschen die Daten ändern, die vom Trigger erfasst werden, erkennen sie möglicherweise, dass sie einen Fehler gemacht haben, bearbeiten sie erneut oder bearbeiten einfach denselben Datensatz und speichern ihn dreimal. Sie werden nicht glücklich sein, wenn sie feststellen, dass sich die Berichte jedes Mal ändern, wenn sie eine Änderung vornehmen, die die Ausgabe für den Bericht nicht ändern soll.

Genauer gesagt, es könnte verlockend sein, den Trigger zu versuchen und zu optimieren, indem Sie etwas Ähnliches tun:

WITH SourceData AS (
  SELECT OrderID, SUM(SalesAmount) AS NewSaleTotal
  FROM inserted
  GROUP BY OrderID
)
MERGE INTO dbo.SalesOrder AS o
USING SourceData AS d
ON o.OrderID = d.OrderID
WHEN MATCHED THEN UPDATE SET
  o.OrderTotal = o.OrderTotal + d.NewSaleTotal;

Wir können die Neuberechnung der neuen Summe vermeiden, indem wir einfach die geänderten Zeilen in der eingefügten Tabelle überprüfen, richtig? Aber was passiert, wenn der Benutzer den Datensatz bearbeitet, um einen Tippfehler im Kundennamen zu korrigieren? Am Ende haben wir eine falsche Summe und der Auslöser arbeitet jetzt gegen uns.

Inzwischen sollten Sie sehen, warum die Regel Nr. 4 uns hilft, indem sie nur die Primärschlüssel an die gespeicherte Prozedur ausgibt, anstatt zu versuchen, Daten an die gespeicherte Prozedur zu übergeben oder dies direkt im Trigger zu tun, wie es das Beispiel getan hätte .

Stattdessen möchten wir einen ähnlichen Code in einer gespeicherten Prozedur haben:

CREATE PROCEDURE dbo.uspUpdateSalesTotal (
  @SalesOrders SalesOrderTableType READONLY
) AS
BEGIN
  WITH SourceData AS (
    SELECT s.OrderID, SUM(s.SalesAmount) AS NewSaleTotal
    FROM dbo.SalesOrder AS s
    WHERE EXISTS (
      SELECT NULL
      FROM @SalesOrders AS x
      WHERE x.SalesOrderID = s.SalesOrderID
    )
    GROUP BY OrderID
  )
  MERGE INTO dbo.SalesOrder AS o
  USING SourceData AS d
  ON o.OrderID = d.OrderID
  WHEN MATCHED THEN UPDATE SET
    o.OrderTotal = d.NewSaleTotal;
END;

Mit @SalesOrders können wir immer noch selektiv nur die Zeilen aktualisieren, die vom Trigger betroffen waren, und wir können auch die neue Gesamtsumme neu berechnen und sie zur neuen Gesamtsumme machen. Selbst wenn der Benutzer einen Tippfehler beim Kundennamen gemacht und ihn bearbeitet hat, führt jede Speicherung zu demselben Ergebnis für diese Zeile.

Noch wichtiger ist, dass uns dieser Ansatz auch eine einfache Möglichkeit bietet, die Gesamtsummen zu korrigieren. Angenommen, wir müssen einen Massenimport durchführen und der Import enthält nicht die Gesamtsumme, also müssen wir sie selbst berechnen. Wir können die gespeicherte Prozedur so schreiben, dass sie direkt in die Tabelle schreibt. Wir können dann die obige gespeicherte Prozedur aufrufen und die IDs aus dem Import übergeben, und alles ist gut. Daher ist die von uns verwendete Logik nicht an den Trigger hinter der Ansicht gebunden. Das hilft, wenn die Logik für den von uns durchgeführten Massenimport nicht erforderlich ist.

Wenn Sie Probleme haben, Ihren Trigger idempotent zu machen, ist dies ein starker Hinweis darauf, dass Sie möglicherweise stattdessen eine gespeicherte Prozedur verwenden und sie direkt aus Ihrer Anwendung aufrufen müssen, anstatt sich auf Trigger zu verlassen. Eine bemerkenswerte Ausnahme von dieser Regel ist, wenn der Trigger in erster Linie ein Auditing-Trigger sein soll. In diesem Fall möchten Sie für jede Bearbeitung eine neue Zeile in die Audit-Tabelle schreiben, einschließlich aller Tippfehler, die der Benutzer macht. Dies ist in Ordnung, da in diesem Fall keine Änderungen an den Daten vorgenommen werden, mit denen der Benutzer interagiert. Aus der Sicht des Benutzers ist es immer noch das gleiche Ergebnis. Aber wann immer der Trigger dieselben Daten manipulieren muss, mit denen der Benutzer arbeitet, ist es viel besser, wenn er idempotent ist.

Abschluss

Hoffentlich können Sie jetzt sehen, wie viel schwieriger es sein kann, einen gut erzogenen Trigger zu entwerfen. Aus diesem Grund sollten Sie sorgfältig überlegen, ob Sie es ganz vermeiden und direkte Aufrufe mit gespeicherter Prozedur verwenden können. Aber wenn Sie zu dem Schluss gekommen sind, dass Sie Auslöser haben müssen, um die über Ansichten vorgenommenen Änderungen zu verwalten, hoffe ich, dass die Regeln Ihnen helfen werden. Mit einigen Anpassungen ist es einfach genug, den Triggersatz satzbasiert zu machen. Um es idempotent zu machen, müssen Sie sich normalerweise mehr Gedanken darüber machen, wie Sie Ihre gespeicherten Prozeduren implementieren werden.

Wenn Sie weitere Vorschläge oder Regeln zum Teilen haben, feuern Sie in den Kommentaren los!