PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Wyzwalacze PostgreSQL i podstawy funkcji przechowywanych

Notatka od Manynines:Ten blog jest publikowany pośmiertnie po śmierci Berenda Tobera 16 lipca 2018 r. Szanujemy jego wkład w społeczność PostgreSQL i życzymy pokoju naszemu przyjacielowi i gościnnemu pisarzowi.

W poprzednim artykule omówiliśmy pseudotyp szeregowy PostgreSQL, który jest przydatny do wypełniania wartości kluczy syntetycznych rosnącymi liczbami całkowitymi. Widzieliśmy, że użycie słowa kluczowego serial data type w instrukcji Table Data Definition Language (DDL) jest zaimplementowane jako deklaracja kolumny typu całkowitego, która jest wypełniana, po wstawieniu bazy danych, wartością domyślną uzyskaną z prostego wywołania funkcji. To zautomatyzowane zachowanie wywoływania kodu funkcjonalnego w ramach integralnej odpowiedzi na działanie języka manipulacji danymi (DML) jest potężną funkcją zaawansowanych systemów zarządzania relacyjnymi bazami danych (RDBMS), takich jak PostgreSQL. W tym artykule zagłębimy się w inny, bardziej wydajny aspekt automatycznego wywoływania niestandardowego kodu, a mianowicie użycie wyzwalaczy i przechowywanych funkcji.Wprowadzenie

Przypadki użycia wyzwalaczy i zapisanych funkcji

Porozmawiajmy o tym, dlaczego warto zainwestować w zrozumienie wyzwalaczy i funkcji przechowywanych. Wbudowując kod DML do samej bazy danych, można uniknąć powielania implementacji kodu związanego z danymi w wielu oddzielnych aplikacjach, które mogą być zbudowane w celu komunikowania się z bazą danych. Zapewnia to spójne wykonywanie kodu DML w celu sprawdzania poprawności danych, czyszczenia danych lub innych funkcji, takich jak audyt danych (tj. rejestrowanie zmian) lub utrzymywanie tabeli podsumowań niezależnie od jakiejkolwiek aplikacji wywołującej. Innym powszechnym zastosowaniem wyzwalaczy i funkcji przechowywanych jest umożliwienie zapisu widoków, tj. umożliwienie wstawiania i/lub aktualizacji złożonych widoków lub ochrona określonych danych w kolumnach przed nieautoryzowaną modyfikacją. Ponadto dane przetwarzane na serwerze, a nie w kodzie aplikacji, nie przechodzą przez sieć, dzięki czemu istnieje mniejsze ryzyko narażenia danych na podsłuch, a także zmniejszenie przeciążenia sieci. Ponadto w PostgreSQL funkcje przechowywane mogą być skonfigurowane do wykonywania kodu na wyższym poziomie uprawnień niż użytkownik sesyjny, co daje pewne potężne możliwości. Kilka przykładów zrobimy później.

Sprawa dotycząca wyzwalaczy i zapisanych funkcji

Przegląd komentarzy na liście mailingowej PostgreSQL General ujawnił pewne opinie nieprzychylne w stosowaniu wyzwalaczy i przechowywanych funkcji, o których wspominam tutaj dla kompletności i aby zachęcić Ciebie i Twój zespół do rozważenia zalet i wad implementacji.

Wśród zarzutów znalazło się na przykład przekonanie, że przechowywane funkcje nie są łatwe w utrzymaniu, co wymaga od doświadczonej osoby posiadającej zaawansowane umiejętności i wiedzę z zakresu administrowania bazami danych do zarządzania nimi. Niektórzy specjaliści ds. oprogramowania zgłaszali, że korporacyjne kontrole zmian w systemach baz danych są zazwyczaj bardziej energiczne niż w kodzie aplikacji, więc jeśli w bazie danych zaimplementowano reguły biznesowe lub inną logikę, wprowadzanie zmian w miarę ewolucji wymagań jest nadmiernie uciążliwe. Inny punkt widzenia uważa wyzwalacze za nieoczekiwany efekt uboczny jakiegoś innego działania i jako takie mogą być niejasne, łatwe do przeoczenia, trudne do debugowania i frustrujące w utrzymaniu, dlatego zwykle powinny być ostatnim wyborem, a nie pierwszym.

Te zastrzeżenia mogą mieć pewną wartość, ale jeśli się nad tym zastanowisz, dane są cennym zasobem, a więc prawdopodobnie potrzebujesz wykwalifikowanej i doświadczonej osoby lub zespołu odpowiedzialnego za RDBMS w organizacji korporacyjnej lub rządowej, i podobnie Zmień Tablice kontrolne są sprawdzonym elementem trwałej konserwacji systemu informacji, a efektem ubocznym jednej osoby jest równie potężna wygoda innej osoby, co jest punktem widzenia przyjętym dla zrównoważenia tego artykułu.

