sql >> Databasteknik >  >> RDS >> PostgreSQL

PostgreSQL-utlösare och grunder för lagrade funktioner

Anteckning från Severalnines:Den här bloggen publiceras postumt när Berend Tober gick bort den 16 juli 2018. Vi hedrar hans bidrag till PostgreSQL-communityt och önskar fred för vår vän och gästskribent.

I en tidigare artikel diskuterade vi PostgreSQL seriella pseudo-typ, som är användbar för att fylla i syntetiska nyckelvärden med ökande heltal. Vi såg att användningen av nyckelordet seriell datatyp i en DDL-sats (tabelldatadefinitionsspråk) implementeras som en kolumndeklaration av heltalstyp som fylls i, vid en databasinfogning, med ett standardvärde som härletts från ett enkelt funktionsanrop. Detta automatiserade beteende att anropa funktionell kod som en del av det integrerade svaret på datamanipulationsspråk (DML)-aktivitet är en kraftfull funktion i sofistikerade relationsdatabashanteringssystem (RDBMS) som PostgreSQL. I den här artikeln går vi närmare in på en annan mer kapabel aspekt för att automatiskt anropa anpassad kod, nämligen användningen av triggers och lagrade funktioner. Inledning

Användningsfall för utlösare och lagrade funktioner

Låt oss prata om varför du kanske vill investera i att förstå triggers och lagrade funktioner. Genom att bygga in DML-kod i själva databasen kan du undvika dubbla implementeringar av datarelaterad kod i flera separata applikationer som kan byggas för att samverka med databasen. Detta säkerställer konsekvent exekvering av DML-kod för datavalidering, datarensning eller annan funktionalitet såsom datagranskning (d.v.s. loggning av ändringar) eller underhåll av en sammanfattningstabell oberoende av alla anropande applikationer. En annan vanlig användning av triggers och lagrade funktioner är att göra vyer skrivbara, det vill säga att möjliggöra infogning och/eller uppdateringar av komplexa vyer eller att skydda viss kolumndata från obehörig modifiering. Dessutom passerar inte data som bearbetas på servern i stället för i applikationskoden nätverket, så det finns en viss mindre risk för att data utsätts för avlyssning samt en minskning av nätverksstockning. Dessutom kan lagrade funktioner i PostgreSQL konfigureras för att exekvera kod på en högre behörighetsnivå än sessionsanvändaren, vilket medger några kraftfulla funktioner. Vi tar några exempel senare.

Färdet mot utlösare och lagrade funktioner

En granskning av kommentarer på PostgreSQL General e-postlistan avslöjade några ogynnsamma åsikter mot användningen av triggers och lagrade funktioner som jag nämner här för fullständighetens skull och för att uppmuntra dig och ditt team att väga för- och nackdelar med din implementering.

Bland invändningarna fanns till exempel uppfattningen att lagrade funktioner inte är lätta att underhålla, vilket kräver en erfaren person med sofistikerad kompetens och kunskap inom databasadministration för att hantera dem. Vissa mjukvaruproffs har rapporterat att företagens förändringskontroller på databassystem vanligtvis är mer kraftfulla än på applikationskod, så om affärsregler eller annan logik implementeras i databasen är det oöverkomligt besvärligt att göra ändringar när kraven utvecklas. En annan synpunkt betraktar triggers som en oväntad bieffekt av någon annan åtgärd och kan som sådan vara oklar, lätt missad, svår att felsöka och frustrerande att underhålla och bör därför vanligtvis vara det sista valet, inte det första.

Dessa invändningar kan ha en viss förtjänst, men om du tänker efter är data en värdefull tillgång och därför vill du förmodligen ha en skicklig och erfaren person eller ett team som ansvarar för RDBMS i ett företag eller en statlig organisation i alla fall, och på liknande sätt, Change Kontrolltavlor är en beprövad komponent för hållbart underhåll för ett informationssystem, och en persons bieffekt är lika väl en annans kraftfulla bekvämlighet, vilket är den synpunkt som antas för balansen i denna artikel.

Deklarera en utlösare

Låt oss börja lära oss muttrar och bultar. Det finns många tillgängliga alternativ i den allmänna DDL-syntaxen för att deklarera en trigger, och det skulle ta lång tid att behandla alla möjliga permutationer, så för korthetens skull kommer vi bara att prata om en minimalt erforderlig delmängd av dem i exempel som följ med denna förkortade 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

De nödvändiga konfigurerbara elementen förutom ett namn är när , varför , var , och vad , d.v.s. tidpunkten för att triggerkoden ska anropas i förhållande till den utlösande åtgärden (när), den specifika typen av utlösande DML-sats (varför), den eller de tabeller som ageras (var) och den lagrade funktionskoden som ska exekveras (vad).

