sql >> Database teknologi >  >> RDS >> PostgreSQL

PostgreSQL-udløsere og grundlæggende funktioner i lagrede funktioner

Note fra Severalnines:Denne blog udgives posthumt, da Berend Tober døde den 16. juli 2018. Vi ærer hans bidrag til PostgreSQL-fællesskabet og ønsker fred for vores ven og gæsteskribent.

I en tidligere artikel diskuterede vi PostgreSQL serielle pseudo-typen, som er nyttig til at udfylde syntetiske nøgleværdier med stigende heltal. Vi så, at anvendelse af nøgleordet seriel datatype i en DDL-sætning (Table Data Definition Language) er implementeret som en kolonnedeklaration af heltalstype, der udfyldes ved en databaseindsættelse med en standardværdi afledt af et simpelt funktionskald. Denne automatiserede adfærd med at påkalde funktionel kode som en del af det integrerede svar på datamanipulationssprog (DML) aktivitet er en kraftfuld funktion i sofistikerede relationelle databasestyringssystemer (RDBMS) som PostgreSQL. I denne artikel dykker vi yderligere ind i et andet mere egnet aspekt til automatisk at fremkalde tilpasset kode, nemlig brugen af ​​triggere og lagrede funktioner.Introduktion

Brugstilfælde til udløsere og lagrede funktioner

Lad os tale om, hvorfor du måske ønsker at investere i at forstå triggere og lagrede funktioner. Ved at indbygge DML-kode i selve databasen kan du undgå dobbeltimplementering af datarelateret kode i flere separate applikationer, der kan bygges til at interface med databasen. Dette sikrer ensartet eksekvering af DML-kode til datavalidering, datarensning eller anden funktionalitet, såsom datarevision (dvs. logning af ændringer) eller vedligeholdelse af en oversigtstabel uafhængigt af enhver kaldende applikation. En anden almindelig brug af triggere og lagrede funktioner er at gøre visninger skrivbare, dvs. at muliggøre indsættelser og/eller opdateringer på komplekse visninger eller at beskytte visse kolonnedata mod uautoriseret modifikation. Derudover krydser data, der behandles på serveren i stedet for i applikationskoden, ikke netværket, så der er en vis mindre risiko for, at data bliver udsat for aflytning samt en reduktion i netværksoverbelastning. I PostgreSQL kan lagrede funktioner også konfigureres til at udføre kode på et højere privilegieniveau end sessionsbrugeren, hvilket indrømmer nogle kraftfulde egenskaber. Vi tager nogle eksempler senere.

Sagen mod triggere og lagrede funktioner

En gennemgang af kommentarer til PostgreSQL General mailinglisten afslørede nogle ugunstige meninger i forhold til brugen af ​​triggere og lagrede funktioner, som jeg nævner her for fuldstændighedens skyld og for at opmuntre dig og dit team til at afveje fordele og ulemper ved din implementering.

Blandt indvendingerne var for eksempel opfattelsen af, at lagrede funktioner ikke er nemme at vedligeholde, og det kræver derfor en erfaren person med sofistikerede færdigheder og viden inden for databaseadministration for at administrere dem. Nogle softwareprofessionelle har rapporteret, at virksomheders ændringskontrol på databasesystemer typisk er mere kraftfuld end på applikationskode, så hvis forretningsregler eller anden logik er implementeret i databasen, så er det uoverkommeligt besværligt at foretage ændringer, efterhånden som kravene udvikler sig. Et andet synspunkt betragter triggere som en uventet bivirkning af en anden handling og kan som sådan være dunkel, let overset, svær at fejlsøge og frustrerende at vedligeholde og bør derfor normalt være det sidste valg, ikke det første.

