Бележка от Severalnines:Този блог се публикува посмъртно, тъй като Беренд Тобер почина на 16 юли 2018 г. Почитаме приноса му към общността на PostgreSQL и желаем мир за нашия приятел и гост писател.
В предишна статия обсъждахме сериен псевдотип на PostgreSQL, който е полезен за попълване на синтетични ключови стойности с нарастващи цели числа. Видяхме, че използването на ключовата дума за сериен тип данни в израза за език за дефиниране на таблици (DDL) се изпълнява като декларация на колона с целочислен тип, която се попълва при вмъкване на база данни със стойност по подразбиране, получена от просто извикване на функция. Това автоматизирано поведение на извикване на функционален код като част от интегралния отговор на дейността на езика за манипулиране на данни (DML) е мощна характеристика на сложни системи за управление на релационни бази данни (RDBMS) като PostgreSQL. В тази статия задълбаваме по-нататък в друг по-способен аспект за автоматично извикване на персонализиран код, а именно използването на тригери и съхранени функции. Въведение
Случаи на употреба за тригери и съхранени функции
Нека поговорим защо може да искате да инвестирате в разбирането на тригери и съхранени функции. Чрез вграждането на DML код в самата база данни можете да избегнете дублираното внедряване на свързан с данни код в множество отделни приложения, които могат да бъдат изградени за взаимодействие с базата данни. Това гарантира последователно изпълнение на DML код за валидиране на данни, почистване на данни или друга функционалност, като одит на данни (т.е. регистриране на промените) или поддържане на обобщена таблица независимо от всяко извикващо приложение. Друга често срещана употреба на тригери и съхранени функции е да се направи изгледите за записване, т.е. да се активират вмъквания и/или актуализации на сложни изгледи или да се защитят определени данни от колона от неоторизирана модификация. Освен това данните, обработени на сървъра, а не в кода на приложението, не преминават през мрежата, така че има по-малък риск данните да бъдат изложени на подслушване, както и намаляване на претоварването на мрежата. Също така, в PostgreSQL съхранените функции могат да бъдат конфигурирани да изпълняват код на по-високо ниво на привилегии от потребителя на сесията, което допуска някои мощни възможности. Ще направим някои примери по-късно.
Случаят срещу тригери и съхранени функции
Преглед на коментар към общия пощенски списък на PostgreSQL разкри някои мнения, неблагоприятни по отношение на използването на тригери и съхранени функции, които споменавам тук за пълнота и за да насърча вас и вашия екип да претеглите плюсовете и минусите за вашата реализация.
Сред възраженията беше например схващането, че съхранените функции не са лесни за поддържане, като по този начин се изисква опитен човек със сложни умения и познания в администрирането на база данни, за да ги управлява. Някои софтуерни специалисти съобщават, че контролът на корпоративните промени в системите за бази данни обикновено е по-енергичен, отколкото в кода на приложението, така че ако бизнес правилата или друга логика се прилагат в базата данни, тогава правенето на промени с развитието на изискванията е непосилно тромаво. Друга гледна точка разглежда тригерите като неочакван страничен ефект от някакво друго действие и като такива може да са неясни, лесно пропуснати, трудни за отстраняване на грешки и разочароващи за поддръжка и затова обикновено трябва да са последният избор, а не първият.
Тези възражения може да имат някои заслуги, но ако се замислите, данните са ценен актив и така вероятно всъщност искате квалифициран и опитен човек или екип, отговорен за RDBMS в корпоративна или правителствена организация, и по подобен начин, Промяна Контролните табла са доказан компонент от устойчива поддръжка на информационна система за запис, а страничният ефект на един човек е също толкова мощно удобство на друг, което е гледната точка, възприета за баланса на тази статия.
Деклариране на тригер
Нека да се заемем с изучаването на гайките и болтовете. Има много опции, налични в общия синтаксис на DDL за деклариране на задействане и би отнело значително време за обработка на всички възможни пермутации, така че за краткост ще говорим само за минимално необходимо подмножество от тях в примери, които следвайте, като използвате този съкратен синтаксис:
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
Необходимите конфигурируеми елементи освен име са кога , защо , къде , и какво т.е. времето за извикване на кода за задействане спрямо задействащото действие (кога), специфичния тип задействащ DML израз (защо), таблицата или таблиците с действието (къде) и съхранения функционален код за изпълнение (какво).
Деклариране на функция
Декларацията на тригера по-горе изисква спецификация на име на функция, така че технически DDL декларацията на тригера не може да бъде изпълнена, докато функцията за задействане не е предварително дефинирана. Общият DDL синтаксис за декларация на функция също има много опции, така че за управляемост ще използваме този минимално достатъчен синтаксис за нашите цели тук:
CREATE [ OR REPLACE ] FUNCTION
name () RETURNS TRIGGER
{ LANGUAGE lang_name
| SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
}...
Функцията за задействане не приема параметри, а типът на връщане трябва да е TRIGGER. Ще говорим за незадължителните модификатори, тъй като ги срещаме в примерите по-долу.
Схема за именуване на тригери и функции
На уважавания компютърен учен Фил Карлтън е приписвано, че декларира (в перифразирана форма тук), че наименуването на нещата е едно от най-големите предизвикателства за софтуерните екипи. Тук ще представя лесна за използване конвенция за именуване на тригери и съхранени функции, която ми послужи добре и ви насърчавам да обмислите да го приемете за вашите собствени проекти за RDBMS. Схемата за именуване в примерите за тази статия следва модел на използване на свързаното име на таблица със суфикс със съкращение, указващо декларирания тригер когато и защо атрибути:Първата буква на наставката ще бъде или „b“, „a“, или „i“ (за „преди“, „след“ или „вместо“), следващата ще бъде една или повече от „i“ , „u“, „d“ или „t“ (за „вмъкване“, „актуализация“, „изтриване“ или „отрязване“), а последната буква е просто „t“ за тригер. (Използвам подобна конвенция за именуване за правила и в този случай последната буква е „r“). Така например, различните комбинации от атрибути за декларация на минимално задействане за таблица с име „my_table“ биха били:
|-------------+-------------+-----------+---------------+-----------------|
| 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 |
|-------------+-------------+-----------+---------------+-----------------|
Точно същото име може да се използва както за тригера, така и за свързаната съхранена функция, което е напълно допустимо в PostgreSQL, тъй като RDBMS следи тригерите и съхранените функции отделно за съответните цели, а контекстът, в който се използва името на елемента, прави ясно за кой елемент се отнася името.
Така например, декларация за задействане, съответстваща на сценария от първия ред от таблицата по-горе, ще се разглежда като внедрена като
CREATE TRIGGER my_table_bit
BEFORE INSERT
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_bit();
В случай, когато задействане е декларирано с множество защо атрибути, просто разширете суфикса по подходящ начин, напр. за вмъкване или актуализиране тригер, горното ще стане
CREATE TRIGGER my_table_biut
BEFORE INSERT OR UPDATE
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_biut();
Покажи ми вече някакъв код!
Нека го направим реално. Ще започнем с прост пример и след това ще го разширим, за да илюстрираме допълнителни функции. Задействащите DDL оператори изискват вече съществуваща функция, както беше споменато, както и таблица, върху която да действаме, така че първо се нуждаем от таблица, върху която да работим. Например, да кажем, че трябва да съхраняваме основни данни за самоличността на акаунта
CREATE TABLE person (
login_name varchar(9) not null primary key,
display_name text
);
Някои налагане на целостта на данните може да се управлява просто с правилна колона DDL, като например в този случай изискване login_name да съществува и да не е повече от девет знака. Опитите за вмъкване на стойност NULL или твърде дълга стойност на login_name се провалят и докладват смислени съобщения за грешка:
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)
Други налагания могат да се обработват с ограничения за проверка, като например изискване на минимална дължина и отхвърляне на определени знаци:
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).
но забележете, че съобщението за грешка не е толкова напълно информативно, както преди, като предава само толкова, колкото е кодирано в името на тригера, а не смислено обяснително текстово съобщение. Като внедрите логиката за проверка в съхранена функция вместо това, можете да използвате изключение, за да изпратите по-полезно текстово съобщение. Освен това изразите за ограничения за проверка не могат да съдържат подзаявки, нито да се отнасят до променливи, различни от колони на текущия ред или други таблици на базата данни.
Така че нека премахнем ограниченията за проверка
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;
и продължете с тригерите и съхранените функции.
Покажи ми още код
Имаме маса. Преминавайки към функцията DDL, ние дефинираме функция с празно тяло, която можем да попълним по-късно с конкретен код:
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
SET search_path = public
AS '
BEGIN
END;
';
Това ни позволява най-накрая да стигнем до тригера DDL, свързващ таблицата и функцията, за да можем да направим някои примери:
CREATE TRIGGER person_bit
BEFORE INSERT ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
PostgreSQL позволява съхранените функции да бъдат написани на различни езици. В този случай и следващите примери, ние съставяме функции на езика PL/pgSQL, който е проектиран специално за PostgreSQL и поддържа използването на всички типове данни, оператори и функции на PostgreSQL RDBMS. Опцията SET SCHEMA задава пътя за търсене на схема, който ще се използва за продължителността на изпълнението на функцията. Задаването на пътя за търсене за всяка функция е добра практика, тъй като спестява необходимостта от поставяне на префикс на обектите на базата данни с име на схема и предпазва от определени уязвимости, свързани с пътя за търсене.
ПРИМЕР 0 – Проверка на данни
Като първи пример, нека приложим по-ранните проверки, но с по-удобни за хората съобщения.
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;
$$;
Квалификаторът „НОВО“ е препратка към реда с данни, които предстои да бъдат вмъкнати. Това е една от множеството специални променливи, налични в рамките на функцията за задействане. Ще ви представим някои други по-долу. Забележете също, че PostgreSQL позволява заместване на единичните кавички, ограничаващи тялото на функцията с други ограничители, в този случай след общоприетото условно използване на двойни знаци за долари като разделител, тъй като самото тяло на функцията включва символи в единични кавички. Функциите за задействане трябва да излязат чрез връщане на НОВИЯ ред, който трябва да бъде вмъкнат, или NULL, за да прекъснат безшумно действието.
Същите опити за вмъкване се провалят според очакванията, но вече с приятелски съобщения:
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.
ПРИМЕР 1 – Регистриране на одит
Със съхранените функции имаме широка свобода за това какво прави извиканият код, включително препращане към други таблици (което не е възможно с ограничения за проверка). Като по-сложен пример ще преминем през прилагането на таблица за одит, тоест поддържане на запис в отделна таблица на вмъквания, актуализации и изтривания в основна таблица. Таблицата за одит обикновено съдържа същите атрибути като основната таблица, които се използват за записване на променените стойности, плюс допълнителни атрибути за записване на операцията, изпълнена за извършване на промяната, както и времева марка на транзакцията и запис на потребителя, който прави промяната. промяна:
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
);
В този случай внедряването на одит е много лесно, ние просто модифицираме съществуващата функция за задействане, за да включим DML, за да повлияе на вмъкването на одитната таблица, и след това предефинираме тригера, за да се задейства при актуализации, както и при вмъквания. Имайте предвид, че избрахме да не променяме суфикса на името на функцията за задействане на „biut“, но ако функционалността на одита е била известно изискване в първоначалния момент на проектиране, това би било използваното име:
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();
Обърнете внимание, че въведохме друга специална променлива „TG_OP“, която системата задава, за да идентифицира DML операцията, която е задействала спусъка съответно като „INSERT“, „UPDATE“, „DELETE“ или „TRUNCATE“.
Трябва да обработваме изтривания отделно от вмъквания и актуализации, тъй като тестовете за валидиране на атрибути са излишни и защото специалната стойност НОВО не е дефинирана при влизане в преди изтриване тригерна функция и така дефинирайте съответната съхранена функция и тригер:
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();
Обърнете внимание на използването на специалната стойност OLD като препратка към реда, който предстои да бъде изтрит, т.е. реда, какъвто е съществувал преди изтриването се случва.
Правим няколко вмъквания, за да тестваме функционалността и да потвърдим, че таблицата за одит включва запис на вмъкванията:
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)
След това правим актуализация на един ред и потвърждаваме, че таблицата за одит включва запис на промяната, добавяйки средно име към едно от показваните имена на записа с данни:
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)
И накрая, ние упражняваме функцията за изтриване и потвърждаваме, че одитната таблица включва и този запис:
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)
ПРИМЕР 2 – Извлечени стойности
Нека направим това още една стъпка по-далеч и да си представим, че искаме да съхраняваме някакъв текстов документ в свободна форма във всеки ред, да речем автобиография, форматирана в обикновен текст или конферентен документ или резюме на развлекателния герой, и искаме да поддържаме използването на мощното търсене в пълен текст възможности на PostgreSQL върху тези текстови документи в свободна форма.
Първо добавяме два атрибута за поддръжка на съхранение на документа и на свързан вектор за текстово търсене към основната таблица. Тъй като векторът за текстово търсене се извлича на база на ред, няма смисъл да го съхраняваме в таблицата за одит, независимо дали добавим колоната за съхранение на документи към свързаната таблица за одит:
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;
ALTER TABLE person_audit ADD COLUMN abstract TEXT;
След това модифицираме функцията за задействане, за да обработим тези нови атрибути. Колоната с обикновен текст се обработва по същия начин като другите въведени от потребителя данни, но векторът за търсене на текст е извлечена стойност и така се обработва от извикване на функция, което намалява текста на документа до тип данни tsvector за ефективно търсене.
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;
$$;
Като тест актуализираме съществуващ ред с подробен текст от 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';
и след това потвърдете, че обработката на вектора за текстово търсене е била успешна:
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)
ПРИМЕР 3 – Задействания и изгледи
Извлеченият вектор за текстово търсене от горния пример не е предназначен за консумация от човека, т.е. не е въведен от потребителя и никога не очакваме да представим стойността на краен потребител. Ако потребителят се опита да вмъкне стойност за колоната ts_abstract, всичко предоставено ще бъде изхвърлено и заменено със стойността, извлечена вътрешно от функцията за задействане, така че имаме защита срещу отравяне на корпуса за търсене. За да скрием колоната напълно, можем да дефинираме съкратен изглед, който не включва този атрибут, но все пак получаваме ползата от задействане на активността върху основната таблица:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;
За опростен изглед PostgreSQL автоматично го прави записваем, така че не е нужно да правим нищо друго, за да вмъкнем или актуализираме успешно данни. Когато DML влезе в сила върху основната таблица, тригерите се активират, сякаш операторът е приложен директно към таблицата, така че все още получаваме както поддръжката за текстово търсене, изпълнявана във фонов режим, попълвайки колоната за вектор за търсене на таблицата с хора, както и добавяне на промяна на информацията в таблицата за одит:
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)
За по-сложни изгледи, които не отговарят на изискванията за автоматично записване, или системата от правила, или вместо тригерите могат да свършат работата, за да поддържат записване и изтриване.
ПРИМЕР 4 – Обобщени стойности
Нека да украсим допълнително и да разгледаме сценария, в който има някакъв вид таблица за транзакции. Това може да бъде запис на отработените часове, добавяне на инвентар и намаляване на складови или търговски наличности, или може би чеков регистър с дебити и кредити за всяко лице:
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)
);
И нека кажем, че макар да е важно да се запази историята на транзакциите, бизнес правилата предполагат използване на нетния баланс при обработката на приложения, а не всички подробности за транзакцията. За да избегнем честото преизчисляване на салдото чрез сумиране на всички транзакции всеки път, когато балансът е необходим, можем да денормализираме и поддържаме текуща стойност на баланса точно там в таблицата с лицата, като добавим нова колона и използваме тригер и съхранена функция за поддържане нетното салдо при вмъкване на транзакции:
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();
Може да изглежда странно да направите актуализацията първо в съхранената функция, преди да потвърдите неотрицателността на стойностите на дебита, кредита и салдото, но по отношение на валидирането на данните поръчката няма значение, тъй като тялото на функцията за задействане се изпълнява като транзакция на база данни, така че ако тези проверки за валидиране не успеят, тогава цялата транзакция се връща обратно, когато изключението бъде повдигнато. Предимството да направите актуализацията първо е, че актуализацията заключва засегнатия ред за продължителността на транзакцията и така всяка друга сесия, която се опитва да актуализира същия ред, се блокира, докато текущата транзакция завърши. Допълнителният тест за валидиране гарантира, че полученото салдо е неотрицателно, а съобщението за информация за изключение може да включва променлива, която в този случай ще върне нарушителния ред за вмъкване на транзакция за отстраняване на грешки.
За да демонстрирате, че действително работи, ето няколко примерни записа и проверка, показваща актуализирания баланс на всяка стъпка:
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")
Обърнете внимание как горната транзакция се проваля при недостатъчни средства, т.е. ще доведе до отрицателен баланс и успешно ще се върне обратно. Също така имайте предвид, че върнахме целия ред със специалната променлива НОВА като допълнителен детайл в съобщението за грешка за отстраняване на грешки.
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)
ПРИМЕР 5 – Задействания и изгледи Redux
Въпреки това има проблем с горната реализация и той е, че нищо не пречи на злонамерен потребител да печата пари:
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;
Отменихме кражбата по-горе засега и ще покажем начин за изграждане на защита срещу чрез използване на тригер в изглед, за да предотвратим актуализации на стойността на баланса.
Първо увеличаваме съкратения изглед от по-рано, за да разкрием колоната за баланс:
CREATE OR REPLACE VIEW abridged_person AS
SELECT login_name, display_name, abstract, balance FROM person;
Това очевидно позволява достъп за четене до баланса, но все още не решава проблема, защото за прости изгледи като този, базирани на една таблица, PostgreSQL автоматично прави изгледа за запис:
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.
Изтеглете Бялата книга днес Управление и автоматизация на PostgreSQL с ClusterControl Научете какво трябва да знаете, за да внедрите, наблюдавате, управлявате и мащабирате PostgreSQLD Изтеглете Бялата книга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 таблица.
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.
Заключение
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.