Deklarera en funktion

Triggerdeklarationen ovan kräver specificering av ett funktionsnamn, så tekniskt sett kan triggerdeklarationen DDL inte exekveras förrän efter att triggerfunktionen har definierats tidigare. Den allmänna DDL-syntaxen för en funktionsdeklaration har också många alternativ så för hanterbarhet kommer vi att använda denna minimalt tillräckliga syntax för våra syften här:

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

En triggerfunktion tar inga parametrar och returtypen måste vara TRIGGER. Vi kommer att prata om de valfria modifierarna när vi möter dem i exemplen nedan.

Ett namnschema för utlösare och funktioner

Den respekterade datavetaren Phil Karlton har tillskrivits (i omskriven form här) att namngivning av saker är en av de största utmaningarna för mjukvaruteam. Jag kommer att presentera här en lättanvänd utlösare och lagrad funktionsnamnkonvention som har tjänat mig väl och uppmuntrar dig att överväga att använda den för dina egna RDBMS-projekt. Namnschemat i exemplen för den här artikeln följer ett mönster av att använda det associerade tabellnamnet med suffixet med en förkortning som indikerar den deklarerade utlösaren när och varför attribut:Den första suffixbokstaven kommer att vara antingen ett "b", "a" eller "i" (för "före", "efter" eller "istället för"), nästa kommer att vara ett eller flera av ett "i" , "u", "d" eller "t" (för "infoga", "uppdatera", "radera" eller "korta av"), och den sista bokstaven är bara ett "t" för utlösare. (Jag använder en liknande namnkonvention för regler, och i så fall är den sista bokstaven "r"). Så till exempel skulle de olika kombinationerna av minimal trigger-deklaration attribut för en tabell med namnet "my_table" vara:

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

Exakt samma namn kan användas för både triggern och den associerade lagrade funktionen, vilket är helt tillåtet i PostgreSQL eftersom RDBMS håller reda på triggers och lagrade funktioner separat efter respektive syfte, och sammanhanget som objektnamnet används i gör rensa vilket objekt namnet hänvisar till.

Så till exempel skulle en triggerdeklaration som motsvarar det första radens scenario från tabellen ovan ses implementerad som

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

I fallet när en utlösare deklareras med flera varför attribut, expandera bara suffixet på lämpligt sätt, t.ex. för en infogning eller uppdatering utlösare, skulle ovanstående bli

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

Visa mig lite kod redan!

Låt oss göra det verkligt. Vi börjar med ett enkelt exempel och utökar sedan det för att illustrera ytterligare funktioner. Trigger DDL-satserna kräver en redan existerande funktion, som nämnts, och även en tabell att agera på, så först behöver vi en tabell att arbeta på. Låt oss till exempel säga att vi behöver lagra grundläggande kontoidentitetsdata

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

Viss dataintegritetsupprätthållande kan hanteras helt enkelt med rätt kolumn DDL, som i det här fallet ett krav på att login_name finns och inte vara mer än nio tecken långt. Försök att infoga ett NULL-värde eller ett för långt värde på login_name misslyckas och rapporterar meningsfulla felmeddelanden:

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)

Andra tillämpningar kan hanteras med kontrollbegränsningar, som att kräva en minimilängd och att avvisa vissa tecken:

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 lägg märke till att felmeddelandet inte är lika fullt informativt som tidigare, det förmedlar bara så mycket som är kodat i triggernamnet snarare än ett meningsfullt förklarande textmeddelande. Genom att implementera kontrolllogiken i en lagrad funktion istället kan du använda ett undantag för att skicka ett mer användbart textmeddelande. Dessutom kan kontrollbegränsningsuttryck inte innehålla underfrågor eller hänvisa till andra variabler än kolumner i den aktuella raden eller andra databastabeller.

Så låt oss släppa kontrollbegränsningarna

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

och gå vidare med triggers och lagrade funktioner.

Visa mig lite mer kod

Vi har ett bord. När vi går vidare till funktionen DDL definierar vi en tom funktion, som vi kan fylla i senare med specifik kod:

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

Detta gör att vi äntligen kan komma till utlösaren DDL som kopplar ihop tabellen och funktionen så att vi kan göra några exempel:

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

PostgreSQL tillåter att lagrade funktioner kan skrivas på en mängd olika språk. I det här fallet och i följande exempel, komponerar vi funktioner i PL/pgSQL-språket som är designat specifikt för PostgreSQL och stöder användningen av alla datatyper, operatorer och funktioner i PostgreSQL RDBMS. Alternativet SET SCHEMA ställer in sökvägen för schemat som kommer att användas under den tid funktionen körs. Att ställa in sökvägen för varje funktion är en bra praxis, eftersom det inte behöver prefixa databasobjekt med ett schemanamn och skyddar mot vissa sårbarheter relaterade till sökvägen.