Disse indvendinger kan have en vis fordel, men hvis du tænker over det, er data et værdifuldt aktiv, og du vil sandsynligvis i virkeligheden have en dygtig og erfaren person eller et team med ansvar for RDBMS i en virksomheds- eller regeringsorganisation alligevel, og på samme måde, Change Kontroltavler er en gennemprøvet komponent i bæredygtig vedligeholdelse af et informationssystem, og en persons bivirkning er lige så vel en andens stærke bekvemmelighed, hvilket er det synspunkt, der er valgt for balancen i denne artikel.

Erklæring af en trigger

Lad os begynde at lære møtrikker og bolte. Der er mange muligheder tilgængelige i den generelle DDL-syntaks til at erklære en trigger, og det ville tage en betydelig tid at behandle alle mulige permutationer, så for korthedens skyld vil vi kun tale om en minimalt påkrævet delmængde af dem i eksempler, der følg ved at bruge denne forkortede syntaks:

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 nødvendige konfigurerbare elementer udover et navn er hvornår , hvorfor , hvor , og hvad , dvs. timingen for, at triggerkoden skal fremkaldes i forhold til den udløsende handling (hvornår), den specifikke type af udløsende DML-sætning (hvorfor), den eller de tabeller, der reageres på (hvor), og den lagrede funktionskode, der skal udføres (hvad).

Erklæring af en funktion

Triggerdeklarationen ovenfor kræver specificering af et funktionsnavn, så teknisk set kan triggerdeklarationen DDL ikke udføres før efter at triggerfunktionen er blevet defineret tidligere. Den generelle DDL-syntaks for en funktionserklæring har også mange muligheder, så af hensyn til håndteringen bruger vi denne minimalt tilstrækkelige syntaks til vores formål her:

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

En triggerfunktion tager ingen parametre, og returtypen skal være TRIGGER. Vi vil tale om de valgfrie modifikatorer, efterhånden som vi støder på dem i eksemplerne nedenfor.

Et navngivningsskema for triggere og funktioner

Den respekterede datalog Phil Karlton er blevet tilskrevet for at erklære (i parafraseret form her), at navngivning af ting er en af ​​de største udfordringer for softwareteams. Jeg vil her præsentere en nem at bruge trigger og lagret funktions navngivningskonvention, som har tjent mig godt, og opfordrer dig til at overveje at adoptere den til dine egne RDBMS-projekter. Navneskemaet i eksemplerne for denne artikel følger et mønster med at bruge det tilknyttede tabelnavn med suffiks med en forkortelse, der angiver den erklærede trigger når og hvorfor attributter:Det første suffiksbogstav vil enten være et "b", "a" eller "i" (for "før", "efter" eller "i stedet for"), næste vil være et eller flere af et "i" , "u", "d" eller "t" (for "indsæt", "opdater", "slet" eller "truncate"), og det sidste bogstav er blot et "t" for udløser. (Jeg bruger en lignende navnekonvention for regler, og i så fald er det sidste bogstav "r"). Så for eksempel ville de forskellige kombinationer af minimal trigger-erklæringsattributter for en tabel med navnet "my_table" være:

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

Det nøjagtige samme navn kan bruges til både triggeren og den tilhørende lagrede funktion, hvilket er fuldstændig tilladt i PostgreSQL, fordi RDBMS'et holder styr på triggere og lagrede funktioner adskilt efter de respektive formål, og den kontekst, som varenavnet bruges i, gør klar, hvilket element navnet refererer til.

Så for eksempel vil en udløsererklæring svarende til scenariet i første række fra tabellen ovenfor ses implementeret som

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

I det tilfælde, hvor en trigger er erklæret med flere hvorfor attributter, skal du blot udvide suffikset passende, f.eks. for en indsættelse eller opdatering trigger, ville ovenstående blive

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

Vis mig allerede noget kode!

Lad os gøre det virkeligt. Vi starter med et simpelt eksempel og udvider derefter for at illustrere yderligere funktioner. Trigger-DDL-sætningerne kræver som nævnt en allerede eksisterende funktion og også en tabel at handle på, så først skal vi have en tabel at arbejde på. Lad os for eksempel sige, at vi skal gemme grundlæggende kontoidentitetsdata

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

