Archiv pro rubriku: Programování

Funkce a triggery v PostgreSQL

postgresql-triggerNásledující článek se bude hodit každému, kdo již má určité zkušenosti s SQL a rád by si vyzkoušel napsat vlastní funkci nebo trigger. Začneme velmi zvolna, nejprve si představíme, jak napsat jednoduchou funkci a následně se vrhneme na triggery.

Vlastní funkce v PostgreSQL

Funkce dělá přesně to, co její matematický protějšek. Pro vstupní hodnotu vrátí určitý údaj. My si nyní vytvoříme funkci, co nebude vyžadovat argument a vždy vrátí číslo jedna — to jen pro zahřátí, taková funkce samozřejmě není moc užitečná, ale alespoň se podíváme, jak vlastně vypadá její syntaxe.


CREATE FUNCTION test() RETURNS integer AS $$

SELECT 1 AS result;

$$ LANGUAGE SQL;

SELECT test(); --vypíše 1

Nejprve s pomocí CREATE FUNCTION vymezíme, že začínáme psát funkci. Poté uvedeme její název (zde test) a prázdnou závorku, protože nepřebíráme žádné argumenty. Databázový server zajímá, jaký datový typ bude naše funkce vracet, proto jej uvedeme (integer). Za klíčovým slovem AS začíná vymezení samotného těla funkce, které z obou strany obklopuje symbol dvou dolarů ($$). Závěrem uvedeme použitý jazyk (LANGUAGE SQL) a nezapomeneme na středník, podobně jako v těle funkce, ve kterém jednoduše s pomocí příkazu SELECT získáme číselnou hodnotu 1.

Pojďme zkusit něco užitečnějšího, naučíme se používat argumenty:


CREATE FUNCTION plus(integer, integer) RETURNS integer AS $$
 SELECT $1 + $2 AS result;
$$ LANGUAGE SQL;

SELECT plus(10, 5); --vypíše 15

SELECT plus(5, 'a'); --Error in query: ERROR: invalid input syntax for integer: "a"

Tato funkce umí sečíst dvě čísla. U argumentů uvádíme jen datový typ a na hodnotu se odkazujeme s pomocí dolaru a indexu (první argument je dostupný s pomocí $1).

Než se vhrneme na náš první trigger, smažeme z databáze vytvořené funkce. Udělá se to následovně:


DROP FUNCTION test();

DROP FUNCTION plus(integer, integer);

Jednoduchý trigger v PostgreSQL

Na úvod si představme, že máme tabulku human  obsahující údaje o lidech. Uchováváme jedinečné ID, jméno, příjmení a oslovení. Napíšeme si trigger, který nám bude zmíněné oslovení generovat automaticky. Nejprve vytvoříme samotnou funkci, kterou server následně automaticky zavolá. Daná funkce nebude dělat nic jiného, než že vezme jméno i příjmení a spojí oba údaje mezerou.

tabulka_human


CREATE FUNCTION concatenate() returns trigger AS $$
BEGIN
UPDATE human SET salutation = (NEW.name || ' ' || NEW.surname) WHERE id = NEW.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

Oproti zápisu funkce je zde několik rozdílů. Trigger nikdy nemá argumenty a jako návratový typ uvádíme slovo trigger. Do těla se nám dostala část BEGIN a END; (pozor na středník). Uvnitř těla smíme používat proměnnou NEW, která odkazuje na nově vytvořený/upravený řádek. Příkazem RETURN vracíme výsledek funkce (zde je NULL, protože nic vracet nechceme). Klauzule LANGUAGE obsahuje plpgsql, protože píšeme trigger pro PostgreSQL.

Pojďme nyní naší tabulku donutit, ať trigger volá automaticky při každém vložení hodnoty:


CREATE TRIGGER human_trigger AFTER INSERT ON human
FOR EACH ROW EXECUTE PROCEDURE concatenate();