EXEMPEL 0 - Datavalidering

Som ett första exempel, låt oss implementera de tidigare kontrollerna, men med mer människovänliga meddelanden.

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

Kvalificeringen "NYA" är en referens till raden med data som ska infogas. Det är en av ett antal specialvariabler som finns tillgängliga inom en triggerfunktion. Vi kommer att presentera några andra nedan. Observera också att PostgreSQL tillåter ersättning av de enkla citattecken som avgränsar funktionskroppen med andra avgränsare, i det här fallet enligt en vanlig konvention att använda dubbla dollartecken som avgränsare, eftersom funktionskroppen i sig innehåller enkla citattecken. Triggerfunktioner måste avslutas genom att returnera antingen den NYA raden som ska infogas eller NULL för att tyst avbryta åtgärden.

Samma insättningsförsök misslyckas som förväntat, men nu med vänliga meddelanden:

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.

EXEMPEL 1 - Granskningsloggning

Med lagrade funktioner har vi ett stort utrymme för vad den anropade koden gör, inklusive hänvisningar till andra tabeller (vilket inte är möjligt med kontrollbegränsningar). Som ett mer komplext exempel kommer vi att gå igenom implementeringen av en revisionstabell, det vill säga att upprätthålla en post, i en separat tabell, över insättningar, uppdateringar och borttagningar till en huvudtabell. Granskningstabellen innehåller vanligtvis samma attribut som huvudtabellen, som används för att registrera de ändrade värdena, plus ytterligare attribut för att registrera operationen som utfördes för att göra ändringen, såväl som en transaktionstidstämpel och en post för användaren som gör ändra:

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 det här fallet är det mycket enkelt att implementera granskning, vi ändrar helt enkelt den befintliga triggerfunktionen för att inkludera DML för att påverka insättningen av granskningstabellen, och sedan omdefinierar utlösaren för att aktiveras på såväl uppdateringar som infogar. Observera att vi har valt att inte ändra suffixet för triggerfunktionens namn till "biut", men om revisionsfunktionen hade varit ett känt krav vid den första designtiden, skulle det vara namnet som användes:

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

Observera att vi har introducerat en annan specialvariabel "TG_OP" som systemet ställer in för att identifiera DML-operationen som aktiverade triggern som antingen "INSERT", "UPDATE", "DELETE", eller "TRUNCATE", respektive.

Vi måste hantera borttagningar separat från infogningar och uppdateringar eftersom attributvalideringstesterna är överflödiga och eftersom det NYA specialvärdet inte definieras vid inträde i en före radering triggerfunktion och så definiera motsvarande lagrad funktion och 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();

Notera användningen av det GAMLA specialvärdet som en referens till raden som är på väg att raderas, dvs. raden som den existerade före raderingen sker.

Vi gör ett par inlägg för att testa funktionaliteten och bekräfta att granskningstabellen innehåller en registrering av bilagorna:

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)

Sedan gör vi en uppdatering av en rad och bekräftar att granskningstabellen innehåller en post över ändringen som lägger till ett mellannamn till ett av dataposternas visningsnamn:

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)

Och slutligen använder vi borttagningsfunktionen och bekräftar att granskningstabellen även inkluderar den posten:

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)

EXEMPEL 2 - Härledda värden

Låt oss ta det här ett steg längre och föreställa oss att vi vill lagra ett textdokument i fritt format inom varje rad, t.ex. ett vanlig textformaterat CV eller konferenspapper eller abstrakt för underhållningskaraktär, och vi vill stödja användningen av den kraftfulla fulltextsökningen funktioner för PostgreSQL på dessa fria textdokument.

Vi lägger först till två attribut för att stödja lagring av dokumentet och en tillhörande textsökningsvektor till huvudtabellen. Eftersom textsökningsvektorn härleds per rad, är det ingen idé att lagra den i granskningstabellen, eftersom vi lägger till dokumentlagringskolumnen till den tillhörande granskningstabellen:

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

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Sedan modifierar vi triggerfunktionen för att bearbeta dessa nya attribut. Oformaterad text-kolumn hanteras på samma sätt som andra användarinmatade data, men textsökningsvektorn är ett härlett värde och hanteras därför av ett funktionsanrop som reducerar dokumenttexten till en tsvector-datatyp för effektiv sökning.

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 ett test uppdaterar vi en befintlig rad med lite detaljtext från 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';