Noget dataintegritetshåndhævelse kan håndteres ganske enkelt med den rigtige kolonne DDL, såsom i dette tilfælde et krav om, at login_name eksisterer og ikke er mere end ni tegn langt. Forsøg på at indsætte en NULL-værdi eller en for lang værdi af login_name mislykkes og rapporterer meningsfulde fejlmeddelelser:

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) 

Andre håndhævelser kan håndteres med kontrolbegrænsninger, såsom at kræve en minimumslængde og afvise visse tegn:

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). 

men bemærk, at fejlmeddelelsen ikke er så fuldt ud informativ som før, den formidler kun så meget, som der er kodet i udløsernavnet i stedet for en meningsfuld forklarende tekstmeddelelse. Ved at implementere kontrollogikken i en gemt funktion i stedet, kan du bruge en undtagelse til at udsende en mere nyttig tekstbesked. Tjek begrænsningsudtryk kan heller ikke indeholde underforespørgsler eller henvise til andre variabler end kolonner i den aktuelle række eller andre databasetabeller.

Så lad os droppe kontrolbegrænsningerne

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

og kom videre med triggere og lagrede funktioner.

Vis mig mere kode

Vi har et bord. Går vi videre til funktionen DDL, definerer vi en tom funktion, som vi kan udfylde senere med specifik kode:

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

Dette giver os mulighed for endelig at komme til triggeren DDL, der forbinder tabellen og funktionen, så vi kan lave nogle eksempler:

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

PostgreSQL tillader gemte funktioner at blive skrevet på en række forskellige sprog. I dette tilfælde og de følgende eksempler komponerer vi funktioner i PL/pgSQL-sproget, som er designet specifikt til PostgreSQL og understøtter brugen af ​​alle datatyper, operatorer og funktioner i PostgreSQL RDBMS. Indstillingen SET SCHEMA indstiller skemasøgestien, der vil blive brugt i varigheden af ​​funktionsudførelsen. Det er en god praksis at indstille søgestien for hver funktion, da det sparer at skulle præfiksere databaseobjekter med et skemanavn og beskytter mod visse sårbarheder relateret til søgestien.

EKSEMPEL 0 - Datavalidering

Lad os som et første eksempel implementere de tidligere kontroller, men med mere menneskevenlige beskeder.

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

"NYE"-kvalifikationen er en reference til rækken af ​​data, der skal indsættes. Det er en af ​​en række specielle variabler, der er tilgængelige i en triggerfunktion. Vi introducerer nogle andre nedenfor. Bemærk også, PostgreSQL tillader substitution af de enkelte anførselstegn, der afgrænser funktionslegemet, med andre afgrænsningstegn, i dette tilfælde efter en almindelig konvention om at bruge dobbelte dollartegn som afgrænsningstegn, da selve funktionslegemet indeholder enkelte anførselstegn. Udløserfunktioner skal afsluttes ved at returnere enten den NYE række, der skal indsættes, eller NULL for at afbryde handlingen i stilhed.

De samme indsættelsesforsøg mislykkes som forventet, men nu med venlig besked:

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. 

EKSEMPEL 1 - Revisionslogning

Med lagrede funktioner har vi et bredt råderum til, hvad den påkaldte kode gør, herunder at henvise til andre tabeller (hvilket ikke er muligt med kontrolbegrænsninger). Som et mere komplekst eksempel vil vi gennemgå implementeringen af ​​en revisionstabel, det vil sige at vedligeholde en registrering i en separat tabel over indsættelser, opdateringer og sletninger til en hovedtabel. Revisionstabellen indeholder typisk de samme attributter som hovedtabellen, som bruges til at registrere de ændrede værdier, plus yderligere attributter til at registrere den udførte handling for at foretage ændringen, samt et transaktionstidsstempel og en registrering af brugeren, der foretager ændre:

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

