PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Grundlagen zu PostgreSQL-Triggern und gespeicherten Funktionen

Anmerkung von Multiplenines:Dieser Blog wird posthum veröffentlicht, da Berend Tober am 16. Juli 2018 verstorben ist. Wir ehren seine Beiträge zur PostgreSQL-Community und wünschen unserem Freund und Gastautor Frieden.

In einem früheren Artikel haben wir den seriellen PostgreSQL-Pseudotyp besprochen, der nützlich ist, um synthetische Schlüsselwerte mit inkrementierenden Ganzzahlen zu füllen. Wir haben gesehen, dass die Verwendung des Schlüsselworts serial data type in einer DDL-Anweisung (Table Data Definition Language) als eine Spaltendeklaration vom Typ Integer implementiert wird, die bei einer Datenbankeinfügung mit einem Standardwert gefüllt wird, der von einem einfachen Funktionsaufruf abgeleitet wird. Dieses automatisierte Verhalten des Aufrufens von funktionalem Code als Teil der integralen Antwort auf die Aktivität der Datenmanipulationssprache (DML) ist eine leistungsstarke Funktion hochentwickelter Managementsysteme für relationale Datenbanken (RDBMS) wie PostgreSQL. In diesem Artikel vertiefen wir uns weiter in einen weiteren leistungsfähigeren Aspekt zum automatischen Aufrufen von benutzerdefiniertem Code, nämlich die Verwendung von Triggern und gespeicherten Funktionen.Einführung

Anwendungsfälle für Trigger und gespeicherte Funktionen

Lassen Sie uns darüber sprechen, warum Sie vielleicht in das Verständnis von Triggern und gespeicherten Funktionen investieren sollten. Indem Sie DML-Code in die Datenbank selbst einbauen, können Sie die doppelte Implementierung von datenbezogenem Code in mehreren separaten Anwendungen vermeiden, die möglicherweise als Schnittstelle mit der Datenbank erstellt werden. Dadurch wird die konsistente Ausführung von DML-Code für die Datenvalidierung, Datenbereinigung oder andere Funktionen wie Datenprüfung (d. h. Protokollierung von Änderungen) oder Pflege einer Übersichtstabelle unabhängig von jeder aufrufenden Anwendung sichergestellt. Eine weitere übliche Verwendung von Triggern und gespeicherten Funktionen besteht darin, Ansichten beschreibbar zu machen, d. h. Einfügungen und/oder Aktualisierungen in komplexen Ansichten zu ermöglichen oder bestimmte Spaltendaten vor unbefugter Änderung zu schützen. Außerdem durchlaufen Daten, die eher auf dem Server als im Anwendungscode verarbeitet werden, das Netzwerk nicht, sodass das Risiko geringer ist, dass Daten abgehört werden, und die Netzwerküberlastung verringert wird. Außerdem können in PostgreSQL gespeicherte Funktionen so konfiguriert werden, dass Code mit einer höheren Berechtigungsstufe als der Sitzungsbenutzer ausgeführt wird, was einige leistungsstarke Fähigkeiten zulässt. Wir werden später einige Beispiele machen.

Der Fall gegen Trigger und gespeicherte Funktionen

Eine Überprüfung der Kommentare auf der PostgreSQL General-Mailingliste ergab einige Meinungen, die der Verwendung von Triggern und gespeicherten Funktionen abträglich sind, die ich hier der Vollständigkeit halber erwähne und um Sie und Ihr Team zu ermutigen, die Vor- und Nachteile für Ihre Implementierung abzuwägen.

Zu den Einwänden gehörte beispielsweise die Wahrnehmung, dass gespeicherte Funktionen nicht einfach zu warten seien und daher eine erfahrene Person mit ausgeprägten Fähigkeiten und Kenntnissen in der Datenbankverwaltung erforderlich seien, um sie zu verwalten. Einige Softwareexperten haben berichtet, dass unternehmensinterne Änderungskontrollen bei Datenbanksystemen in der Regel strenger sind als bei Anwendungscode, so dass, wenn Geschäftsregeln oder andere Logik in der Datenbank implementiert sind, Änderungen bei sich entwickelnden Anforderungen unerschwinglich umständlich sind. Eine andere Sichtweise betrachtet Trigger als unerwarteten Nebeneffekt einer anderen Aktion und kann als solche undurchsichtig, leicht zu übersehen, schwer zu debuggen und frustrierend zu warten sein und sollte daher normalerweise die letzte Wahl sein, nicht die erste.

