PL/pgSQL - Triggery

Ciele

  1. Precvičiť si tvorbu triggerov.
  2. Vyskúšať si implementáciu editovateľnosti inherentne needitovateľných pohľadov.

Úvod

Dnesne cvičenie je venované trigger-om (hrubo preložené ako spúšťač). Trigger v PostgreSQL je funkcia, ktorá je vyvolaná vždy ak dôjde k udalosti ako insert, uptade, delete alebo truncate. Je to špeciálna funkcia asociovaná s tabuľkou. Hlavný rozdiel medzi funkciou a triggerom je, že trigger sa automatický vyvolá pri danej situácií.

V tomto týždni máte možnosť absolvovať opravný zápočtový test. Upozorňujem, že pri hodnotení sa ráta posledný absolvovaný pokus.

Postup

Krok 1

Pre vytvorenie triggera je potrebné najprv vytvoriť "trigger funkciu". Takáto funkcia sa vytvára ako regulárna funkciu, avšak nesmie brať žiadne parametre a musí vrátiť hodnotu typu TRIGGER. Zjednodušená syntax trigger funkcie:

CREATE FUNCTION trigger_function() 
   RETURNS TRIGGER 
   LANGUAGE PLPGSQL
AS $$
BEGIN
   -- trigger logic
END;
$$

Trigger funkciu je možne vytvoriť v akomkoľvek jazyku podporovanom v PostgreSQL. My pokračujeme v PL/pgSQL.

Samotný trigger je možné vytvoriť pomocou príkazu CREATE TRIGGER. Zjednodušená syntax príkazu CREATE TRIGGER:

CREATE TRIGGER trigger_name 
   {BEFORE | AFTER | INSTEAD OF} { event }
   ON table_name
   [FOR [EACH] { ROW | STATEMENT }]
       EXECUTE PROCEDURE trigger_function

V tejto syntaxi:

  1. Zadajte názov triggera po kľúčovom slove TRIGGER.
  2. Určte časovanie, ktoré spôsobí spustenie triggera. Môže to byť pred BEFORE alebo po AFTER udalosti, ktorá vyvolala trigger. Kľučové slovo INSTEAD OF sa využíva pre trigger nad pohľadom.
  3. Zadajte udalosť, ktorá vyvolá trigger. Udalosť môže byť INSERT , DELETE, UPDATE alebo TRUNCATE.
  4. Po kľúčovom slove ON zadajte názov tabuľky asociovanej s triggerom.
  5. Špecifikujte typ triggera, ktorý môže byť:
    1. FOR EACH ROW z názvu vypovedá, tento typ triggera sa spustí pri vyvolaní zmeny na každom riadku (v prípade triggera nad tabuľkou, jedina dovolená možnosť),
    2. FOR EACH STATEMENT, tento typ triggera sa spustí raz, bez ohľadu na to koľko riadkou akcia ktorá spustila trigger ovplyvňuje.
  6. Na záver zadajte meno trigger funkcie po kľúčovom slove EXECUTE PROCEDURE.

Na siedmom cvičení ste v jednej z úloh mali získať zoznam umelcov spolu so skladateľmi a to bez duplikátov, usporiadaný podľa abecedy. Nasledujúci blok kódu vytvára pohľad, ktorý ukazuje práve takýto zoznam:

CREATE OR REPLACE VIEW allArtists AS
SELECT name
FROM   artist
UNION
SELECT composer
FROM   track
WHERE  composer IS NOT NULL
ORDER BY name;

Úloha 1.1

Vytvorte daný pohľad vo svojej databáze.

Pretože tento pohľad používa zjednotenie (UNION) a usporiadanie (ORDER BY), takýto pohľad nie je editovateľný (neumožňuje operácie INSERT/UPDATE/DELETE). Napríklad pokúste sa zmeniť umelca 'AC/DC' na 'Le Matos'. V ďalšom kroku sa z tohto pohľadu pokúsime spraviť editovateľný pohľad.

Najdôležitejšia a zároveň najťažšia časť vytvorenia triggera na editovateľnosť pohľadu je nájsť správny a vhodný "preklad" DML operácie nad pohľadom do tabuliek, ktoré pohľad využíva. V kontexte aktuálnej úlohy, ako sa zmena mena umelca 'AC/DC' na meno 'Le Matos' prejaví na tabuľkách Artist a Track, z ktorých čerpá pohľad údaje?

Úloha 1.2

Napíšte trigger, ktorý umožní UPDATE nad pohľadom allArtists.

Poznámka

Trigger bude INSTEAD OF, čím nahradí update nad pohľadom nejakou akciou nad tabuľkami, z ktorých pohľad vychádza.

Poznámka

Zmenu mena umelca z pohľadu budeme interpretovať tak, že sa má podľa toho zmeniť príslušné meno umelca aj v tabuľke artist aj v tabuľke composer (ak napr. 'AC/DC' je interpretom a zároveň skladateľom, jeho update cez pohľad allArtists na hodnotu 'Le Matos' bude znamenať, že sa tak aktualizuje aj jeho meno v tabuľke artist, ale aj všetky jeho výskyty v tabuľke Track v stĺpci composer).

Úloha 1.3

Napíšte nový trigger, ktorý umožní INSERT nad pohľadom allArtists.

Poznámka