I dette tilfælde er implementering af revision meget let, vi ændrer simpelthen den eksisterende triggerfunktion til at inkludere DML for at påvirke revisionstabelindsættelsen, og omdefinerer derefter triggeren til at udløse på opdateringer såvel som inserts. Bemærk, at vi har valgt ikke at ændre udløserfunktionsnavnets suffiks til "biut", men hvis revisionsfunktionaliteten havde været et kendt krav på det oprindelige designtidspunkt, ville det være det anvendte navn:

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

Bemærk, at vi har introduceret en anden speciel variabel "TG_OP", som systemet indstiller til at identificere den DML-operation, der udløste triggeren som henholdsvis "INSERT", "UPDATE", "DELETE", eller "TRUNCATE".

Vi er nødt til at håndtere sletninger separat fra indsættelser og opdateringer, da attributvalideringstestene er overflødige, og fordi den NYE specielle værdi ikke defineres ved indtastning til en før sletning trigger funktion og så definere tilsvarende gemt funktion og 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(); 

Bemærk brugen af ​​den OLD specialværdi som reference til den række, der er ved at blive slettet, dvs. rækken, som den eksisterede før sletningen sker.

Vi laver et par indstik for at teste funktionaliteten og bekræfte, at revisionstabellen indeholder en registrering af indsatserne:

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) 

Derefter foretager vi en opdatering til en række og bekræfter, at revisionstabellen indeholder en registrering af ændringen, der tilføjer et mellemnavn til et af datapostens visningsnavne:

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) 

Og til sidst udøver vi slettefunktionaliteten og bekræfter, at revisionstabellen også inkluderer denne post:

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) 

EKSEMPEL 2 - Afledte værdier

Lad os tage dette et skridt videre og forestille os, at vi ønsker at gemme et tekstdokument i frit format inden for hver række, f.eks. et almindeligt-tekstformateret CV eller et konferenceoplæg eller et abstrakt for underholdningskarakterer, og vi ønsker at understøtte brugen af ​​den kraftfulde fuldtekstsøgning funktionerne i PostgreSQL på disse frie tekstdokumenter.

Vi tilføjer først to attributter for at understøtte lagring af dokumentet og af en tilhørende tekstsøgningsvektor til hovedtabellen. Da tekstsøgevektoren udledes på en række-basis, er det ingen mening at gemme den i revisionstabellen, men vi tilføjer dokumentlagringskolonnen til den tilhørende revisionstabel:

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

ALTER TABLE person_audit ADD COLUMN abstract TEXT; 

Derefter ændrer vi triggerfunktionen for at behandle disse nye attributter. Kolonnen med almindelig tekst håndteres på samme måde som andre brugerindtastede data, men tekstsøgevektoren er en afledt værdi og håndteres derfor af et funktionskald, som reducerer dokumentteksten til en tsvector-datatype for effektiv søgning.

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

Som en test opdaterer vi en eksisterende række med noget detaljeret tekst fra 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'; 

og bekræft derefter, at tekstsøgningsvektorbehandlingen var vellykket:

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) 

EKSEMPEL 3 - Triggere og visninger

Den afledte tekstsøgningsvektor fra ovenstående eksempel er ikke beregnet til konsum, dvs. den er ikke indtastet af brugeren, og vi forventer aldrig at præsentere værdien for en slutbruger. Hvis en bruger forsøger at indsætte en værdi for kolonnen ts_abstract, vil alt angivet blive kasseret og erstattet med værdien afledt internt til triggerfunktionen, så vi har beskyttelse mod forgiftning af søgekorpuset. For at skjule kolonnen fuldstændigt, kan vi definere en forkortet visning, der ikke inkluderer denne egenskab, men vi får stadig fordelen af ​​triggeraktivitet på den underliggende tabel:

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