Diese Einwände mögen einen gewissen Wert haben, aber wenn Sie darüber nachdenken, sind Daten ein wertvolles Gut, und daher möchten Sie wahrscheinlich sowieso eine qualifizierte und erfahrene Person oder ein Team, das für das RDBMS in einem Unternehmen oder einer Regierungsorganisation verantwortlich ist, und ähnlich ändern Kontrolltafeln sind eine bewährte Komponente nachhaltiger Wartung für ein Informationssystem der Aufzeichnungen, und die Nebenwirkung einer Person ist genauso gut die mächtige Annehmlichkeit einer anderen, was der Standpunkt ist, der für den Rest dieses Artikels angenommen wird.

Auslöser deklarieren

Kommen wir zum Erlernen der Schrauben und Muttern. In der allgemeinen DDL-Syntax stehen viele Optionen zum Deklarieren eines Triggers zur Verfügung, und es würde viel Zeit in Anspruch nehmen, alle möglichen Permutationen zu behandeln, daher werden wir der Kürze halber in Beispielen nur über eine minimal erforderliche Teilmenge davon sprechen Folgen Sie dieser verkürzten Syntax:

CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
    ON table_name
    FOR EACH ROW EXECUTE PROCEDURE function_name()

where event can be one of:

    INSERT
    UPDATE [ OF column_name [, ... ] ]
    DELETE
    TRUNCATE

Die erforderlichen konfigurierbaren Elemente neben einem Namen sind das wann , das warum , das wo , und das was , d. h. das Timing für den aufzurufenden Triggercode relativ zur auslösenden Aktion (wann), der spezifische Typ der auslösenden DML-Anweisung (warum), die Tabelle oder Tabellen, auf die eingewirkt wird (wo) und der gespeicherte auszuführende Funktionscode (was).

Eine Funktion deklarieren

Die obige Triggerdeklaration erfordert die Angabe eines Funktionsnamens, daher kann die Triggerdeklarations-DDL technisch erst ausgeführt werden, nachdem die Triggerfunktion zuvor definiert wurde. Die allgemeine DDL-Syntax für eine Funktionsdeklaration hat auch viele Optionen, daher verwenden wir zur Verwaltbarkeit diese minimal ausreichende Syntax für unsere Zwecke hier:

CREATE [ OR REPLACE ] FUNCTION
    name () RETURNS TRIGGER
  { LANGUAGE lang_name
    | SECURITY DEFINER
    | SET configuration_parameter { TO value | = value | FROM CURRENT }
    | AS 'definition'
  }...

Eine Triggerfunktion akzeptiert keine Parameter und der Rückgabetyp muss TRIGGER sein. Wir werden über die optionalen Modifikatoren sprechen, wenn wir ihnen in den folgenden Beispielen begegnen.

Ein Namensschema für Trigger und Funktionen

Dem angesehenen Informatiker Phil Karlton wird zugeschrieben (hier in paraphrasierter Form), dass das Benennen von Dingen eine der größten Herausforderungen für Softwareteams ist. Ich werde hier eine einfach zu verwendende Benennungskonvention für Trigger und gespeicherte Funktionen vorstellen, die mir gute Dienste geleistet hat, und Sie ermutigen, sie für Ihre eigenen RDBMS-Projekte zu übernehmen. Das Benennungsschema in den Beispielen für diesen Artikel folgt einem Muster, bei dem der zugeordnete Tabellenname mit einem angehängten Kürzel verwendet wird, das den deklarierten Trigger when angibt und warum Attribute:Der erste Suffixbuchstabe ist entweder ein „b“, „a“ oder „i“ (für „before“, „after“ oder „statt“), der nächste Buchstabe ist ein oder mehrere „i“ , „u“, „d“ oder „t“ (für „insert“, „update“, „delete“ oder „truncate“), und der letzte Buchstabe ist nur ein „t“ für Trigger. (Ich verwende eine ähnliche Namenskonvention für Regeln, und in diesem Fall ist der letzte Buchstabe „r“). Die verschiedenen minimalen Attributkombinationen für Trigger-Deklarationen für eine Tabelle mit dem Namen „my_table“ wären also beispielsweise:

|-------------+-------------+-----------+---------------+-----------------|
|  TABLE NAME |  WHEN       |  WHY      |  TRIGGER NAME |  FUNCTION NAME  |
|-------------+-------------+-----------+---------------+-----------------|
|  my_table   |  BEFORE     |  INSERT   |  my_table_bit |  my_table_bit   |
|  my_table   |  BEFORE     |  UPDATE   |  my_table_but |  my_table_but   |
|  my_table   |  BEFORE     |  DELETE   |  my_table_bdt |  my_table_bdt   |
|  my_table   |  BEFORE     |  TRUNCATE |  my_table_btt |  my_table_btt   |
|  my_table   |  AFTER      |  INSERT   |  my_table_ait |  my_table_ait   |
|  my_table   |  AFTER      |  UPDATE   |  my_table_aut |  my_table_aut   |
|  my_table   |  AFTER      |  DELETE   |  my_table_adt |  my_table_adt   |
|  my_table   |  AFTER      |  TRUNCATE |  my_table_att |  my_table_att   |
|  my_table   |  INSTEAD OF |  INSERT   |  my_table_iit |  my_table_iit   |
|  my_table   |  INSTEAD OF |  UPDATE   |  my_table_iut |  my_table_iut   |
|  my_table   |  INSTEAD OF |  DELETE   |  my_table_idt |  my_table_idt   |
|  my_table   |  INSTEAD OF |  TRUNCATE |  my_table_itt |  my_table_itt   |
|-------------+-------------+-----------+---------------+-----------------|

Der exakt gleiche Name kann sowohl für den Trigger als auch für die zugehörige gespeicherte Funktion verwendet werden, was in PostgreSQL durchaus zulässig ist, da das RDBMS Trigger und gespeicherte Funktionen getrennt nach den jeweiligen Zwecken und dem Kontext, in dem der Elementname verwendet wird, verfolgt klar machen, auf welches Element sich der Name bezieht.

So würde beispielsweise eine Triggerdeklaration, die dem Szenario der ersten Zeile aus der obigen Tabelle entspricht, als implementiert angesehen werden

CREATE TRIGGER my_table_bit 
    BEFORE INSERT
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_bit();

Für den Fall, dass ein Trigger mit mehreren warum deklariert wird Attribute erweitern Sie einfach das Suffix entsprechend, z. B. für ein insert oder update auslösen, würde das obige zu

CREATE TRIGGER my_table_biut 
    BEFORE INSERT OR UPDATE
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_biut();

Zeig mir schon etwas Code!

Machen wir es wahr. Wir beginnen mit einem einfachen Beispiel und erweitern es dann, um weitere Funktionen zu veranschaulichen. Die Trigger-DDL-Anweisungen erfordern, wie erwähnt, eine bereits vorhandene Funktion und auch eine Tabelle, auf die reagiert werden kann, also brauchen wir zuerst eine Tabelle, an der wir arbeiten können. Nehmen wir zum Beispiel an, wir müssen grundlegende Kontoidentitätsdaten speichern

CREATE TABLE person (
    login_name varchar(9) not null primary key,
    display_name text
);

Einige Datenintegritätserzwingungen können einfach mit der richtigen Spalten-DDL gehandhabt werden, wie in diesem Fall die Anforderung, dass der Anmeldename vorhanden und nicht länger als neun Zeichen sein darf. Versuche, einen NULL-Wert oder einen zu langen Wert von login_name einzufügen, schlagen fehl und melden aussagekräftige Fehlermeldungen:

INSERT INTO person VALUES (NULL, 'Felonious Erroneous');
ERROR:  null value in column "login_name" violates not-null constraint
DETAIL:  Failing row contains (null, Felonious Erroneous).

INSERT INTO person VALUES ('atoolongusername', 'Felonious Erroneous');
ERROR:  value too long for type character varying(9)

Andere Erzwingungen können mit Prüfbeschränkungen gehandhabt werden, z. B. das Erfordernis einer Mindestlänge und das Zurückweisen bestimmter Zeichen:

ALTER TABLE person 
    ADD CONSTRAINT PERSON_LOGIN_NAME_NON_NULL 
    CHECK (LENGTH(login_name) > 0);

ALTER TABLE person 
    ADD CONSTRAINT person_login_name_no_space 
    CHECK (POSITION(' ' IN login_name) = 0);

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  new row for relation "person" violates check constraint "person_login_name_non_null"
DETAIL:  Failing row contains (, Felonious Erroneous).

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  new row for relation "person" violates check constraint "person_login_name_no_space"
DETAIL:  Failing row contains (space man, Major Tom).

Beachten Sie jedoch, dass die Fehlermeldung nicht so umfassend informativ ist wie zuvor und nur so viel übermittelt, wie im Triggernamen codiert ist, anstatt eine sinnvolle erklärende Textnachricht. Indem Sie die Prüflogik stattdessen in einer gespeicherten Funktion implementieren, können Sie eine Ausnahme verwenden, um eine hilfreichere Textnachricht auszugeben. Außerdem dürfen Check-Constraint-Ausdrücke keine Unterabfragen enthalten oder auf andere Variablen als Spalten der aktuellen Zeile oder andere Datenbanktabellen verweisen.

