Opmerking van Multiplenines:deze blog wordt postuum gepubliceerd toen Berend Tober op 16 juli 2018 overleed. We eren zijn bijdragen aan de PostgreSQL-gemeenschap en wensen vrede voor onze vriend en gastschrijver.
In een vorig artikel hebben we het seriële pseudo-type PostgreSQL besproken, dat handig is voor het vullen van synthetische sleutelwaarden met oplopende gehele getallen. We hebben gezien dat het gebruik van het seriële gegevenstype-sleutelwoord in een DDL-instructie (Table Data Definition Language) wordt geïmplementeerd als een kolomdeclaratie van het integer-type die bij een database-insertie wordt gevuld met een standaardwaarde die is afgeleid van een eenvoudige functieaanroep. Dit geautomatiseerde gedrag van het aanroepen van functionele code als onderdeel van de integrale reactie op datamanipulatietaal (DML)-activiteit is een krachtig kenmerk van geavanceerde relationele databasebeheersystemen (RDBMS) zoals PostgreSQL. In dit artikel gaan we dieper in op een ander, meer capabel aspect om automatisch aangepaste code op te roepen, namelijk het gebruik van triggers en opgeslagen functies.Inleiding
Gebruiksscenario's voor triggers en opgeslagen functies
Laten we het hebben over waarom u zou willen investeren in het begrijpen van triggers en opgeslagen functies. Door DML-code in de database zelf in te bouwen, kunt u dubbele implementatie van gegevensgerelateerde code voorkomen in meerdere afzonderlijke toepassingen die kunnen worden gebouwd om te communiceren met de database. Dit zorgt voor een consistente uitvoering van DML-code voor gegevensvalidatie, gegevensopschoning of andere functionaliteit zoals gegevenscontrole (d.w.z. het loggen van wijzigingen) of het bijhouden van een overzichtstabel, onafhankelijk van een aanroepende toepassing. Een ander veelgebruikt gebruik van triggers en opgeslagen functies is om views beschrijfbaar te maken, d.w.z. om invoegingen en/of updates van complexe views mogelijk te maken of om bepaalde kolomgegevens te beschermen tegen ongeoorloofde wijziging. Bovendien kruisen gegevens die op de server worden verwerkt in plaats van in toepassingscode het netwerk niet, dus er is een kleiner risico dat gegevens worden afgeluisterd en vermindert de netwerkcongestie. Ook kunnen in PostgreSQL opgeslagen functies worden geconfigureerd om code uit te voeren op een hoger privilegeniveau dan de sessiegebruiker, wat een aantal krachtige mogelijkheden toelaat. We zullen later wat voorbeelden geven.
De zaak tegen triggers en opgeslagen functies
Een recensie van commentaar op de PostgreSQL Algemene mailinglijst onthulde enkele ongunstige meningen over het gebruik van triggers en opgeslagen functies die ik hier voor de volledigheid noem en om u en uw team aan te moedigen de voor- en nadelen voor uw implementatie af te wegen.
Een van de bezwaren was bijvoorbeeld de perceptie dat opgeslagen functies niet gemakkelijk te onderhouden zijn, waardoor een ervaren persoon met geavanceerde vaardigheden en kennis op het gebied van databasebeheer nodig is om ze te beheren. Sommige softwareprofessionals hebben gemeld dat de controle op wijzigingen in databasesystemen doorgaans krachtiger is dan op toepassingscode, zodat als bedrijfsregels of andere logica in de database worden geïmplementeerd, het aanbrengen van wijzigingen naarmate de vereisten evolueren, onbetaalbaar is. Een ander gezichtspunt beschouwt triggers als een onverwacht neveneffect van een andere actie en kan als zodanig onduidelijk, gemakkelijk over het hoofd gezien, moeilijk te debuggen en frustrerend te onderhouden zijn en zou daarom meestal de laatste keuze moeten zijn, niet de eerste.
Deze bezwaren kunnen enige verdienste hebben, maar als u erover nadenkt, zijn gegevens een waardevol bezit en dus wilt u waarschijnlijk toch een bekwame en ervaren persoon of team die verantwoordelijk is voor het RDBMS in een bedrijfs- of overheidsorganisatie, en op dezelfde manier, Besturingskaarten zijn een bewezen onderdeel van duurzaam onderhoud van een geregistreerd informatiesysteem, en de bijwerking van de een is net zo goed het krachtige gemak van de ander, wat het standpunt is dat voor de rest van dit artikel is aangenomen.
Een trigger declareren
Laten we beginnen met het leren van de moeren en bouten. Er zijn veel opties beschikbaar in de algemene DDL-syntaxis voor het declareren van een trigger, en het zou veel tijd kosten om alle mogelijke permutaties te behandelen, dus kortheidshalve zullen we het hebben over slechts een minimaal vereiste subset ervan in voorbeelden die volg met behulp van deze verkorte syntaxis:
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
De vereiste configureerbare elementen naast een naam zijn de wanneer , het waarom , de waar , en de wat , d.w.z. de timing voor het aanroepen van de triggercode ten opzichte van de activerende actie (wanneer), het specifieke type triggerende DML-instructie (waarom), de tabel of tabellen waarop actie is ondernomen (waar) en de opgeslagen functiecode die moet worden uitgevoerd (wat).
Een functie declareren
De triggerdeclaratie hierboven vereist specificatie van een functienaam, dus technisch gezien kan de triggerdeclaratie DDL pas worden uitgevoerd nadat de triggerfunctie eerder is gedefinieerd. De algemene DDL-syntaxis voor een functiedeclaratie heeft ook veel opties, dus voor beheersbaarheid zullen we deze minimaal voldoende syntaxis voor onze doeleinden hier gebruiken:
CREATE [ OR REPLACE ] FUNCTION
name () RETURNS TRIGGER
{ LANGUAGE lang_name
| SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
}...
Een triggerfunctie heeft geen parameters nodig en het retourtype moet TRIGGER zijn. We zullen het hebben over de optionele modifiers zoals we ze tegenkomen in onderstaande voorbeelden.
Een naamgevingsschema voor triggers en functies
Aan de gerespecteerde computerwetenschapper Phil Karlton wordt toegeschreven dat hij (hier in geparafraseerde vorm) verklaart dat het benoemen van dingen een van de grootste uitdagingen is voor softwareteams. Ik ga hier een gebruiksvriendelijke conventie voor het benoemen van triggers en opgeslagen functies presenteren die mij goed van pas zijn gekomen en ik moedig u aan om te overwegen deze voor uw eigen RDBMS-projecten te gebruiken. Het naamgevingsschema in de voorbeelden voor dit artikel volgt een patroon van het gebruik van de bijbehorende tabelnaam met het achtervoegsel van een afkorting die de gedeclareerde trigger aangeeft wanneer en waarom attributen:de eerste letter van het achtervoegsel is een "b", "a" of "i" (voor "voor", "na" of "in plaats van"), de volgende is een of meer van een "i" , "u", "d" of "t" (voor "insert", "update", "delete" of "truncate"), en de laatste letter is slechts een "t" voor trigger. (Ik gebruik een vergelijkbare naamgevingsconventie voor regels, en in dat geval is de laatste letter "r"). Dus de verschillende combinaties van minimale triggerdeclaratieattributen voor een tabel met de naam "my_table" zijn bijvoorbeeld:
|-------------+-------------+-----------+---------------+-----------------|
| 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 |
|-------------+-------------+-----------+---------------+-----------------|
De exacte dezelfde naam kan worden gebruikt voor zowel de trigger als de bijbehorende opgeslagen functie, wat volledig is toegestaan in PostgreSQL omdat het RDBMS triggers en opgeslagen functies afzonderlijk bijhoudt voor de respectievelijke doeleinden, en de context waarin de itemnaam wordt gebruikt maakt het duidelijk naar welk item de naam verwijst.
Een triggerdeclaratie die overeenkomt met het scenario op de eerste rij uit de bovenstaande tabel zou dus geïmplementeerd worden als
CREATE TRIGGER my_table_bit
BEFORE INSERT
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_bit();
In het geval dat een trigger wordt gedeclareerd met meerdere waarom attributen, breid gewoon het achtervoegsel op de juiste manier uit, bijvoorbeeld voor een invoeging of update trigger, wordt het bovenstaande
CREATE TRIGGER my_table_biut
BEFORE INSERT OR UPDATE
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_biut();
Laat me al wat code zien!
Laten we het echt maken. We beginnen met een eenvoudig voorbeeld en breiden dat uit om verdere functies te illustreren. De trigger-DDL-instructies vereisen een reeds bestaande functie, zoals vermeld, en ook een tabel waarop moet worden gereageerd, dus eerst hebben we een tabel nodig om aan te werken. Laten we bijvoorbeeld zeggen dat we elementaire accountidentiteitsgegevens moeten opslaan
CREATE TABLE person (
login_name varchar(9) not null primary key,
display_name text
);
Sommige handhaving van de gegevensintegriteit kan eenvoudig worden afgehandeld met de juiste kolom-DDL, zoals in dit geval een vereiste dat de loginnaam bestaat en niet meer dan negen tekens lang is. Pogingen om een NULL-waarde of een te lange waarde van login_name in te voegen mislukken en rapporteren zinvolle foutmeldingen:
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 handhavingen kunnen worden afgehandeld met controlebeperkingen, zoals het vereisen van een minimumlengte en het afwijzen van bepaalde tekens:
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).
maar merk op dat de foutmelding niet zo volledig informatief is als voorheen, en alleen zoveel overbrengt als is gecodeerd in de triggernaam in plaats van een zinvolle verklarende tekstuele boodschap. Door in plaats daarvan de controlelogica in een opgeslagen functie te implementeren, kunt u een uitzondering gebruiken om een nuttiger tekstbericht te verzenden. Check constraint-expressies kunnen ook geen subquery's bevatten of verwijzen naar andere variabelen dan kolommen van de huidige rij of andere databasetabellen.
Dus laten we de controlebeperkingen laten vallen
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;
en ga aan de slag met triggers en opgeslagen functies.
Laat me wat meer code zien
We hebben een tafel. Als we verder gaan met de functie DDL, definiëren we een functie met lege inhoud, die we later kunnen invullen met specifieke code:
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
SET search_path = public
AS '
BEGIN
END;
';
Dit stelt ons in staat om eindelijk bij de trigger-DDL te komen die de tafel en de functie verbindt, zodat we enkele voorbeelden kunnen doen:
CREATE TRIGGER person_bit
BEFORE INSERT ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
Met PostgreSQL kunnen opgeslagen functies in verschillende talen worden geschreven. In dit geval en de volgende voorbeelden stellen we functies samen in de PL/pgSQL-taal die speciaal is ontworpen voor PostgreSQL en die het gebruik van alle gegevenstypen, operators en functies van het PostgreSQL RDBMS ondersteunt. De optie SET SCHEMA stelt het zoekpad voor het schema in dat wordt gebruikt voor de duur van de uitvoering van de functie. Het is een goede gewoonte om het zoekpad voor elke functie in te stellen, omdat het voorkomt dat databaseobjecten een schemanaam moeten krijgen, en het beschermt tegen bepaalde kwetsbaarheden met betrekking tot het zoekpad.
VOORBEELD 0 - Gegevensvalidatie
Laten we als eerste voorbeeld de eerdere controles implementeren, maar met meer mensvriendelijke berichten.
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;
$$;
De kwalificatie "NIEUW" is een verwijzing naar de rij gegevens die op het punt staan te worden ingevoegd. Het is een van een aantal speciale variabelen die beschikbaar zijn binnen een triggerfunctie. We zullen hieronder enkele anderen introduceren. Merk ook op dat PostgreSQL de vervanging toestaat van de enkele aanhalingstekens die de hoofdtekst van de functie afbakenen met andere scheidingstekens, in dit geval volgens een algemene afspraak om dubbele dollartekens als scheidingsteken te gebruiken, aangezien de hoofdtekst van de functie zelf enkele aanhalingstekens bevat. Triggerfuncties moeten worden afgesloten door de in te voegen NIEUWE rij te retourneren of NULL om de actie stil af te breken.
Dezelfde invoegpogingen mislukken zoals verwacht, maar nu met vriendelijke berichten:
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.
VOORBEELD 1 - Auditregistratie
Met opgeslagen functies hebben we veel vrijheid over wat de aangeroepen code doet, inclusief het verwijzen naar andere tabellen (wat niet mogelijk is met controlebeperkingen). Als een complexer voorbeeld zullen we de implementatie van een controletabel doornemen, dat wil zeggen, het bijhouden van een record, in een aparte tabel, van invoegingen, updates en verwijderingen naar een hoofdtabel. De controletabel bevat doorgaans dezelfde attributen als de hoofdtabel, die worden gebruikt om de gewijzigde waarden vast te leggen, plus aanvullende attributen om de bewerking vast te leggen die is uitgevoerd om de wijziging aan te brengen, evenals een transactietijdstempel en een record van de gebruiker die de wijziging maakt. wijzigen:
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 dit geval is het implementeren van auditing heel eenvoudig. We passen gewoon de bestaande triggerfunctie aan om DML op te nemen om de audittabel in te voegen, en herdefiniëren vervolgens de trigger om zowel updates als inserts te activeren. Houd er rekening mee dat we ervoor hebben gekozen om het achtervoegsel van de triggerfunctienaam niet te wijzigen in "biut", maar als de auditfunctionaliteit een bekende vereiste was geweest bij de eerste ontwerptijd, zou dat de gebruikte naam zijn:
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();
Merk op dat we een andere speciale variabele "TG_OP" hebben geïntroduceerd die het systeem instelt om de DML-bewerking die de trigger heeft geactiveerd te identificeren als respectievelijk "INSERT", "UPDATE", "DELETE", of "TRUNCATE".
We moeten verwijderingen afzonderlijk van invoegingen en updates afhandelen, omdat de attribuutvalidatietests overbodig zijn en omdat de speciale waarde NEW niet wordt gedefinieerd bij het invoeren van een vóór delete trigger-functie en definieer zo de bijbehorende opgeslagen functie en 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();
Let op het gebruik van de speciale waarde OUDE als een verwijzing naar de rij die op het punt staat te worden verwijderd, d.w.z. de rij zoals deze voor bestaat het verwijderen gebeurt.
We maken een aantal bijlagen om de functionaliteit te testen en te bevestigen dat de controletabel een record van de bijlagen bevat:
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)
Vervolgens maken we een update naar één rij en bevestigen dat de controletabel een record van de wijziging bevat door een tweede naam toe te voegen aan een van de weergavenamen van de gegevensrecords:
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)
En tot slot oefenen we de verwijderfunctie uit en bevestigen we dat de controletabel dat record ook bevat:
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)
VOORBEELD 2 - Afgeleide waarden
Laten we nog een stap verder gaan en ons voorstellen dat we in elke rij een tekstdocument in vrije vorm willen opslaan, bijvoorbeeld een cv in platte tekst of een conferentiepaper of een samenvatting van een amusementskarakter, en we willen het gebruik van de krachtige zoekfunctie voor volledige tekst ondersteunen mogelijkheden van PostgreSQL op deze vrije tekstdocumenten.
We voegen eerst twee attributen toe ter ondersteuning van de opslag van het document en van een bijbehorende tekstzoekvector aan de hoofdtabel. Aangezien de tekstzoekvector per rij wordt afgeleid, heeft het geen zin om deze in de controletabel op te slaan, tenzij we de documentopslagkolom aan de bijbehorende controletabel toevoegen:
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;
ALTER TABLE person_audit ADD COLUMN abstract TEXT;
Vervolgens passen we de triggerfunctie aan om deze nieuwe attributen te verwerken. De kolom met platte tekst wordt op dezelfde manier behandeld als andere door de gebruiker ingevoerde gegevens, maar de tekstzoekvector is een afgeleide waarde en wordt dus afgehandeld door een functieaanroep die de documenttekst reduceert tot een tsvector-gegevenstype voor efficiënt zoeken.
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 updaten we een bestaande rij met wat detailtekst van 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';
en bevestig vervolgens dat de vectorverwerking voor het zoeken naar tekst is gelukt:
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)
VOORBEELD 3 - Triggers en weergaven
De afgeleide tekstzoekvector uit het bovenstaande voorbeeld is niet bedoeld voor menselijke consumptie, d.w.z. het is niet door de gebruiker ingevoerd, en we verwachten nooit dat de waarde aan een eindgebruiker wordt gepresenteerd. Als een gebruiker een waarde probeert in te voeren voor de ts_abstract-kolom, wordt alles wat is opgegeven weggegooid en vervangen door de waarde die intern is afgeleid van de triggerfunctie, zodat we bescherming hebben tegen vergiftiging van het zoekcorpus. Om de kolom volledig te verbergen, kunnen we een verkorte weergave definiëren die dat kenmerk niet bevat, maar we krijgen nog steeds het voordeel van triggeractiviteit op de onderliggende tabel:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;
Voor een eenvoudige weergave maakt PostgreSQL het automatisch beschrijfbaar, zodat we niets anders hoeven te doen om gegevens met succes in te voegen of bij te werken. Wanneer de DML van kracht wordt op de onderliggende tabel, worden de triggers geactiveerd alsof de instructie rechtstreeks op de tabel is toegepast, dus we krijgen nog steeds zowel de tekstzoekondersteuning op de achtergrond die de zoekvectorkolom van de persoonstabel vult als het toevoegen van de informatie wijzigen in de controletabel:
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)
Voor meer gecompliceerde weergaven die niet voldoen aan de vereisten om automatisch te kunnen schrijven, ofwel het regelsysteem of in plaats van triggers kunnen het werk doen om schrijven en verwijderen te ondersteunen.
VOORBEELD 4 - Samenvattingswaarden
Laten we verder verfraaien en het scenario behandelen waarin er een soort transactietabel is. Het kan een overzicht zijn van gewerkte uren, voorraadtoevoegingen en -verminderingen van magazijn- of winkelvoorraad, of misschien een chequeregister met af- en bijschrijvingen voor elke persoon:
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)
);
En laten we zeggen dat hoewel het belangrijk is om de transactiegeschiedenis te bewaren, de bedrijfsregels het gebruik van het nettosaldo in de applicatieverwerking inhouden in plaats van een van de transactiedetails. Om te voorkomen dat we het saldo vaak opnieuw moeten berekenen door alle transacties op te tellen telkens wanneer het saldo nodig is, kunnen we denormaliseren en een huidige saldowaarde daar in de persoonstabel houden door een nieuwe kolom toe te voegen en een trigger en opgeslagen functie te gebruiken om het netto saldo als transacties worden ingevoegd:
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();
Het lijkt misschien vreemd om eerst de update uit te voeren in de opgeslagen functie voordat de niet-negativiteit van de debet-, credit- en saldowaarden wordt gevalideerd, maar in termen van gegevensvalidatie doet de volgorde er niet toe omdat de hoofdtekst van een triggerfunctie wordt uitgevoerd als een databasetransactie, dus als die validatiecontroles mislukken, wordt de hele transactie teruggedraaid wanneer de uitzondering wordt gemaakt. Het voordeel van het eerst uitvoeren van de update is dat de update de betreffende rij vergrendelt voor de duur van de transactie en dat dus elke andere sessie die probeert om dezelfde rij bij te werken, wordt geblokkeerd totdat de huidige transactie is voltooid. De verdere validatietest zorgt ervoor dat het resulterende saldo niet-negatief is, en het uitzonderingsinformatiebericht kan een variabele bevatten, die in dit geval de overtredende poging tot invoegtransactierij voor foutopsporing retourneert.
Om aan te tonen dat het echt werkt, volgen hier een paar voorbeelditems en een vinkje met het bijgewerkte saldo bij elke stap:
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")
Merk op hoe de bovenstaande transactie mislukt bij onvoldoende saldo, d.w.z. het zou een negatief saldo opleveren en met succes worden teruggedraaid. Merk ook op dat we de hele rij hebben geretourneerd met de NIEUWE speciale variabele als extra detail in de foutmelding voor debuggen.
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)
VOORBEELD 5 - Triggers en weergaven Redux
Er is echter een probleem met de bovenstaande implementatie, en dat is dat niets een kwaadwillende gebruiker ervan weerhoudt geld bij te drukken:
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;
We hebben de bovenstaande diefstal voorlopig teruggedraaid en zullen een manier laten zien om bescherming tegen in te bouwen door een trigger te gebruiken om updates van de saldowaarde te voorkomen.
We vergroten eerst de verkorte weergave van eerder om de balanskolom zichtbaar te maken:
CREATE OR REPLACE VIEW abridged_person AS
SELECT login_name, display_name, abstract, balance FROM person;
Dit geeft uiteraard leestoegang tot de balans, maar het lost het probleem nog steeds niet op, want voor eenvoudige weergaven zoals deze op basis van een enkele tabel maakt PostgreSQL de weergave automatisch beschrijfbaar:
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.
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaperEXAMPLE 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.
Conclusie
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.