och bekräfta sedan att textsökningsvektorbehandlingen lyckades:

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)

EXEMPEL 3 - Utlösare och visningar

Den härledda textsökningsvektorn från exemplet ovan är inte avsedd för mänsklig konsumtion, det vill säga den är inte användarinmatad och vi förväntar oss aldrig att presentera värdet för en slutanvändare. Om en användare försöker infoga ett värde för kolumnen ts_abstract, kommer allt som tillhandahålls att kasseras och ersättas med värdet som härleds internt till triggerfunktionen, så vi har skydd mot att förgifta sökkorpusen. För att dölja kolumnen helt kan vi definiera en förkortad vy som inte inkluderar det attributet, men vi får ändå fördelen av att utlösa aktivitet på den underliggande tabellen:

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

För en enkel vy gör PostgreSQL det automatiskt skrivbart så att vi inte behöver göra något annat för att framgångsrikt infoga eller uppdatera data. När DML träder i kraft på den underliggande tabellen, aktiveras triggarna som om uttalandet applicerades direkt på tabellen så vi får fortfarande både textsökningsstödet kört i bakgrunden och fyller i sökvektorkolumnen i persontabellen samt lägger till ändra information till granskningstabellen:

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 mer komplicerade vyer som inte uppfyller kraven för att automatiskt vara skrivbara, antingen regelsystemet eller istället för triggers kan göra jobbet för att stödja skrivningar och raderingar.

EXEMPEL 4 - Sammanfattningsvärden

Låt oss försköna ytterligare och behandla scenariot där det finns någon typ av transaktionstabell. Det kan vara ett register över arbetade timmar, lagertillägg och minskningar av lager eller detaljhandelslager, eller kanske ett checkregister med debiteringar och krediter för varje 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)
);

Och låt oss säga att även om det är viktigt att behålla transaktionshistoriken, innebär affärsregler att använda nettobalansen i ansökningsbehandlingen snarare än någon av transaktionsdetaljerna. För att undvika att ofta behöva räkna om saldot genom att summera alla transaktioner varje gång saldot behövs, kan vi avnormalisera och behålla ett aktuellt saldovärde precis där i persontabellen genom att lägga till en ny kolumn och använda en trigger och lagrad funktion för att underhålla nettobalansen när transaktioner infogas:

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 tyckas konstigt att göra uppdateringen först i den lagrade funktionen innan man validerar icke-negativiteten för debet-, kredit- och saldovärdena, men när det gäller datavalidering spelar ordern ingen roll eftersom kroppen i en triggerfunktion exekveras som en databastransaktion, så om dessa valideringskontroller misslyckas, rullas hela transaktionen tillbaka när undantaget tas upp. Fördelen med att göra uppdateringen först är att uppdateringen låser den berörda raden under transaktionens varaktighet och så att alla andra sessioner som försöker uppdatera samma rad blockeras tills den aktuella transaktionen slutförs. Det ytterligare valideringstestet försäkrar att det resulterande saldot är icke-negativt, och undantagsinformationsmeddelandet kan inkludera en variabel, som i detta fall kommer att returnera den misslyckade försöket att infoga transaktionsraden för felsökning.

För att visa att det faktiskt fungerar, här är några exempel på poster och en kontroll som visar det uppdaterade saldot vid varje steg:

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

Notera hur transaktionen ovan misslyckas på otillräckliga medel, d.v.s. den skulle ge ett negativt saldo och rullar tillbaka framgångsrikt. Observera också att vi returnerade hela raden med den NYA specialvariabeln som extra detalj i felmeddelandet för felsökning.

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)

EXEMPEL 5 - Utlösare och vyer Redux

Det finns dock ett problem med implementeringen ovan, och det är att ingenting hindrar en illvillig användare från att skriva ut pengar:

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 rullat tillbaka stölden ovan för nu och kommer att visa ett sätt att bygga in skydd mot genom att använda en utlösare i syfte att förhindra uppdateringar av saldovärdet.

Vi utökar först den förkortade vyn från tidigare för att exponera balanskolumnen:

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

Detta ger uppenbarligen läsåtkomst till saldot, men det löser fortfarande inte problemet eftersom för enkla vyer som dessa baserade på en enda tabell, gör PostgreSQL automatiskt vyn 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.

Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda 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 bord.

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.

Slutsats

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. Hur man ställer in ett standardvärde för en befintlig kolumn

  2. Hantera din MS SQL-replikering

  3. Hur listar du primärnyckeln för en SQL Server-tabell?

  4. Så här fixar du "Procedur förväntar sig parametern '@statement' av typen 'ntext/nchar/nvarchar'." Fel i SQL Server