Deklarowanie wyzwalacza

Przejdźmy do nauki nakrętek i śrub. W ogólnej składni DDL dostępnych jest wiele opcji deklarowania wyzwalacza, a potraktowanie wszystkich możliwych permutacji zajęłoby dużo czasu, więc dla zwięzłości omówimy tylko minimalnie wymagany podzbiór w przykładach, które postępuj zgodnie z tą skróconą składnią:

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

Wymagane konfigurowalne elementy oprócz nazwykiedy , dlaczego , gdzie i co , tj. czas wywołania kodu wyzwalacza w stosunku do akcji wyzwalającej (kiedy), określony typ instrukcji wyzwalającej DML (dlaczego), tabela lub tabele zastosowane do działania (gdzie) oraz przechowywany kod funkcji do wykonania (co).

Deklarowanie funkcji

Powyższa deklaracja wyzwalacza wymaga podania nazwy funkcji, więc technicznie deklaracja wyzwalacza DDL nie może zostać wykonana, dopóki funkcja wyzwalacza nie zostanie wcześniej zdefiniowana. Ogólna składnia DDL dla deklaracji funkcji ma również wiele opcji, więc dla ułatwienia zarządzania użyjemy tej składni, która jest minimalnie wystarczająca do naszych celów tutaj:

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

Funkcja wyzwalacza nie przyjmuje parametrów, a typem zwracanym musi być TRIGGER. Porozmawiamy o opcjonalnych modyfikatorach, które napotkamy w poniższych przykładach.

Schemat nazewnictwa wyzwalaczy i funkcji

Szanowany informatyk Phil Karlton został uznany za deklarującego (w sparafrazowanej formie tutaj), że nazywanie rzeczy jest jednym z największych wyzwań dla zespołów programistycznych. Zaprezentuję tutaj prosty w użyciu wyzwalacz i konwencję nazewnictwa funkcji przechowywanych, która dobrze mi służyła i zachęcam do rozważenia zastosowania jej we własnych projektach RDBMS. Schemat nazewnictwa w przykładach w tym artykule jest zgodny ze wzorcem używania nazwy powiązanej tabeli z sufiksem wskazującym zadeklarowany wyzwalacz kiedy i dlaczego atrybuty:pierwszą literą przyrostka będzie „b”, „a” lub „i” (od „przed”, „po” lub „zamiast”), następna będzie jedna lub więcej z „i” , „u”, „d” lub „t” (od „insert”, „update”, „delete” lub „truncate”), a ostatnia litera to po prostu „t” dla wyzwalacza. (Używam podobnej konwencji nazewnictwa reguł, w tym przypadku ostatnią literą jest „r”). Na przykład różne kombinacje atrybutów minimalnej deklaracji wyzwalacza dla tabeli o nazwie „moja_tabela” to:

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

Ta sama nazwa może być użyta zarówno dla wyzwalacza, jak i powiązanej funkcji przechowywanej, co jest całkowicie dozwolone w PostgreSQL, ponieważ RDBMS śledzi wyzwalacze i przechowywane funkcje oddzielnie według odpowiednich celów, a kontekst, w którym użyto nazwy elementu, sprawia, że wyczyść, do którego elementu odnosi się nazwa.

Na przykład deklaracja wyzwalacza odpowiadająca scenariuszowi pierwszego wiersza z powyższej tabeli będzie postrzegana jako zaimplementowana jako

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

W przypadku, gdy wyzwalacz jest zadeklarowany z wieloma dlaczego atrybuty, po prostu odpowiednio rozwiń sufiks, np. dla wstaw lub zaktualizuj wyzwalacz, powyższe stanie się

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

Pokaż mi już jakiś kod!

Uczyńmy to rzeczywistością. Zaczniemy od prostego przykładu, a następnie rozwiniemy go, aby zilustrować dalsze funkcje. Instrukcje DDL wyzwalacza wymagają wcześniej istniejącej funkcji, jak wspomniano, a także tabeli, na której będą działać, więc najpierw potrzebujemy tabeli do pracy. Na przykład załóżmy, że musimy przechowywać podstawowe dane dotyczące tożsamości konta

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