Zápis je hodně upovídaný a osobně se mi špatně pamatuje. Zase se naopak dobře čte :-). Uvádíme pojmenování našeho triggeru (human_trigger), název tabulky (human) a samotnou funkci, co bude volána (concatenate());

A nyní pozor, stačí nám spustit následující příkaz a v tabulce objevíme automaticky vygenerované oslovení:


INSERT INTO human(name, surname, salutation) VALUES ('Jan', 'Novák', '?');

Sloupec salutation nedovoluje hodnoty NULL, proto musíme nějakou uvést, i když bude následně přepsána triggerem. To je nepříjmené, pojďme si náš trigger vylepšit! Funkci nebudeme volat po zápisu řádku, ale před zápisem (a v tuto chvíli si vygenerujeme oslovení).


CREATE FUNCTION concatenate() returns trigger AS $$
BEGIN
NEW.salutation := NEW.name || ' ' || NEW.surname;
return new;
END;
$$ LANGUAGE plpgsql;

S pomocí proměnné NEW přiřadíme oslovení. Hotovu funkci poté budeme volat před provedením INSERT nebo v případě UPDATE:


CREATE TRIGGER human_trigger BEFORE INSERT OR UPDATE ON human
 FOR EACH ROW EXECUTE PROCEDURE concatenate();

Vlastnosti typových systémů

vlastnosti-typovych-systemuK čemu v programování slouží typový systém? Jde o nástroj k porovnávání datových typů jednotlivých proměnných, které se provádí před operací v libovolném výrazu. Díky typovému systému zajistíme, že nedojde k chybě spočívající v kombinaci špatných datových typů (například když se pokusíme sečíst číslo a textový řetězec).

Silně a slabě typované jazyky

Typový systém může na problematiku nahlížet dvěma způsoby. Silně typované programovací jazyky striktně vyžadují pouze předem vymezené datové typy. Každá operace v takovém jazyce má proto určené jaké datové typy argumentů akceptuje. Do této kategorie patří např. Java nebo C.

Naopak tomu slabě typovaný jazyk se snaží operaci provést za každou cenu a proto přistupuje k přetypování. Vraťme se k příkladu z úvodu. Součet čísla a textového řetězce zdá se nedává smysl, ovšem co když v textu máme uložené opět číslo? Pak jazyku nic nebrání v tom, aby provedl přetypování textu na číslo a vypočítal výsledný součet. Ač se popsané chování může jevit jako jednoznačná výhoda, záleží na úhlu pohledu, někdy mohou díky této slabé kontrole datových typů vznikat hůře odhalitelné chyby. Popsaným způsobem se chová např. PHP nebo JavaScript.

Staticky a dynamicky typované jazyky

Programovací jazyk, který datové typy proměnných zjišťuje staticky, provádí vstupní analýzu kódu, během které je schopný odhalit datový typ každé proměnné. Napomáhá tomu mimo jiné to, že datový typ je zpravidla nutno deklarovat. Díky tomu pak překladač může lépe optimalizovat vytvářený kód (přesně totiž ví, s čím přichází do styku). Uvedené informace se vztahují k většině dnešních programovacích jazyků.

Existuje ovšem i jiný přístup. Co kdybychom datový typ zjistili až za běhu programu? Pak může jedna proměnná obsahovat několik naprosto rozdílných hodnot. Nemluvíme zde proto o typu proměnné (to platí pro staticky typované jazyky) ale o typu hodnoty. S uvedeným přístupem přichází např. jazyk Perl.

Bezpečně a nebezpečně typované jazyky

U bezpečně typovaných jazyků máme zajištěno, že pokud je operace proveditelná (tedy je splněna náležitost v podobě správných argumentů), tak vždy bude operace úspěšně dokončena. Nestane se zkrátka, že kvůli prováděné operaci aplikace spadne.