For en enkel visning gør PostgreSQL det automatisk skrivbart, så vi ikke behøver at gøre andet for at indsætte eller opdatere data. Når DML'en træder i kraft på den underliggende tabel, aktiveres triggerne, som om sætningen blev anvendt direkte på tabellen, så vi stadig får både tekstsøgningsunderstøttelsen udført i baggrunden, der udfylder søgevektorkolonnen i persontabellen samt tilføjer ændre oplysninger til revisionstabellen:

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) 

For mere komplicerede visninger, der ikke opfylder kravene for automatisk at kunne skrives, kan enten regelsystemet eller i stedet for triggere kan gøre arbejdet med at understøtte skrivning og sletning.

EKSEMPEL 4 - Sammenfatningsværdier

Lad os pynte yderligere og behandle scenariet, hvor der er en form for transaktionstabel. Det kan være en registrering af arbejdstimer, tilføjelser til lager og reduktioner af lager- eller detaillager, eller måske et checkregister med debiteringer og krediteringer for hver person:

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

Og lad os sige, at selvom det er vigtigt at bevare transaktionshistorikken, indebærer forretningsregler brug af nettobalancen i ansøgningsbehandlingen frem for nogen af ​​transaktionsdetaljerne. For at undgå at skulle genberegne saldoen hyppigt ved at summere alle transaktionerne hver gang saldoen er nødvendig, kan vi denormalisere og beholde en aktuel saldoværdi lige der i persontabellen ved at tilføje en ny kolonne og bruge en trigger og lagret funktion til at opretholde nettosaldoen efterhånden som transaktioner indsættes:

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

Det kan virke mærkeligt at foretage opdateringen først i den lagrede funktion, før man validerer ikke-negativiteten af ​​debet-, kredit- og saldoværdierne, men med hensyn til datavalidering er ordren ligegyldig, fordi kroppen af ​​en triggerfunktion udføres som en databasetransaktion, så hvis disse valideringstjek mislykkes, så rulles hele transaktionen tilbage, når undtagelsen hæves. Fordelen ved at udføre opdateringen først er, at opdateringen låser den berørte række i hele transaktionens varighed, så enhver anden session, der forsøger at opdatere den samme række, blokeres, indtil den aktuelle transaktion er fuldført. Den yderligere valideringstest sikrer, at den resulterende saldo er ikke-negativ, og undtagelsesinformationsmeddelelsen kan indeholde en variabel, som i dette tilfælde vil returnere den stødende forsøgsindsættelsestransaktionsrække til fejlretning.

For at demonstrere, at det rent faktisk virker, er her et par eksempler på posteringer og en kontrol, der viser den opdaterede saldo ved hvert trin:

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") 

Bemærk, hvordan ovenstående transaktion mislykkes på grund af utilstrækkelige midler, dvs. den ville producere en negativ saldo og med succes ruller tilbage. Bemærk også, at vi returnerede hele rækken med den NYE specialvariabel som ekstra detalje i fejlmeddelelsen til fejlretning.

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) 

EKSEMPEL 5 - Udløsere og visninger Redux

Der er dog et problem med ovenstående implementering, og det er, at intet forhindrer en ondsindet bruger i at udskrive penge:

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; 

Vi har rullet ovenstående tyveri tilbage for nu og vil vise en måde at indbygge beskyttelse mod ved at bruge en trigger for at forhindre opdateringer af saldoværdien.

Vi udvider først den forkortede visning fra tidligere for at afsløre balancesøjlen:

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

Dette giver naturligvis læseadgang til saldoen, men det løser stadig ikke problemet, fordi for simple visninger som denne baseret på en enkelt tabel, gør PostgreSQL automatisk visningen skrivbar:

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 Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload Whitepaper

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 tabel.

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.

Konklusion

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.


  1. Kan ikke logge på SQL Server + SQL Server Authentication + Fejl:18456

  2. Sådan oprettes og vedligeholdes MySQL-databaser i cPanel

  3. Sådan tjekker du PostgreSQL-versionen

  4. Sådan sorteres resultatet fra string_agg()