Niektóre wymuszanie integralności danych można po prostu obsłużyć za pomocą odpowiedniej kolumny DDL, na przykład w tym przypadku wymaganie, aby nazwa_logowania istniała i miała nie więcej niż dziewięć znaków. Próby wstawienia wartości NULL lub zbyt długiej wartości login_name kończą się niepowodzeniem i zgłaszają znaczące komunikaty o błędach:

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)

Inne wymuszenia można obsługiwać za pomocą ograniczeń sprawdzających, takich jak wymaganie minimalnej długości i odrzucanie niektórych znaków:

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 zauważ, że komunikat o błędzie nie jest tak w pełni informacyjny, jak poprzednio, przekazuje tylko tyle, ile jest zakodowane w nazwie wyzwalacza, a nie sensowny wyjaśniający komunikat tekstowy. Implementując logikę sprawdzania w funkcji przechowywanej, możesz użyć wyjątku, aby wyemitować bardziej pomocną wiadomość tekstową. Ponadto wyrażenia ograniczające check nie mogą zawierać podzapytań ani odwoływać się do zmiennych innych niż kolumny bieżącego wiersza lub inne tabele bazy danych.

Odrzućmy więc ograniczenia sprawdzające

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

i zajmij się wyzwalaczami i przechowywanymi funkcjami.

Pokaż mi więcej kodu

Mamy stolik. Przechodząc do funkcji DDL, definiujemy funkcję o pustym ciele, którą możemy później wypełnić określonym kodem:

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

To pozwala nam w końcu dotrzeć do wyzwalacza DDL łączącego tabelę i funkcję, dzięki czemu możemy zrobić kilka przykładów:

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

PostgreSQL umożliwia pisanie funkcji przechowywanych w wielu różnych językach. W tym przypadku iw poniższych przykładach komponujemy funkcje w języku PL/pgSQL, który został zaprojektowany specjalnie dla PostgreSQL i obsługuje wszystkie typy danych, operatory i funkcje PostgreSQL RDBMS. Opcja SET SCHEMA ustawia ścieżkę wyszukiwania schematu, która będzie używana na czas wykonywania funkcji. Ustawienie ścieżki wyszukiwania dla każdej funkcji jest dobrą praktyką, ponieważ pozwala uniknąć przedrostków obiektów bazy danych nazwą schematu i chroni przed pewnymi lukami związanymi ze ścieżką wyszukiwania.

PRZYKŁAD 0 — Weryfikacja danych

Jako pierwszy przykład zaimplementujmy wcześniejsze kontrole, ale z bardziej przyjaznym dla człowieka komunikatem.

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

Kwalifikator „NOWY” jest odniesieniem do wiersza danych, które mają zostać wstawione. Jest to jedna z wielu specjalnych zmiennych dostępnych w funkcji wyzwalacza. Poniżej przedstawimy kilka innych. Zauważ również, że PostgreSQL pozwala na zastąpienie pojedynczych cudzysłowów, które ograniczają treść funkcji innymi ogranicznikami, w tym przypadku zgodnie ze wspólną konwencją używania podwójnych znaków dolara jako ogranicznika, ponieważ sama treść funkcji zawiera pojedyncze znaki cudzysłowu. Funkcje wyzwalające muszą zostać zakończone, zwracając albo NOWY wiersz, który ma zostać wstawiony, albo NULL, aby dyskretnie przerwać akcję.

Te same próby wstawienia kończą się niepowodzeniem zgodnie z oczekiwaniami, ale teraz z przyjaznym komunikatem:

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.

PRZYKŁAD 1 — Rejestrowanie audytu

Dzięki przechowywanym funkcjom mamy dużą swobodę co do tego, co robi wywoływany kod, w tym odwoływanie się do innych tabel (co nie jest możliwe w przypadku ograniczeń sprawdzających). Jako bardziej złożony przykład omówimy implementację tabeli audytu, czyli utrzymywanie w osobnej tabeli rekordu wstawiania, aktualizowania i usuwania w tabeli głównej. Tabela kontroli zazwyczaj zawiera te same atrybuty co tabela Principal, które są używane do rejestrowania zmienionych wartości, a także dodatkowe atrybuty do rejestrowania operacji wykonanej w celu dokonania zmiany, a także znacznik czasu transakcji oraz rejestr użytkownika dokonującego zmiana:

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

W tym przypadku wdrożenie audytu jest bardzo łatwe, po prostu modyfikujemy istniejącą funkcję wyzwalacza, aby zawierała DML w celu wstawienia tabeli audytu, a następnie ponownie definiujemy wyzwalacz, aby uruchamiał się zarówno przy aktualizacjach, jak i wstawieniach. Zwróć uwagę, że zdecydowaliśmy się nie zmieniać sufiksu nazwy funkcji wyzwalacza na „biut”, ale jeśli funkcja audytu byłaby znanym wymogiem w początkowym czasie projektowania, byłaby użyta nazwa:

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