Naopak tomu jako už název prozrazuje nebezpečně typované jazyky dávají programátorovi šanci dostat výpočetní proces do nekonzistentního stavu. Díky existenci pointeru je poměrně jednoduché tohoto cíle dosáhnout v jazyce C nebo C++. Můžeme totiž různě přesouvat obsahy proměnných i obměňovat místa, kam v paměti ukazujeme.

void main()
{
    int *x = 0; //nulový ukazatel (NULL pointer)
    *x = 42; //chyba, na toto nulové místo se snažíme uložit hodnotu
}

Lexikální a dynamický rozsah platnosti proměnné

Až budete příště pracovat ve svém oblíbeném programovacím jazyku, pozastavte se na chvíli nad tím, že principy, které tak již bezpečně dobře znáte, nemusí platit všude. Řeč bude na následujících řádcích o rozsahu platnosti proměnné. Uvažovat lze přitom dva modely — lexikální a dynamický rozsah.

Lexikální rozsah platnosti proměnné

Následujcí chování vám bude dobře známé. Používá jej totiž většina soudobých programovacích jazyků. Pokud se v nich rozhodnete aplikovat libovolnou proceduru obsahující ve svém těle proměnou neinicializovanou v lokálním prostředí této procedury, hledá se hodnota (vazba) proměnné v prostředí vzniku procedury. A co si představit pod pojmem prostředí vzniku procedury? Jde o prostředí nadřazené tomu lokálnímu. Důsledkem toho pokud v proceduře použijeme proměnnou, jejíž hodnota není v těle inicializována, je nutno vazbu hledat výše, kde s největší pravděpodobností narazíme na příslušnou globální proměnnou a z ní hodnotu vyčteme. Pokud by zde náhodou taková proměnná nebyla, nezbude než nahlásit chybu — proměnná totiž neexistuje.

Abychom to lépe pochopili, podívejme se nyní společně na komentovaný příklad:

(define test
  (lambda (y)
    (lambda (x)
      (+ y x))))

(define y 100);v globálním prostředí máme y = 100

(define f (test 20));vzniká procedura, nyní máme y = 20
(f 10);aplikace procedury, za x dosadíme 10 a spočítáme 20 + 10 = 30

;procedura ví, že má sečíst y a x
;hodnotu pro x zná (je to 10), tato hodnota byla předána v argumentu
;nyní je třeba zjistit hodnotu pro y, ta se totiž v lokálním prostředí nevyskytuje
;procedura tedy začíná hledat v "prostředí vzniku procedury", to je to prostředí, které je nadřazené tomu aktuálnímu
;v tomto nadřazeném prostředí je nalezeno kýžené y (s hodnotou 20)
;procedura sečte 20 + 10 a vrátí 30
;hodnota y = 100 v globálním prostředí je v tomto případě směle ignorována

Dynamický rozsah platnosti

A teď si představme, že by se hodnoty proměnných hledaly úplně jinak, konkrétně v prostředí aplikace procedury. Nutno podotknout, že tuto metodu téměř nikdo nepoužívá, ale existují výjimky. Než se podíváme na příklad, řekněme si, jaká je hlavní potíž tohoto přístupu — hodnoty proměnných zjistíme až za běhu programu, čímž stoupá časová náročnost ladění a hledání chyb.

(define test
  (lambda (y)
    (lambda (x)
      (+ y x))))

(define y 100);v globálním prostředí máme y = 100

(define f (test 20));vzniká procedura, nyní máme y = 20
(f 10);aplikace procedury, za x dosadíme 10 a spočítáme 100 + 10 = 110

;procedura ví, že má sečíst y a x
;hodnotu pro x zná (je to 10), tato hodnota byla předána v argumentu
;nyní je třeba zjistit hodnotu pro y, ta se totiž v lokálním prostředí nevyskytuje
;procedura tedy začíná hledat v "prostředí aplikace procedury"
;v tomto aktuálním prostředí se y = 100
;procedura sečte 100 + 10 a vrátí 110