Ciele
- Vytvoriť vlastnú PL/pgSQL funkciu a storovanú procedúru.
- Použiť lokálne premenné a vyskúšať si ich naplnenie použitím SELECT ... INTO ...
- Použitie cyklu, podmienok.
- Práca s kurzormi.
Úvod
Dnešné cvičenie je venované rozšíreniu jazyka SQL o možnosť tvorby imperatívnych procedúr a funkcií. V PostgreSQL je takéto rozšírenie implementované ako jazyk PL/pgSQL.
PL/pgSQL procedurálny jazyk rozširuje štandardný SQL o mnohé procedurálne prvky ako napríklad: riadiace štruktúry, cykly, alebo možnosť komplexných výpočtov. Umožňuje vyvíjať aj komplexné funkcie a storované procedúry v PostgreSQL, ktoré nie je možné napísať v čistom SQL.
Hlavným rozdielom medzi funkciou a storovanou procedúrov v PostgreSQL je, že funkcia vracia výsledok, pričom storovaná procedúra nie. Zámerom takejto implementácie je vykonať určitý druh činnosti, dokončiť činnosť a vrátiť volajúcemu kontrolu. Do verzie PostgreSQL 11, boli storované procedúry implementované ako funkcie, ktoré nevracali údaje. V aktuálnej verzii existuje spôsob, ako priamo deklarovať storované procedúry, ktorých výhodou je tiež to, že sú schopné vo svojom kóde začať novú transakciu.
Poznámka
V PostgreSQL je možné využiť viaceré jazyky (PL/Tcl, PL/Perl a PL/Python) na definovanie funkcií a procedúr avšak je potrebné ich nainštalovať. V rámci cvičení budeme pracovať z jazykom PL/pgSQL, ktorý je v PostgreSQL predinštalovaný.
Postup
Krok 1
Pracujeme na databáze Movies s nasledovnou schémou.
Syntax funkcií v jazyku PL/pgSQL je nasledovná:
CREATE [OR REPLACE] FUNCTION functionName(someParameter 'parameterType')
RETURNS 'DATATYPE'
AS $_block_name_$
DECLARE
--declare variables
BEGIN
--do something
--return something
END;
$_block_name_$
LANGUAGE plpgsql;
Volanie funkcie vykonáte pomocou príkazu:
SELECT functionName(someParameter);
Poznámka
V nástroji DBeaver vytovríte funkciu pravým tačidlom myši kliknutím na Functions -> Create New Function v záložke Database Navigator nasledovane:
Úloha 1.1
Vytvorte funkciu, ktorá vypíše 1 riadok a 1 stĺpec s názvom greet a hodnotou "hello world!".
Riešenie
CREATE OR REPLACE FUNCTION greet()
RETURNS text
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN 'hello world!';
END;
$function$
;
Úloha 1.2
Vytvorte funkciu, ktorá vypíše 1 riadok a 1 stĺpec s názvom text a hodnotou "hello meno!". Meno nech je vstupný parameter.
Riešenie
CREATE OR REPLACE FUNCTION greet_name(name text)
RETURNS text
LANGUAGE plpgsql
AS $function$
begin
return 'hello ' || name || '!';
END;
$function$
;
Úloha 1.3
Napíšte dopyt, ktorý vypíše celú tabuľku c_city. Posledný stĺpec vo výsledku nech je stĺpec s hodnotou "hello mesto!". Využite funkciu vytvorenú v predchádzajúcej úlohe. Názvy miest nech sú z tabuľky c_city (stĺpec cit_name).
Riešenie
select *, greet_name(cit_name) from c_city cc;
Krok 2
Úloha 2.1
Napíšte funkciu slúžiacu na INSERT dát do tabuľky c_city. Funkcia na vkladanie údajov nech má 2 vstupné a 1 výstupný parameter. Vstupné parametre sú: názov mesta (cit_name) a krajina (xcit_cou_id), v ktorej sa mesto nachádza. Výstupný parameter nech je id novovytvoreného mesta (čiže integer).
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_c_city_ins(cit_name text, xcit_cou_id text, out cit_id integer)
RETURNS int4
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
WITH vlozene AS (
INSERT INTO c_city(
cit_name, xcit_cou_id
)
VALUES (
fn_c_city_ins.cit_name, fn_c_city_ins.xcit_cou_id
)
RETURNING
c_city.cit_id
)
SELECT
vlozene.cit_id
INTO
fn_c_city_ins.cit_id
FROM
vlozene
;
RETURN;
END;
$function$
;
Úloha 2.2
Napíšte procedúru slúžiacu na UPDATE dát v tabuľke c_city. Procedúra na zmenu údajov nech má 4 vstupné parametre. Vstupné parametre sú: id mesta, ktorého atribúty chceme zmeniť (cit_id), nový názov mesta, nový názov krajiny a meno používateľa, ktorý záznam modifikuje. Využitím COALESCE ošetrite prípad, aby stĺpec cit_modified_by nemal hodnotu NULL, ale aby sa v takom prípade doplnila hodnota systémovej funkcie session_user. Takisto doplnite do stĺpca cit_modified aktuálny čas a dátum príslušnou funkciou.
Riešenie
CREATE OR REPLACE PROCEDURE movies.pr_c_city_upd(cit_id integer, cit_name text, xcit_cou_id text, cit_modified_by text DEFAULT NULL::text)
LANGUAGE plpgsql
AS $procedure$
begin
update c_city
set cit_name = pr_c_city_upd.cit_name,
xcit_cou_id = pr_c_city_upd.xcit_cou_id,
cit_modified_by = coalesce(pr_c_city_upd.cit_modified_by, "session_user"()::text),
cit_modified = now()
where c_city.cit_id = pr_c_city_upd.cit_id
;
END;
$procedure$
;
Úloha 2.3
Napíšte procedúru slúžiacu na DELETE dát v tabuľke c_city. Procedúra na zmazanie údajov nech má 1 vstupný parameter. Vstupný parameter je: id mesta, ktoré chceme vymazať (cit_id).
Riešenie
CREATE OR REPLACE PROCEDURE movies.pr_c_city_del(cit_id integer)
LANGUAGE plpgsql
AS $procedure$
begin
delete from c_city
where c_city.cit_id = pr_c_city_del.cit_id ;
END;
$procedure$
;
Úloha 2.4
Napíšte funkciu slúžiacu na SELECT dát z tabuľky c_city. Funkcia na select údajov nech má 1 vstupný parameter. Vstupný parameter je: podmienka, podľa ktorej chceme dáta vyselectovať. Funkcia nech vráti tabuľku c_city.
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_c_city_sel(podmienka text)
RETURNS TABLE(cit_id integer, cit_name character varying, cit_inserted_by text, cit_inserted timestamp without time zone, cit_modified_by text, cit_modified timestamp without time zone, xcit_cou_id char)
LANGUAGE plpgsql
AS $function$
begin
RETURN QUERY
execute
'SELECT
cit_id
, cit_name
, cit_inserted_by
, cit_inserted
, cit_modified_by
, cit_modified
, xcit_cou_id
FROM
c_city '::text
|| coalesce(' WHERE ' || fn_c_city_sel.podmienka , ''::text)
|| ' order by cit_name asc'::text
;
END;
$function$
;
Poznámka
V praxi sa aj volanie jednoduchých príkazov INSERT, UPDATE, DELETE, SELECT realizuje pomocou funkcií, z dôvodu optimalizácie a diagnostiky práce s databázou. Napríklad slúži na logovanie, kto spustil select alebo na doplnenie dátových kontrol pred samotným vykonaním funkcie. Taktiež na logovanie času trvania vykonávania funkcie. Ďalší dôvod vytvárania takýchto funkcií je potreba jasného ohraničenia (odelenia) medzi GUI aplikácie a databázou (žiadne SQL príkazy nie sú v GUI - v GUI sa nachádzajú iba volania funkcií).
Krok 3
Úloha 3.1
Vytvorte funkciu, ktorá vypíše názov dňa v týždni podľa jeho poradia v rámci týždňa (prvý nech je pondelok). Vstupný parameter je: číslo dňa. Vráťte názov dňa. Ošetrite nesprávny vstup (číslo menšie ako 1 a väčšie ako 7). Zadefinujte lokálnu premennú typu pole reťazcov na uloženie názvov dní, kde v poli reťazcov budú vymenované názvy dní v týždni.
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_den(cislo_dna integer)
RETURNS text
LANGUAGE plpgsql
AS $function$
DECLARE
dni varchar[]:= array ['Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota','Nedeľa'];
i int;
vystup varchar;
BEGIN
if (cislo_dna > 0 and cislo_dna < 8) then
for i in 1..7 loop
if i = cislo_dna then
vystup:=dni[i];
end if;
--raise notice 'den: %', dni[i];
end loop;
else
vystup:='Cislo mimo rozsah';
end if;
return vystup;
END;
$function$
;
Poznámka
Polia v PosgreSQL defaultne začínajú od 1.
Poznámka
Pri ladení funkcií pl/pgsql odporúčame používať výpis do konsoly cez RAISE NOTICE.
Poznámka
Ak má niekto z Vás skúsenosť s prácou s databázou Oracle, treba si dať pozor na vracanie hodnôt pomocou výstupných parametrov kvôli odlišnostiam v PostgreSQL. Na túto tému si odporúčam prečítať diskusiu.
Úloha 3.2
Vytvorte tabuľku s názvom t_log na logovanie udalostí v databáze. Nech ma stĺpce: log_id (primárny kľúč typu serial), log_event(text), log_time (timestamp s default hodnotou now()), log_user (text s default hodnotou session_user), log_value (float), log_object (text). Všetky stĺpce majú byť povinné.
Riešenie
-- DROP TABLE t_log;
CREATE TABLE t_log (
log_id serial NOT NULL,
log_event text NOT NULL,
log_time timestamp(0) NOT NULL DEFAULT now(),
log_user text NOT NULL DEFAULT SESSION_USER,
log_object text NOT NULL,
log_value float8 NOT NULL,
CONSTRAINT t_log_pk PRIMARY KEY (log_id)
);
Úloha 3.3
Vytvorte funkciu (napr. s názvom fn_t_log_ins) na logovanie dĺžky trvania vykonávania ľubovoľnej funkcie ( neskôr si vyberiete, na ktorej z už vytvorených funkcií si logovanie odskúšate). To znamená, že chceme vytvoriť funkciu fn_t_log_ins, ktorá bude neskôr volaná inou funkciou. Účelom fn_t_log_ins je spraviť INSERT do tabuľky t_log, s údajom o dĺžke vykonávania funckie, ktorá ju bude volať. Vstupné parametre sú: log_event, log_object a log_value. Výsledok zaznamenajte (insert) do tabuľky t_log. Výstupný parameter je id vytvoreného záznamu v tabuľke t_log.
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_t_log_ins(log_event text, log_object text, log_value real, OUT log_id integer)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
WITH vlozene AS (
INSERT INTO t_log (log_event, log_object , log_value )
VALUES ( fn_t_log_ins.log_event, fn_t_log_ins.log_object, fn_t_log_ins.log_value)
RETURNING
t_log.log_id
)
SELECT
vlozene.log_id
INTO
fn_t_log_ins.log_id
FROM
vlozene
;
RETURN;
END;
$function$
;
Úloha 3.4
Upravte už existujúcu funkciu tak, aby vo svojom tele volala funkciu z predchádzajúcej úlohy. Môžte využiť aj pg_sleep(random());. My sme upravili funkciu úlohy 1.1.
Riešenie
CREATE OR REPLACE FUNCTION movies.greet()
RETURNS text
LANGUAGE plpgsql
AS $function$
declare
t timestamptz := clock_timestamp();
begin
perform pg_sleep(random());
perform fn_t_log_ins('EXECUTE','greet()',extract(second from clock_timestamp() - t)::real); --perform ked nechceme spracovat vystup z volania
RETURN 'hello world!';
END;
$function$
;
Poznámka
Teraz si prezrite tabuľku t_log. Mali Vám do nej pribudnúť záznamy (dĺžka vykonávania) vami upravenej funkcie.
Úloha 3.5
Vytvorte funkciu, ktorá skontroluje správnosť formátu slovenského PSČ. Znaky nech sú iba číslice od 0-9 a dĺžka PSČ je rovná 5. Vstupný parameter je: psc (text). Funkcia nech vráti 1 v prípade chyby PSČ (napríklad dlhšie PSČ, iné znaky ako číslice,...). Inak nech vráti 0. V prípade, že dĺžka PSČ je rovná 5 ale PSČ aj tak nie je platné, využitím raise notice vypíšte, na ktorej pozícii sa nachádza chybný znak.
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_kontrola_psc_sk(psc text)
RETURNS integer
LANGUAGE plpgsql
AS $function$
declare
dlzka int;
i int;
begin
dlzka:=char_length(fn_kontrola_psc_sk.psc);
if dlzka<>5 then
raise notice 'Nesedi dlzka zadaneho PSC, musi byt 5';
return 1;
end if;
for i in 1..5 loop
if ascii(substring(fn_kontrola_psc_sk.psc from i for 1)) < 48 or ascii(substring(fn_kontrola_psc_sk.psc from i for 1)) > 57 then
raise notice 'Na pozicii % nie je cislica',i;
return 1;
end if;
end loop;
return 0; --0 - ok, 1 - nesedi PSC, 2 - chyba funkcie
END;
$function$
;
Krok 4
Úloha 4.1
Vytvorte funkciu, v ktorej skontrolujete správnosť formátu PSČ už zadaných záznamov v tabuľke t_cinema. Môžte pritom využiť funkciu z predošlej úlohy. Použite CURSOR, ktorý dotaz "zapuzdrí" a umožní spracovávať výsledky dotazu po jednotlivých riadkoch. Kurzory sa tiež využívajú, ak chceme zabrániť preplneniu pamäte v prípade, že výsledok dotazu obsahuje veľký počet riadkov. Kurzor, ktorý zapuzdrujue všetky riadky z tabuľky t_cinema si môžte deklarovať nasledovne:
cur_cinema CURSOR FOR SELECT * FROM t_cinema;
Riešenie
CREATE OR REPLACE FUNCTION movies.fn_psc_cinema()
RETURNS int4
LANGUAGE plpgsql
AS $function$
declare
--zaznam movies.t_cinema%ROWTYPE;
cur_cinema CURSOR FOR SELECT * FROM t_cinema;
riadok record;
psc_spravne integer;
begin
open cur_cinema;
loop
fetch cur_cinema into riadok;
exit when not found;
select fn_kontrola_psc_sk(riadok.cin_postal_code) into psc_spravne;
if (psc_spravne<>0) then
raise notice 'Riadok s ID: % %' ,riadok.cin_id, riadok.cin_name;
return 1;
end if;
end loop;
close cur_cinema;
return 0;
END;
$function$
;
Upozornenie
Po vypracovaní úloh z cvičenia prosím o vyplnenie nasledujúceho dotazníka.