Zwróć uwagę, że wprowadziliśmy inną specjalną zmienną „TG_OP”, którą system ustawia w celu identyfikacji operacji DML, która uruchomiła wyzwalacz, odpowiednio jako „INSERT”, „UPDATE”, „DELETE” lub „TRUNCATE”.

Musimy obsługiwać usuwanie niezależnie od wstawiania i aktualizacji, ponieważ testy sprawdzania poprawności atrybutów są zbędne, a specjalna wartość NEW nie jest zdefiniowana przy wejściu do przed usunięciem funkcję wyzwalacza, a więc zdefiniuj odpowiednią zapisaną funkcję i wyzwalacz:

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

Zwróć uwagę na użycie wartości specjalnej OLD jako odniesienia do wiersza, który ma zostać usunięty, tj. wiersza takiego, jaki istnieje przed nastąpi usunięcie.

Wykonujemy kilka wstawek, aby przetestować funkcjonalność i potwierdzić, że tabela kontroli zawiera zapis wstawek:

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)

Następnie dokonujemy aktualizacji jednego wiersza i potwierdzamy, że tabela audytu zawiera rekord zmiany dodając drugie imię do jednej z wyświetlanych nazw rekordów danych:

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)

Na koniec korzystamy z funkcji usuwania i potwierdzamy, że tabela kontroli zawiera również ten rekord:

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)

PRZYKŁAD 2 – Wartości pochodne

Pójdźmy o krok dalej i wyobraź sobie, że chcemy przechowywać dowolny dokument tekstowy w każdym wierszu, powiedzmy życiorys w formacie zwykłego tekstu, referat z konferencji lub streszczenie postaci rozrywkowej, i chcemy wspierać korzystanie z zaawansowanego wyszukiwania pełnotekstowego możliwości PostgreSQL w tych dowolnych dokumentach tekstowych.

Najpierw dodajemy dwa atrybuty do obsługi przechowywania dokumentu i skojarzonego wektora wyszukiwania tekstu do głównej tabeli. Ponieważ wektor wyszukiwania tekstu jest wyprowadzany na podstawie wiersza, nie ma sensu przechowywać go w tabeli audytu, nawet jeśli dodamy kolumnę przechowywania dokumentów do powiązanej tabeli audytu:

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

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Następnie modyfikujemy funkcję wyzwalacza, aby przetworzyła te nowe atrybuty. Kolumna zwykłego tekstu jest obsługiwana w taki sam sposób, jak inne dane wprowadzone przez użytkownika, ale wektor wyszukiwania tekstu jest wartością pochodną i dlatego jest obsługiwany przez wywołanie funkcji, która redukuje tekst dokumentu do typu danych tsvector w celu wydajnego wyszukiwania.

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

W ramach testu aktualizujemy istniejący wiersz o szczegółowy tekst z Wikipedii:

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 następnie potwierdź, że przetwarzanie wektora wyszukiwania tekstu powiodło się:

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)

PRZYKŁAD 3 – Wyzwalacze i widoki

Pochodny wektor wyszukiwania tekstu z powyższego przykładu nie jest przeznaczony do spożycia przez ludzi, tj. nie jest wprowadzany przez użytkownika i nigdy nie oczekujemy przedstawienia wartości użytkownikowi końcowemu. Jeśli użytkownik spróbuje wstawić wartość do kolumny ts_abstract, wszystko, co zostanie podane, zostanie odrzucone i zastąpione wartością wyprowadzoną wewnętrznie z funkcji wyzwalacza, dzięki czemu mamy ochronę przed zatruciem korpusu wyszukiwania. Aby całkowicie ukryć kolumnę, możemy zdefiniować skrócony widok, który nie zawiera tego atrybutu, ale nadal uzyskujemy korzyść z aktywności wyzwalacza w tabeli bazowej:

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

Dla uproszczenia, PostgreSQL automatycznie czyni go zapisywalnym, więc nie musimy robić nic więcej, aby pomyślnie wstawić lub zaktualizować dane. Kiedy DML zaczyna działać w tabeli bazowej, wyzwalacze aktywują się tak, jakby instrukcja została zastosowana bezpośrednio do tabeli, więc nadal otrzymujemy zarówno obsługę wyszukiwania tekstu wykonywanego w tle, wypełniając kolumnę wektora wyszukiwania tabeli osoba, jak i dołączenie zmień informacje w tabeli audytu:

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)

