Poznámka od několikanásobného:Tento blog je publikován posmrtně, protože Berend Tober zemřel 16. července 2018. Ctíme jeho příspěvky do komunity PostgreSQL a přejeme mír našemu příteli a hostujícímu spisovateli.
V předchozím článku jsme diskutovali o sériovém pseudotypu PostgreSQL, který je užitečný pro naplnění syntetických klíčových hodnot rostoucími celými čísly. Viděli jsme, že použití klíčového slova typu sériový datový typ v příkazu jazyka definice dat tabulky (DDL) je implementováno jako deklarace sloupce typu integer, která je po vložení databáze naplněna výchozí hodnotou odvozenou z jednoduchého volání funkce. Toto automatizované chování vyvolání funkčního kódu jako součást integrální odezvy na aktivitu jazyka pro manipulaci s daty (DML) je výkonnou funkcí sofistikovaných systémů pro správu relačních databází (RDBMS), jako je PostgreSQL. V tomto článku se dále ponoříme do dalšího schopnějšího aspektu pro automatické vyvolání vlastního kódu, konkrétně použití spouštěčů a uložených funkcí. Úvod
Případy použití pro spouštěče a uložené funkce
Pojďme si promluvit o tom, proč byste mohli chtít investovat do porozumění spouštěčům a uloženým funkcím. Zabudováním kódu DML do databáze samotné se můžete vyhnout duplicitní implementaci kódu souvisejícího s daty ve více samostatných aplikacích, které mohou být vytvořeny pro rozhraní s databází. To zajišťuje konzistentní provádění kódu DML pro ověřování dat, čištění dat nebo další funkce, jako je audit dat (tj. protokolování změn) nebo udržování souhrnné tabulky nezávisle na jakékoli volající aplikaci. Dalším běžným použitím spouštěčů a uložených funkcí je učinit pohledy zapisovatelnými, tj. umožnit vkládání a/nebo aktualizace komplexních pohledů nebo chránit určitá data sloupců před neoprávněnými úpravami. Navíc data zpracovávaná na serveru, nikoli v kódu aplikace, neprocházejí sítí, takže existuje určité menší riziko, že budou data vystavena odposlechu, a také se sníží přetížení sítě. V PostgreSQL lze také uložené funkce nakonfigurovat tak, aby spouštěly kód na vyšší úrovni oprávnění než uživatel relace, což připouští některé výkonné schopnosti. Některé příklady uvedeme později.
Případ proti spouštěčům a uloženým funkcím
Přezkoumání komentáře k PostgreSQL General mailing listu odhalilo některé nepříznivé názory na používání spouštěčů a uložených funkcí, které zde uvádím pro úplnost a abych vás a váš tým povzbudil ke zvážení pro a proti vaší implementace.
Mezi námitky byl například názor, že uložené funkce není snadné udržovat, a proto je k jejich správě potřeba zkušená osoba se sofistikovanými dovednostmi a znalostmi v administraci databází. Někteří softwaroví profesionálové uvedli, že kontroly podnikových změn na databázových systémech jsou obvykle důraznější než na aplikačním kódu, takže pokud jsou v databázi implementována obchodní pravidla nebo jiná logika, pak je provádění změn podle vyvíjejících se požadavků neúměrně těžkopádné. Jiný úhel pohledu považuje spouštěče za neočekávaný vedlejší účinek nějaké jiné akce a jako takové mohou být nejasné, snadno přehlédnutelné, obtížně laditelné a frustrující na údržbu, a proto by obvykle měly být až poslední volbou, nikoli první.
Tyto námitky mohou mít určitou opodstatněnost, ale pokud se nad tím zamyslíte, data jsou cenným aktivem, a tak pravděpodobně ve skutečnosti ve skutečnosti chcete kvalifikovanou a zkušenou osobu nebo tým odpovědný za RDBMS v korporátní nebo vládní organizaci, a podobně, Change Řídicí panely jsou osvědčenou součástí udržitelné údržby informačního systému záznamů a vedlejším efektem jedné osoby je stejně tak silné pohodlí druhé osoby, což je hledisko přijaté pro vyvážení tohoto článku.
Vyhlášení spouštěče
Pojďme se naučit matice a šrouby. V obecné syntaxi DDL je k dispozici mnoho možností pro deklaraci spouštěče a zpracování všech možných permutací by zabralo značný čas, takže pro stručnost budeme hovořit pouze o minimálně požadované podmnožině z nich v příkladech, které postupujte podle této zkrácené syntaxe:
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
Požadované konfigurovatelné prvky kromě name jsou kdy , proč , kde a co , tj. načasování spouštěcího kódu, který má být vyvolán, vzhledem ke spouštěcí akci (kdy), konkrétní typ spouštěcího příkazu DML (proč), aktivovanou tabulku nebo tabulky (kde) a kód uložené funkce, který se má provést (co).
Deklarace funkce
Výše uvedená deklarace spouštěče vyžaduje specifikaci názvu funkce, takže technicky nelze deklaraci spouštěče DDL provést, dokud nebyla předtím definována spouštěcí funkce. Obecná syntaxe DDL pro deklaraci funkce má také mnoho možností, takže kvůli správnosti zde pro naše účely použijeme tuto minimálně postačující syntaxi:
CREATE [ OR REPLACE ] FUNCTION
name () RETURNS TRIGGER
{ LANGUAGE lang_name
| SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
}...
Spouštěcí funkce nemá žádné parametry a návratový typ musí být TRIGGER. O volitelných modifikátorech si povíme, jak se s nimi setkáme v příkladech níže.
Schéma pojmenování pro spouštěče a funkce
Respektovaný počítačový vědec Phil Karlton byl připisován prohlášení (v parafrázované formě zde), že pojmenování věcí je jednou z největších výzev pro softwarové týmy. Uvedu zde snadno použitelnou konvenci pojmenování spouštěčů a uložených funkcí, která mi dobře posloužila, a vybízím vás, abyste zvážili její přijetí pro své vlastní projekty RDBMS. Schéma pojmenování v příkladech pro tento článek se řídí vzorem použití přidruženého názvu tabulky s příponou se zkratkou označující deklarovaný spouštěč kdy a proč atributy:První písmeno přípony bude buď „b“, „a“ nebo „i“ (pro „před“, „po“ nebo „místo“), další bude jedno nebo více písmen „i“ , „u“, „d“ nebo „t“ (pro „vložit“, „aktualizovat“, „vymazat“ nebo „zkrátit“) a poslední písmeno je pouze „t“ pro spuštění. (Podobnou konvenci pojmenování používám pro pravidla a v tom případě je poslední písmeno „r“). Takže například různé minimální kombinace atributů deklarace spouštěče pro tabulku s názvem „my_table“ by byly:
|-------------+-------------+-----------+---------------+-----------------|
| 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 |
|-------------+-------------+-----------+---------------+-----------------|
Úplně stejný název lze použít jak pro spouštěč, tak pro přidruženou uloženou funkci, což je v PostgreSQL zcela přípustné, protože RDBMS sleduje spouštěče a uložené funkce odděleně podle příslušných účelů a kontext, ve kterém se název položky používá, umožňuje jasné, ke které položce se název vztahuje.
Takže například deklarace spouštěče odpovídající scénáři prvního řádku z výše uvedené tabulky by byla implementována jako
CREATE TRIGGER my_table_bit
BEFORE INSERT
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_bit();
V případě, že je spouštěč deklarován s více proč atributy, stačí příponu vhodně rozšířit, např. pro vložení nebo aktualizaci spouště, výše uvedené by se stalo
CREATE TRIGGER my_table_biut
BEFORE INSERT OR UPDATE
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_biut();
Ukažte mi již nějaký kód!
Udělejme to skutečné. Začneme jednoduchým příkladem a poté jej rozšíříme, abychom ilustrovali další funkce. Spouštěcí příkazy DDL vyžadují již existující funkci, jak bylo zmíněno, a také tabulku, podle které se má jednat, takže nejprve potřebujeme tabulku, se kterou budeme pracovat. Řekněme například, že potřebujeme uložit základní údaje o identitě účtu
CREATE TABLE person (
login_name varchar(9) not null primary key,
display_name text
);
Některá vynucení integrity dat lze jednoduše zvládnout pomocí správného sloupce DDL, jako je v tomto případě požadavek, aby přihlašovací_jméno existovalo a nebylo delší než devět znaků. Pokusy vložit hodnotu NULL nebo příliš dlouhou hodnotu login_name se nezdaří a hlásí smysluplné chybové zprávy:
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)
Další vynucení lze řešit pomocí kontrolních omezení, jako je vyžadování minimální délky a odmítnutí určitých znaků:
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).
ale všimněte si, že chybová zpráva není tak plně informativní jako dříve, sděluje pouze tolik, kolik je zakódováno v názvu spouštěče, spíše než smysluplnou vysvětlující textovou zprávu. Pokud místo toho implementujete kontrolní logiku do uložené funkce, můžete použít výjimku k odeslání užitečnější textové zprávy. Výrazy kontrolního omezení také nemohou obsahovat poddotazy ani odkazovat na jiné proměnné než sloupce aktuálního řádku ani jiné databázové tabulky.
Zrušme tedy kontrolní omezení
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;
a pokračujte se spouštěči a uloženými funkcemi.
Ukaž mi další kód
Máme stůl. Když přejdeme k funkci DDL, definujeme funkci s prázdným tělem, kterou můžeme později vyplnit specifickým kódem:
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
SET search_path = public
AS '
BEGIN
END;
';
To nám umožňuje konečně se dostat ke spouštěči DDL spojujícímu tabulku a funkci, takže si můžeme udělat několik příkladů:
CREATE TRIGGER person_bit
BEFORE INSERT ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
PostgreSQL umožňuje zapisovat uložené funkce v mnoha různých jazycích. V tomto případě a následujících příkladech skládáme funkce v jazyce PL/pgSQL, který je navržen speciálně pro PostgreSQL a podporuje použití všech datových typů, operátorů a funkcí PostgreSQL RDBMS. Volba SET SCHEMA nastavuje cestu pro vyhledávání schématu, která bude použita po dobu provádění funkce. Nastavení vyhledávací cesty pro každou funkci je dobrou praxí, protože ušetří nutnost přidávat databázové objekty před název schématu a chrání před určitými zranitelnostmi souvisejícími s vyhledávací cestou.
PŘÍKLAD 0 – Ověření dat
Jako první příklad zavedeme dřívější kontroly, ale s lidsky vstřícnějším zasíláním zpráv.
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;
$$;
Kvalifikátor „NEW“ je odkaz na řádek dat, který se má vložit. Je to jedna z mnoha speciálních proměnných dostupných v rámci spouštěcí funkce. Některé další představíme níže. Všimněte si také, že PostgreSQL povoluje nahrazení jednoduchých uvozovek vymezujících tělo funkce jinými oddělovači, v tomto případě se řídí běžnou konvencí použití dvojitých znaků dolaru jako oddělovačů, protože samotné tělo funkce obsahuje jednoduché uvozovky. Spouštěcí funkce se musí ukončit vrácením buď NEW řádku, který se má vložit, nebo NULL, aby se akce tiše přerušila.
Stejné pokusy o vložení selžou podle očekávání, ale nyní s přátelským zasíláním zpráv:
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.
PŘÍKLAD 1 – Protokolování auditu
S uloženými funkcemi máme širokou škálu možností, co vyvolaný kód dělá, včetně odkazování na jiné tabulky (což není možné s kontrolními omezeními). Jako složitější příklad si projdeme implementaci tabulky auditu, to znamená udržování záznamu v samostatné tabulce o vkládání, aktualizacích a odstraňování v hlavní tabulce. Tabulka auditu obvykle obsahuje stejné atributy jako hlavní tabulka, které se používají k zaznamenání změněných hodnot, plus další atributy pro zaznamenání operace provedené za účelem provedení změny, stejně jako časové razítko transakce a záznam uživatele, který provedl změnu. změnit:
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
);
V tomto případě je implementace auditování velmi snadná, jednoduše upravíme stávající funkci spouštěče tak, aby zahrnovala DML, aby se provedlo vložení auditní tabulky, a poté znovu definujeme spouštěč, aby se spouštěl při aktualizacích i vloženích. Všimněte si, že jsme se rozhodli nezměnit příponu názvu spouštěcí funkce na „biut“, ale pokud byla funkce auditu známým požadavkem při počátečním návrhu, byl by použit tento název:
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();
Všimněte si, že jsme zavedli další speciální proměnnou „TG_OP“, kterou systém nastavuje pro identifikaci operace DML, která spustila spouštěč, jako „INSERT“, „UPDATE“, „DELETE“ nebo „TRUNCATE“.
Odstranění musíme zpracovávat odděleně od vložení a aktualizací, protože testy ověřování atributů jsou nadbytečné a protože speciální hodnota NEW není definována při vstupu do před odstraněním spouštěcí funkce a tak definujte odpovídající uloženou funkci a spouštění:
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();
Všimněte si použití speciální hodnoty OLD jako odkazu na řádek, který se chystá smazat, tj. řádek tak, jak existuje před dojde k odstranění.
Vytvoříme několik vložek, abychom otestovali funkčnost a potvrdili, že tabulka auditu obsahuje záznam vložek:
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)
Poté provedeme aktualizaci jednoho řádku a potvrdíme, že tabulka auditu obsahuje záznam o změně přidání prostředního jména k jednomu ze zobrazovaných jmen datových záznamů:
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)
A nakonec použijeme funkci odstranění a potvrdíme, že tabulka auditu obsahuje také tento záznam:
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)
PŘÍKLAD 2 – Odvozené hodnoty
Udělejme to ještě o krok dále a představme si, že chceme do každého řádku uložit nějaký volně formátovaný textový dokument, řekněme životopis ve formátu prostého textu nebo konferenční příspěvek nebo abstrakt zábavního charakteru, a chceme podporovat použití výkonného fulltextového vyhledávání. schopnosti PostgreSQL na těchto volně formátovaných textových dokumentech.
Nejprve přidáme dva atributy pro podporu ukládání dokumentu a souvisejícího vektoru textového vyhledávání do hlavní tabulky. Vzhledem k tomu, že vektor textového vyhledávání je odvozen na základě řádků, nemá smysl jej ukládat do tabulky auditu, pokud přidáme sloupec úložiště dokumentů do související tabulky auditu:
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;
ALTER TABLE person_audit ADD COLUMN abstract TEXT;
Poté upravíme spouštěcí funkci, aby zpracovala tyto nové atributy. Se sloupcem prostého textu se zachází stejně jako s jinými daty zadanými uživatelem, ale vektor textového vyhledávání je odvozená hodnota, a tak je zpracováván voláním funkce, která redukuje text dokumentu na datový typ tsvector pro efektivní vyhledávání.
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;
$$;
Jako test aktualizujeme existující řádek nějakým podrobným textem z Wikipedie:
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';
a poté potvrďte, že zpracování vektoru textového vyhledávání bylo úspěšné:
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)
PŘÍKLAD 3 – Spouštěče a zobrazení
Odvozený vektor textového vyhledávání z výše uvedeného příkladu není určen k lidské spotřebě, to znamená, že jej nezadává uživatel, a nikdy neočekáváme, že hodnotu předložíme koncovému uživateli. Pokud se uživatel pokusí vložit hodnotu do sloupce ts_abstract, vše poskytnuté bude zahozeno a nahrazeno hodnotou odvozenou interně ze spouštěcí funkce, takže máme ochranu proti otravě vyhledávacího korpusu. Abychom sloupec úplně skryli, můžeme definovat zkrácené zobrazení, které tento atribut nezahrnuje, ale přesto získáme výhodu aktivační aktivity v podkladové tabulce:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;
Pro jednoduché zobrazení PostgreSQL automaticky umožňuje zápis, takže pro úspěšné vkládání nebo aktualizaci dat nemusíme dělat nic jiného. Když DML vstoupí v platnost na podkladovou tabulku, aktivují se spouštěče, jako kdyby byl příkaz aplikován přímo na tabulku, takže stále získáme podporu textového vyhledávání spuštěnou na pozadí, která vyplní sloupec vyhledávacího vektoru tabulky osob, a také připojení změnit informace do tabulky auditu:
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)
U složitějších zobrazení, která nesplňují požadavky na automatický zápis, buď systém pravidel, nebo místo spouštěče mohou dělat práci při podpoře zápisů a mazání.
PŘÍKLAD 4 – Souhrnné hodnoty
Pojďme to dále přikrášlit a zabývat se scénářem, kde existuje nějaký typ transakční tabulky. Může to být záznam odpracovaných hodin, přírůstky a úbytky skladových nebo maloobchodních zásob nebo možná kontrolní registr s debety a kredity pro každou osobu:
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)
);
A řekněme, že i když je důležité uchovávat transakční historii, obchodní pravidla znamenají použití čistého zůstatku při zpracování aplikací spíše než jakýchkoli detailů transakce. Abychom nemuseli často přepočítávat zůstatek sčítáním všech transakcí pokaždé, když je zůstatek potřeba, můžeme denormalizovat a udržovat aktuální hodnotu zůstatku přímo v tabulce osob přidáním nového sloupce a použitím spouštěcí a uložené funkce k udržení čistý zůstatek po vložení transakcí:
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();
Může se zdát zvláštní provést aktualizaci nejprve v uložené funkci před ověřením nezápornosti debetních, kreditních a zůstatkových hodnot, ale z hlediska ověření dat na objednávce nezáleží, protože tělo spouštěcí funkce se provádí jako databázové transakce, takže pokud tyto ověřovací kontroly selžou, bude celá transakce při vyvolání výjimky odvolána. Výhoda provedení aktualizace jako první spočívá v tom, že aktualizace uzamkne dotčený řádek po dobu trvání transakce, takže jakákoli další relace pokoušející se aktualizovat stejný řádek je zablokována, dokud se aktuální transakce nedokončí. Další ověřovací test zajišťuje, že výsledný zůstatek není záporný, a informační zpráva o výjimce může obsahovat proměnnou, která v tomto případě vrátí řádek pokusu o vložení problematické transakce k ladění.
Abychom demonstrovali, že to skutečně funguje, zde je několik vzorových záznamů a kontrola ukazující aktualizovaný zůstatek v každém kroku:
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")
Všimněte si, jak výše uvedená transakce selže při nedostatku finančních prostředků, tj. vytvoří záporný zůstatek a úspěšně se vrátí zpět. Všimněte si také, že jsme vrátili celý řádek se speciální proměnnou NEW jako další podrobnosti v chybové zprávě pro ladění.
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)
PŘÍKLAD 5 – Spouštění a redukce zobrazení
S výše uvedenou implementací je však problém, a to ten, že nic nebrání uživateli se zlými úmysly tisknout peníze:
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;
Prozatím jsme zrušili výše uvedenou krádež a ukážeme způsob, jak zabudovat ochranu proti použití spouštěče na pohledu, aby se zabránilo aktualizacím hodnoty zůstatku.
Nejprve rozšíříme zkrácený pohled z předchozího zobrazení, abychom odhalili sloupec rovnováhy:
CREATE OR REPLACE VIEW abridged_person AS
SELECT login_name, display_name, abstract, balance FROM person;
To samozřejmě umožňuje přístup pro čtení k zůstatku, ale stále to neřeší problém, protože pro jednoduché pohledy, jako je tento založený na jedné tabulce, PostgreSQL automaticky umožňuje zápis:
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;
Mohli bychom použít pravidlo, ale abychom ilustrovali, že spouštěče lze definovat v pohledech i tabulkách, použijeme druhou cestu a použijeme místo aktualizace 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.
Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si 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 tabulka.
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.
Závěr
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.