Lassen Sie uns also die Check Constraints fallen

ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;

und weiter mit Triggern und gespeicherten Funktionen.

Zeig mir mehr Code

Wir haben einen Tisch. Weiter zur Funktions-DDL definieren wir eine Funktion mit leerem Körper, die wir später mit spezifischem Code füllen können:

CREATE OR REPLACE FUNCTION person_bit() 
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    SET search_path = public
    AS '
    BEGIN
    END;
    ';

Dies ermöglicht es uns, endlich zur Trigger-DDL zu gelangen, die die Tabelle und die Funktion verbindet, damit wir einige Beispiele ausführen können:

CREATE TRIGGER person_bit 
    BEFORE INSERT ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

PostgreSQL ermöglicht es, dass gespeicherte Funktionen in einer Vielzahl unterschiedlicher Sprachen geschrieben werden. In diesem Fall und den folgenden Beispielen erstellen wir Funktionen in der PL/pgSQL-Sprache, die speziell für PostgreSQL entwickelt wurde und die Verwendung aller Datentypen, Operatoren und Funktionen des PostgreSQL-RDBMS unterstützt. Die Option SET SCHEMA legt den Schemasuchpfad fest, der für die Dauer der Funktionsausführung verwendet wird. Das Festlegen des Suchpfads für jede Funktion ist eine gute Vorgehensweise, da es erspart, Datenbankobjekten einen Schemanamen voranzustellen, und vor bestimmten Schwachstellen im Zusammenhang mit dem Suchpfad schützt.

BEISPIEL 0 – Datenvalidierung

Lassen Sie uns als erstes Beispiel die früheren Überprüfungen implementieren, aber mit menschenfreundlicheren Nachrichten.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;
    RETURN NEW;
    END;
    $$;

Der Qualifizierer „NEW“ ist ein Verweis auf die einzufügende Datenzeile. Es ist eine von mehreren speziellen Variablen, die innerhalb einer Triggerfunktion verfügbar sind. Wir werden einige andere unten vorstellen. Beachten Sie auch, dass PostgreSQL das Ersetzen der einfachen Anführungszeichen, die den Funktionskörper begrenzen, durch andere Trennzeichen zulässt, in diesem Fall gemäß einer allgemeinen Konvention, doppelte Dollarzeichen als Trennzeichen zu verwenden, da der Funktionskörper selbst einfache Anführungszeichen enthält. Triggerfunktionen müssen beendet werden, indem entweder die einzufügende NEW-Zeile oder NULL zurückgegeben wird, um die Aktion stillschweigend abzubrechen.

Dieselben Einfügeversuche schlagen wie erwartet fehl, aber jetzt mit freundlichem Messaging:

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  Login name must not be empty.

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  Login name must not include white space.

BEISPIEL 1 – Auditprotokollierung

Bei gespeicherten Funktionen haben wir einen großen Spielraum, was der aufgerufene Code tut, einschließlich des Verweisens auf andere Tabellen (was mit Check Constraints nicht möglich ist). Als komplexeres Beispiel gehen wir die Implementierung einer Audit-Tabelle durch, d. h. das Verwalten eines Datensatzes in einer separaten Tabelle über Einfügungen, Aktualisierungen und Löschungen in einer Haupttabelle. Die Audit-Tabelle enthält typischerweise die gleichen Attribute wie die Haupttabelle, die zum Aufzeichnen der geänderten Werte verwendet werden, sowie zusätzliche Attribute zum Aufzeichnen der Operation, die zum Vornehmen der Änderung ausgeführt wurde, sowie einen Transaktionszeitstempel und eine Aufzeichnung des Benutzers, der die Änderung vornimmt ändern:

CREATE TABLE person_audit (
    login_name varchar(9) not null,
    display_name text,
    operation varchar,
    effective_at timestamp not null default now(),
    userid name not null default session_user
);

In diesem Fall ist die Implementierung der Überwachung sehr einfach. Wir ändern einfach die vorhandene Triggerfunktion so, dass sie DML enthält, um die Einfügung der Überwachungstabelle zu bewirken, und definieren dann den Trigger neu, um sowohl bei Aktualisierungen als auch bei Einfügungen ausgelöst zu werden. Beachten Sie, dass wir uns dafür entschieden haben, das Suffix des Auslöserfunktionsnamens nicht in „biut“ zu ändern, aber wenn die Audit-Funktionalität eine bekannte Anforderung zur ursprünglichen Entwurfszeit gewesen wäre, wäre dies der verwendete Name:

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- New code to record audits

    INSERT INTO person_audit (login_name, display_name, operation) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP);

    RETURN NEW;
    END;
    $$;