W przypadku bardziej skomplikowanych widoków, które nie spełniają wymagań automatycznego zapisu, użyj systemu reguł lub zamiast wyzwalacze mogą wykonać zadanie, obsługując zapisy i usuwanie.

PRZYKŁAD 4 – Wartości podsumowujące

Upiększmy dalej i potraktujmy scenariusz, w którym istnieje jakiś rodzaj tabeli transakcyjnej. Może to być rejestr przepracowanych godzin, uzupełnienia zapasów i zmniejszenia zapasów magazynowych lub detalicznych, a może rejestr czeków z obciążeniami i kredytami dla każdej osoby:

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

I załóżmy, że chociaż ważne jest zachowanie historii transakcji, reguły biznesowe wiążą się z wykorzystaniem salda netto w przetwarzaniu aplikacji, a nie jakichkolwiek szczegółów transakcji. Aby uniknąć konieczności częstego przeliczania salda poprzez sumowanie wszystkich transakcji za każdym razem, gdy potrzebne jest saldo, możemy zdenormalizować i zachować bieżącą wartość salda w tabeli osoby, dodając nową kolumnę i używając wyzwalacza i funkcji przechowywanej w celu utrzymania saldo netto w miarę wstawiania transakcji:

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

Może wydawać się dziwne, aby wykonać aktualizację najpierw w zapisanej funkcji przed sprawdzeniem nieujemności wartości debetu, kredytu i salda, ale pod względem walidacji danych kolejność nie ma znaczenia, ponieważ treść funkcji wyzwalacza jest wykonywana jako transakcja bazy danych, więc jeśli te sprawdzenia walidacji nie powiodą się, cała transakcja zostanie wycofana po zgłoszeniu wyjątku. Zaletą wykonania aktualizacji w pierwszej kolejności jest to, że aktualizacja blokuje dany wiersz na czas trwania transakcji, więc każda inna sesja próbująca zaktualizować ten sam wiersz jest blokowana do czasu zakończenia bieżącej transakcji. Dalszy test weryfikacyjny zapewnia, że ​​wynikowe saldo jest nieujemne, a komunikat z informacją o wyjątku może zawierać zmienną, która w tym przypadku zwróci nieprawidłową próbę wstawienia wiersza transakcji do debugowania.

Aby zademonstrować, że to faktycznie działa, oto kilka przykładowych wpisów i czek pokazujący zaktualizowane saldo na każdym 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")

Zwróć uwagę, jak powyższa transakcja kończy się niepowodzeniem przy niewystarczających środkach, tj. Spowoduje to ujemne saldo i pomyślnie wycofa się. Zwróć również uwagę, że zwróciliśmy cały wiersz ze specjalną zmienną NEW jako dodatkowy szczegół w komunikacie o błędzie do debugowania.

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)

PRZYKŁAD 5 — Redux wyzwalaczy i widoków

Istnieje jednak problem z powyższą implementacją, a mianowicie, że nic nie stoi na przeszkodzie, aby złośliwy użytkownik wydrukował pieniądze:

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;

Na razie wycofaliśmy powyższą kradzież i pokażemy sposób na budowanie ochrony przed użyciem wyzwalacza w widoku, aby zapobiec aktualizacjom wartości salda.

Najpierw powiększamy skrócony widok z poprzedniego, aby odsłonić kolumnę równowagi:

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

To oczywiście umożliwia dostęp do odczytu bilansu, ale nadal nie rozwiązuje problemu, ponieważ dla prostych widoków, takich jak ten, opartych na pojedynczej tabeli, PostgreSQL automatycznie umożliwia zapis widoku:

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;

Moglibyśmy użyć reguły, ale aby zilustrować, że wyzwalacze można zdefiniować zarówno w widokach, jak i tabelach, wybierzemy tę drugą drogę i użyjemy zamiast aktualizacji 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.

Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument

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

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR:  permission denied for relation person
CONTEXT:  SQL statement "UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement

The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:

RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
 dfunny     | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
(4 rows)

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $3,686.19
(1 row)

Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.

Wniosek

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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Agregacja chmur punktów współrzędnych (x,y) w PostgreSQL

  2. Zarządzaj łączeniem połączeń w aplikacji internetowej dla wielu dzierżawców za pomocą Spring, Hibernate i C3P0

  3. Dlaczego nie mogę wykluczyć kolumn zależnych z `GROUP BY` podczas agregacji według klucza?

  4. Głowy w chmurze na CHAR(10)

  5. Zabij sesję/połączenie postgresql