Vloženie nového umelca budeme interpretovať, ako vloženie nového interpreta. To znamená, že ak sa pokúsi používateľ vložiť nové meno medzi umelcov, vložíme do tabuľky artist nový záznam s daným menom. artistid nového interpreta bude najvyššie aktuálne artistid zvýšené o jedna.

Úloha 1.4

Overte si správnosť implementácie oboch triggerov pridaním nového záznamu (INSERT) a aktualizáciou existujúceho (UPDATE).

Krok 2

V tomto kroku upravíme implementácie predchádzajúcich triggerov tak, aby zachovávali konzistenciu pohľadu allArtist. Ak sa totiž vloží nový záznam s menom umelca, aký už existuje, vzhľadom na pohľad allArtist sa stav pohľadu nezmení - UNION totiž odstráni duplikáty. Napriek tomu v tabuľke artist vznikne nový záznam.

Úloha 2.1

Upravte trigger umožňujúci INSERT nad pohľadom allArtist tak, aby sa vykonal iba vtedy, ak v pohľade allArtist neexistuje umelec s menom, aké sa používateľ snaží do pohľadu vložiť.

Poznámka

Napr. v databáze a teda aj v pohľade už je umelec s menom 'AC/DC'. Ak sa teda v takom stave pokúsi používateľ do pohľadu vložiť nový záznam s menom 'AC/DC', vyhoďte vlastnú používateľskú výnimku a INSERT nevykonajte (napr. 'nepovolená operácia').

Poznámka

Asi najjednoduchší spôsob ako zistiť, či tam už umelec s takým menom je alebo nie je, je deklarovať si číselnú premennú a do nej načítať počet umelcov v pohľade allArtist, ktorých meno je rovnaké ako meno práve vkladaného umelca. Ak je počet väčší ako 0, znamená to, že taký umelec tam už je, a je preto potrebné vyhodiť výnimku.

Rovnaký problém ako pri INSERT môže nastať aj pri UPDATE - v prípade pokusu zmeniť meno jedného umelca na také meno, aké tam už je. Umožnenie takej zmeny by sa navonok javilo ako zmazanie daného umelca z databázy, pretože by sa po zmene považoval za duplikát a vo výsledku by sa teda objavil len raz.

Úloha 2.2

Upravte trigger pre UPDATE nad pohľadom allArtists tak, aby v prípade, že nové meno umelca je v pohľade už obsadené, sa neudialo nič.

Poznámka

Pre takúto úpravu môžete opäť použiť logiku ako pri predchádzajúcej úlohe, tzn. najprv si spočítať, koľkokrát je také meno už použité, a upraviť meno iba vtedy, ak také meno ešte použité nie je.

Opäť si otestujte správnosť implementácie vhodnými INSERT a UPDATE príkazmi. Aj keď umožnenie DELETE príkazov by nebolo oveľa náročnejšie, to už necháme ako doplňujúcu úlohu.

Krok 3

V tomto ďalšom kroku už bude úloha náročnejšia. Povedzme, že pri vytvorení nového zákazníka mu chceme v rámci zákazníckej podpory automaticky priradiť nejakého zamestnanca obchodu. Vnútorná politika firmy je taká, že rozdelenie zákazníkov pre jednotlivých zamestnancov v roli "Sales Support Agent" má byť rovnomerné (spravodlivo rozdelené).

Úloha 3.1

Napíšte trigger, ktorý pri vložení nového zákazníka do databázy nastaví jeho supportRepId na id toho pracovníka zákazníckej podpory, ktorý práve zastupuje najmenej zákazníkov. Ak je tých, čo zastupujú najmenej zákazníkov, viac, vyberte jedného náhodne.

Poznámka

Zo zadania vyplýva, že to musí byť jeden zo zamestnancov s titulom "Sales Support Agent", a zo všetkých tých zamestnancov, ktorí majú daný titul, musí reprezentovať najmenej zákazníkov (tzn. v prípade pridania nového agenta zákazníckej podpory, trigger bude priraďovať nových zamestnancov práve jemu). Samotný trigger nie je veľmi zložitý, je však potrebné napísať správny select, ktorý vráti id práve toho zamestnanca, ktorý spĺňa uvedené podmienky. Odporúčam začať selectom, ktorý vráti takého zamestnanca (resp. stačí jeho id).

Zdroje

  1. PostgreSQL tutorial - PostgreSQL Triggers
  2. PosgreSQL dokumentácia

Doplňujúce úlohy

Úloha A.1

Chinook

Napíšte trigger umožňujúci zmazanie umelca z pohľadu allArtists.

Poznámka

Zmazanie budeme interpretovať nasledovne: ak v tabuľke Track existuje skladateľ (composer) s daným menom, daný composer v tabuľke Track sa nastaví na NULL. Ak existuje interpret v tabuľke Artist s daným menom, zmaže sa jeho záznam. Pozor, na tabuľku Artist sa môže odkazovať jeden alebo viacero albumov, najprv je teda potrebné zmazať všetky albumy odkazujúce sa na daného umelca (Artist). Aby nebolo nutné kaskádne mazať aj tracky v albume a potom položky na faktúre, pre všetky skladby (track), ktoré sa zasa odkazujú na album, nastavte cudzí kľúč albumid na hodnotu NULL (albumId v Track môže mať hodnotu NULL).