DROP TRIGGER person_bit ON person;

CREATE TRIGGER person_biut 
    BEFORE INSERT OR UPDATE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

Beachten Sie, dass wir eine weitere spezielle Variable „TG_OP“ eingeführt haben, die das System festlegt, um die DML-Operation zu identifizieren, die den Trigger als „INSERT“, „UPDATE“, „DELETE“ oder „TRUNCATE“ ausgelöst hat.

Wir müssen Löschungen getrennt von Einfügungen und Aktualisierungen handhaben, da die Attributvalidierungstests überflüssig sind und weil der Sonderwert NEW beim Eintritt in ein before delete nicht definiert wird Trigger-Funktion und definieren Sie so die entsprechende gespeicherte Funktion und den Trigger:

CREATE OR REPLACE FUNCTION person_bdt()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN

    -- Record deletion in audit table

    INSERT INTO person_audit (login_name, display_name, operation) 
      VALUES (OLD.login_name, OLD.display_name, TG_OP);

    RETURN OLD;
    END;
    $$;
        
CREATE TRIGGER person_bdt 
    BEFORE DELETE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bdt();

Beachten Sie die Verwendung des Sonderwerts OLD als Referenz auf die zu löschende Zeile, d. h. die Zeile, wie sie vorher existierte die Löschung erfolgt.

Wir machen ein paar Einfügungen, um die Funktionalität zu testen und zu bestätigen, dass die Audit-Tabelle eine Aufzeichnung der Einfügungen enthält:

INSERT INTO person VALUES ('dfunny', 'Doug Funny');
INSERT INTO person VALUES ('pmayo', 'Patti Mayonnaise');

SELECT * FROM person;
 login_name |   display_name   
------------+------------------
 dfunny     | Doug Funny
 pmayo      | Patti Mayonnaise
(2 rows)

SELECT * FROM person_audit;
 login_name |   display_name   | operation |        effective_at        |  userid  
------------+------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny       | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise | INSERT    | 2018-05-26 18:48:07.698623 | postgres
(2 rows)

Dann aktualisieren wir eine Zeile und bestätigen, dass die Audit-Tabelle einen Datensatz der Änderung enthält, indem wir einem der Anzeigenamen des Datensatzes einen zweiten Vornamen hinzufügen:

UPDATE person SET display_name = 'Doug Yancey Funny' WHERE login_name = 'dfunny';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 pmayo      | Patti Mayonnaise
 dfunny     | Doug Yancey Funny
(2 rows)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-26 18:48:07.698623 | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-26 18:48:07.707284 | postgres
(3 rows)

Und schließlich üben wir die Löschfunktion aus und bestätigen, dass die Audit-Tabelle auch diesen Datensatz enthält:

DELETE FROM person WHERE login_name = 'pmayo';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 dfunny     | Doug Yancey Funny
(1 row)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-27 08:13:22.747226 | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-27 08:13:22.74839  | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-27 08:13:22.749495 | postgres
 pmayo      | Patti Mayonnaise  | DELETE    | 2018-05-27 08:13:22.753425 | postgres
(4 rows)

BEISPIEL 2 – Abgeleitete Werte

Gehen wir noch einen Schritt weiter und stellen wir uns vor, wir möchten in jeder Zeile ein beliebiges Textdokument in freier Form speichern, z. B. einen im Nur-Text-Format formatierten Lebenslauf oder einen Konferenzbeitrag oder eine Zusammenfassung einer Unterhaltungsfigur, und wir möchten die Verwendung der leistungsstarken Volltextsuche unterstützen Möglichkeiten von PostgreSQL für diese Freiform-Textdokumente.

Zuerst fügen wir der Haupttabelle zwei Attribute hinzu, um die Speicherung des Dokuments und eines zugehörigen Textsuchvektors zu unterstützen. Da der Textsuchvektor zeilenweise abgeleitet wird, macht es keinen Sinn, ihn in der Audit-Tabelle zu speichern, sei es, dass wir die Dokumentspeicherspalte zur zugehörigen Audit-Tabelle hinzufügen:

ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Dann modifizieren wir die Triggerfunktion, um diese neuen Attribute zu verarbeiten. Die Klartextspalte wird genauso behandelt wie andere vom Benutzer eingegebene Daten, aber der Textsuchvektor ist ein abgeleiteter Wert und wird daher von einem Funktionsaufruf behandelt, der den Dokumenttext für eine effiziente Suche auf einen tsvector-Datentyp reduziert.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- Modified audit code to include text abstract

    INSERT INTO person_audit (login_name, display_name, operation, abstract) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP, NEW.abstract);

    -- New code to reduce text to text-search vector

    SELECT to_tsvector(NEW.abstract) INTO NEW.ts_abstract;

    RETURN NEW;
    END;
    $$;

Als Test aktualisieren wir eine vorhandene Zeile mit etwas Detailtext aus Wikipedia:

UPDATE person SET abstract = 'Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd.' WHERE login_name = 'dfunny';

und bestätigen Sie dann, dass die Verarbeitung des Textsuchvektors erfolgreich war:

SELECT login_name, ts_abstract  FROM person;
 login_name |                                                                                                                ts_abstract                                                                                                                
------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dfunny     | '11':11 '12':13 'an':5 'and':9 'as':4 'boy':16 'crowd':24 'depicted':3 'doug':1 'fit':20 'gullible':10 'in':21 'insecure':8 'introverted':6 'is':2 'later':12 'old':15 'quiet':7 'the':23 'to':19 'wants':18 'who':17 'with':22 'year':14
(1 row)

BEISPIEL 3 – Auslöser und Aufrufe

Der abgeleitete Textsuchvektor aus dem obigen Beispiel ist nicht für den menschlichen Gebrauch bestimmt, d. h. er wird nicht vom Benutzer eingegeben, und wir erwarten niemals, den Wert einem Endbenutzer zu präsentieren. Wenn ein Benutzer versucht, einen Wert für die Spalte ts_abstract einzufügen, wird alles, was bereitgestellt wird, verworfen und durch den Wert ersetzt, der intern von der Trigger-Funktion abgeleitet wird, sodass wir Schutz vor einer Vergiftung des Suchkörpers haben. Um die Spalte vollständig auszublenden, können wir eine gekürzte Ansicht definieren, die dieses Attribut nicht enthält, aber wir erhalten dennoch den Vorteil der Triggeraktivität für die zugrunde liegende Tabelle:

CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;

Für eine einfache Ansicht macht PostgreSQL sie automatisch beschreibbar, sodass wir nichts weiter tun müssen, um Daten erfolgreich einzufügen oder zu aktualisieren. Wenn die DML auf die zugrunde liegende Tabelle wirksam wird, werden die Trigger aktiviert, als ob die Anweisung direkt auf die Tabelle angewendet würde, sodass wir immer noch sowohl die Textsuchunterstützung im Hintergrund ausführen, die die Suchvektorspalte der Personentabelle füllt, als auch anhängen Informationen in der Audit-Tabelle ändern:

INSERT INTO abridged_person VALUES ('skeeter', 'Mosquito Valentine', 'Skeeter is Doug''s best friend. He is famous in both series for the honking sounds he frequently makes.');


SELECT login_name, ts_abstract FROM person WHERE login_name = 'skeeter';
 login_name |                                                                                   ts_abstract                                                                                    
------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 skeeter    | 'best':5 'both':11 'doug':3 'famous':9 'for':13 'frequently':18 'friend':6 'he':7,17 'honking':15 'in':10 'is':2,8 'makes':19 's':4 'series':12 'skeeter':1 'sounds':16 'the':14
(1 row)


SELECT login_name, display_name, operation, userid FROM person_audit ORDER BY effective_at;
 login_name |    display_name    | operation |  userid  
------------+--------------------+-----------+----------
 dfunny     | Doug Funny         | INSERT    | postgres
 pmayo      | Patti Mayonnaise   | INSERT    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 pmayo      | Patti Mayonnaise   | DELETE    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 skeeter    | Mosquito Valentine | INSERT    | postgres
(6 rows)

Für kompliziertere Ansichten, die die Anforderungen für die automatische Beschreibbarkeit nicht erfüllen, entweder das Regelsystem oder statt Trigger können die Aufgabe übernehmen, Schreib- und Löschvorgänge zu unterstützen.

BEISPIEL 4 – Zusammenfassungswerte

Lassen Sie uns das Szenario weiter verschönern und behandeln, in dem es eine Art Transaktionstabelle gibt. Es kann sich um eine Aufzeichnung der geleisteten Arbeitsstunden, Bestandszugänge und -reduzierungen von Lager- oder Einzelhandelsbeständen oder vielleicht um ein Scheckregister mit Belastungen und Gutschriften für jede Person handeln:

CREATE TABLE transaction (
    login_name character varying(9) NOT NULL,
    post_date date,
    description character varying,
    debit money,
    credit money,
    FOREIGN KEY (login_name) REFERENCES person (login_name)
);

Und nehmen wir an, dass es zwar wichtig ist, den Transaktionsverlauf aufzubewahren, die Geschäftsregeln jedoch die Verwendung des Nettosaldos bei der Antragsverarbeitung erfordern und nicht die Transaktionsdetails. Um zu vermeiden, dass der Kontostand häufig neu berechnet werden muss, indem jedes Mal, wenn der Kontostand benötigt wird, alle Transaktionen summiert werden, können wir einen aktuellen Kontostandswert direkt in der Personentabelle denormalisieren und beibehalten, indem wir eine neue Spalte anhängen und einen Trigger und eine gespeicherte Funktion zur Verwaltung verwenden der Nettosaldo, wenn Transaktionen eingefügt werden:

ALTER TABLE person ADD COLUMN balance MONEY DEFAULT 0;

CREATE FUNCTION transaction_bit() RETURNS trigger
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    DECLARE
    newbalance money;
    BEGIN

    -- Update person account balance

    UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name
                RETURNING balance INTO newbalance;

    -- Data validation

    IF COALESCE(NEW.debit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Debit value must be non-negative';
    END IF;

    IF COALESCE(NEW.credit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Credit value must be non-negative';
    END IF;

    IF newbalance < 0::money THEN
        RAISE EXCEPTION 'Insufficient funds: %', NEW;
    END IF;

    RETURN NEW;
    END;
    $$;



CREATE TRIGGER transaction_bit 
      BEFORE INSERT ON transaction 
      FOR EACH ROW EXECUTE PROCEDURE transaction_bit();

Es mag seltsam erscheinen, die Aktualisierung zuerst in der gespeicherten Funktion durchzuführen, bevor die Nicht-Negativität der Soll-, Haben- und Saldowerte validiert wird, aber in Bezug auf die Datenvalidierung spielt die Reihenfolge keine Rolle, da der Hauptteil einer Triggerfunktion als ausgeführt wird Datenbanktransaktion. Wenn also diese Validierungsprüfungen fehlschlagen, wird die gesamte Transaktion zurückgesetzt, wenn die Ausnahme ausgelöst wird. Der Vorteil, zuerst die Aktualisierung durchzuführen, besteht darin, dass die Aktualisierung die betroffene Zeile für die Dauer der Transaktion sperrt und somit jede andere Sitzung, die versucht, dieselbe Zeile zu aktualisieren, blockiert wird, bis die aktuelle Transaktion abgeschlossen ist. Der weitere Validierungstest stellt sicher, dass der resultierende Kontostand nicht negativ ist, und die Ausnahmeinformationsnachricht kann eine Variable enthalten, die in diesem Fall die anstößige Zeile der versuchten Einfügetransaktion zum Debuggen zurückgibt.

Um zu demonstrieren, dass es tatsächlich funktioniert, sind hier einige Beispieleinträge und eine Überprüfung, die das aktualisierte Guthaben bei jedem Schritt zeigt:

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name | balance 
------------+---------
 dfunny     |   $0.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-11', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$2780.52', NULL);
ERROR:  Insufficient funds: (dfunny,2018-01-17,"FOR:BGE PAYMENT ACH Withdrawal",,"$2,780.52")

Beachten Sie, wie die obige Transaktion bei unzureichendem Guthaben fehlschlägt, d. h. sie würde einen negativen Saldo erzeugen und erfolgreich rückgängig gemacht werden. Beachten Sie auch, dass wir die gesamte Zeile mit der Sondervariablen NEW als zusätzliches Detail in der Fehlermeldung zum Debuggen zurückgegeben haben.

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$278.52', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,721.48
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal', '$35.29', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

BEISPIEL 5 – Redux von Triggern und Ansichten

Es gibt jedoch ein Problem mit der obigen Implementierung, und zwar, dass nichts einen böswilligen Benutzer daran hindert, Geld zu drucken:

BEGIN;
UPDATE person SET balance = '1000000000.00';

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Wir haben den oben genannten Diebstahl vorerst rückgängig gemacht und werden einen Weg zeigen, wie Sie einen Schutz dagegen einbauen können, indem Sie einen Auslöser für eine Ansicht verwenden, um Aktualisierungen des Guthabenwerts zu verhindern.

Wir erweitern zuerst die verkürzte Ansicht von früher, um die Saldospalte freizulegen:

CREATE OR REPLACE VIEW abridged_person AS
  SELECT login_name, display_name, abstract, balance FROM person;

Dies ermöglicht offensichtlich einen Lesezugriff auf den Kontostand, löst das Problem aber immer noch nicht, da PostgreSQL die Ansicht für einfache Ansichten wie diese, die auf einer einzelnen Tabelle basieren, automatisch beschreibbar macht:

BEGIN;
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

We could use a rule, but to illustrate that triggers can be defined on views as well as tables, we will take the latter route and use an instead of update trigger on the view to block unwanted DML, preventing non-transactional changes to the balance value:

CREATE FUNCTION abridged_person_iut() RETURNS TRIGGER
    LANGUAGE plpgsql
    SET search_path TO public
    AS $$
    BEGIN

    -- Disallow non-transactional changes to balance

      NEW.balance = OLD.balance;
    RETURN NEW;
    END;
    $$;

CREATE TRIGGER abridged_person_iut
    INSTEAD OF UPDATE ON abridged_person
    FOR EACH ROW EXECUTE PROCEDURE abridged_person_iut();

The above instead of update trigger and stored procedure discards any attempted updates to the balance value and instead forces use of the value present in the database prior to the triggering update statement:

UPDATE abridged_person SET balance = '1000000000.00';

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

which affords protection against un-auditable changes to the balance value.

Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter

EXAMPLE 6 - Elevated Privileges

So far all the example code above has been executed at the database owner level by the postgres login role, so any of our anti-tampering efforts could be obviated… that’s just a fact of the database owner super-user privileges.

Our final example illustrates how triggers and stored functions can be used to allow the execution of code by a non-privileged user at a higher privilege than the logged in session user normally has by employing the SECURITY DEFINER attribute associated with stored functions.

First, we define a non-privileged login role, eve and confirm that upon instantiation there are no privileges:

CREATE USER eve;
\dp
                                  Access privileges
 Schema |      Name       | Type  | Access privileges | Column privileges | Policies 
--------+-----------------+-------+-------------------+-------------------+----------
 public | abridged_person | view  |                   |                   | 
 public | person          | table |                   |                   | 
 public | person_audit    | table |                   |                   | 
 public | transaction     | table |                   |                   | 
(4 rows)

We grant read, update, and create privileges on the abridged person view and read and create to the transaction table:

GRANT SELECT,INSERT, UPDATE ON abridged_person TO eve;
GRANT SELECT,INSERT ON transaction TO eve;
\dp
                                      Access privileges
 Schema |      Name       | Type  |     Access privileges     | Column privileges | Policies 
--------+-----------------+-------+---------------------------+-------------------+----------
 public | abridged_person | view  | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=arw/postgres          |                   | 
 public | person          | table |                           |                   | 
 public | person_audit    | table |                           |                   | 
 public | transaction     | table | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=ar/postgres           |                   | 
(4 rows)

By way of confirmation we see that eve is denied access to the person and person_audit tables:

SET SESSION AUTHORIZATION eve;

SELECT * FROM person;
ERROR:  permission denied for relation person

SELECT * from person_audit;
ERROR:  permission denied for relation person_audit

and that she does have appropriate read access to the abridged_person and transaction tables:

SELECT * FROM abridged_person;
 login_name |    display_name    |                                                            abstract                                                             |  balance  
------------+--------------------+---------------------------------------------------------------------------------------------------------------------------------+-----------
 skeeter    | Mosquito Valentine | Skeeter is Doug's best friend. He is famous in both series for the honking sounds he frequently makes.                          |     $0.00
 dfunny     | Doug Yancey Funny  | Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd. | $1,686.19
(2 rows)

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
(3 rows)

However, even though she has write privilege on the transaction table, a transaction insert attempt fails due to lack of privilege on the person table.

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR:  permission denied for relation person
CONTEXT:  SQL statement "UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement

The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:

RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
 dfunny     | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
(4 rows)

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $3,686.19
(1 row)

Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.

Schlussfolgerung

As lengthy as this article is, there’s still a lot more to say about triggers and stored functions. What we covered here is a basic introduction with a consideration of pros and cons of triggers and stored functions. We illustrated six use-case examples showing data validation, change logging, deriving values from inserted data, data hiding with simple updatable views, maintaining summary data in separate tables, and allowing safe invocation of code at elevated privilege. Look for a future article on using triggers and stored functions to prevent missing values in sequentially-incrementing (serial) columns.