Dinamikus SQL egy példán keresztül
Abstract
Ebben a részben egy példán keresztül próbálom bemutatni a natív Dinamikus SQL lehetőségeit, használhatóságát.
A Példaprogramok között lévő "Dinamikus SQL" alatt lévő példával együtt elég nagy szeletét le is fedi
ennek a felhasználhatóságnak.
Az alábbi példát egy olyan kis rendszernek kell elképzelni, amely bejövő vásárlási tranzakciók
alapján kiszámolja az évszakonkénti egyenleget bizonyos szabályok alapján.
A folyamat közben létrejön egy temporális tábla, majd erre adunk jogosultságokat(GRANT)
más felhasználóknak, valamint csinálunk rá indexet, majd el is dobjuk a táblát.
Ezek mind azért érdekes műveletek, mert futási időben hajtjuk végre őket,
és ezeket nem lehetne megcsinálni "normál" statikus SQL alapú eljárásban.
Bármilyen kérdés esetén küldj e-mailt a drdarkman@vipmail.hu címre!
--------------------------------------------------------
-- Procedure CALC_SEASONLY_BAL_PROC
--------------------------------------------------------
CREATE OR REPLACE PROCEDURE calc_seasonly_bal_proc
AS
/****************************************************************/
/* Created: DARKMAN */
/* Purpuse: Demonstrate the main goals of Dynamic SQL */
/* through a detailed example */
/* Date: 2014.06.12. */
/****************************************************************/
--ez a "csúnya" típus a BULK COLLECT használatához kell
TYPE t_trx_table_type IS TABLE OF v_incoming_trx_per_customer%ROWTYPE;
--és kell egy "csúnya" típusú változó is még
v_tab t_trx_table_type;
c_proc_name CONSTANT VARCHAR2(18) := 'CALC_SEASONLY_BAL_PROC';
v_error_msg error_log.message%TYPE;
v_count NUMBER := 0;
v_actual_table VARCHAR2(30);
v_act_season VARCHAR2(6); --SPRING, SUMMER, AUTUMN, WINTER
v_year VARCHAR2(4);
v_stmt_num NUMBER :=0;
error_excp EXCEPTION;
BEGIN
SELECT EXTRACT (YEAR FROM SYSDATE)
INTO v_year
FROM DUAL;
-- visszaadja hogy milyen évszak van, jelenleg ORACLE-nek csak olyan fgv-e van ami negyedévet ad vissza
v_stmt_num := 10;
v_act_season := func_get_season();
-- összekonkatenáljuk a két változót, hogy rövidítsük a kódot
v_stmt_num := 20;
v_actual_table := v_act_season || '_' || v_year;
--itt pedig v_act_season alapján létrehozzuk a temporális táblánkat
v_stmt_num := 30;
EXECUTE IMMEDIATE
'CREATE TABLE temp_' || v_actual_table || '_transactions
(
year NUMBER,
season NUMBER,
prod_id NUMBER,
cust_id NUMBER,
tran_date DATE,
quantity NUMBER,
points FLOAT
)';
--a táblára dobunk rá GRANT-ot, majd INDEX-et futási időben, hiszen ilyet statikus SQL-ben nem lehetne
v_stmt_num := 40;
EXECUTE IMMEDIATE
'GRANT SELECT, INSERT ON system.temp_' || v_actual_table || '_transactions TO SYS';
--most pedig máshogy hajtjuk végre a GRANT-ot az UPDATE-re és a DELETE-re, mert a dinamikus utasításokat kétféleképp is fel tudjuk építeni,
-- és hogy láthatóvá tegyem ezt a különbséget
v_stmt_num := 50;
EXECUTE IMMEDIATE
'GRANT UPDATE, DELETE ON system.temp_' || '(:1)' || '_' || '(:2)' || '_transactions TO SYS;'
USING v_act_season, v_year;
--nézzük meg a két GRANT utasításnál, hogy a ";"-ket melyiknél hova raktam. Erre figyeljünk,
--hogyha csak konkatenálunk akkor a parancs string-jébe ne rakjunk ";"-őt, de ha USING-ot használunk
--akkor bizony bele kell rakni, hiszen az maga egy rendes utasítás
/*USING előnye: ekkor a paramétereket leellenőrzi futási időben a fordító
és ha nincs vagy rossz érték van megadva akkor hibát jelez, mégpedig futási idejű hibát, hogy elszállt
az adott eljárás például a '1512'-edik sorban.
Ugyanis ha konkrét lekérdezés vagy annak részletei vannak belerejtve az EXECUTE IMMEDIATE paraméterének string-jébe,
(pl. vki további infót akar kicsikarni a DB-ből, SQL-injection), akkor USING használata nélkül
egy lekérdezés szintű hibát kapna amiben lehetnek bizonyos információk a DB struktúrájáról,
sőt konkrét eredményhalmazokat is vissza lehet kapni vagy manipulálni!*/
--tegyünk rá index-et is, ha már létrejött az a tábla és tudjuk, hogy lesz benne mondjuk 20-30 ezer sor
--Azért is jó így, mert nem felejtjük el létrehozni az index-(ek)et, mert így a végrehajtási logika itt tartalmazni fogja
v_stmt_num := 60;
EXECUTE IMMEDIATE
'CREATE INDEX temp_' || '(:1)' || '_' || '(:2)' || '_idx ON system.temp_' || '(:1)' || '_' || '(:2)' || '_transactions (year,season,cust_id) ;'
--itt az index neve van összekonkatenálva :)
USING v_act_season, v_year;
--NOTE: itt akár létrehozhatunk PRIMARY vagy UNIQUE KEY-t, amik automatikusan UNIQUE típusú indexet is tesznek a táblára :)
-- BULK COLLECT segítségével végigmegyünk a feldolgozandó halmazon
-- ehhez először egy SELECT szükséges, amihez kellett a fenti "csúnya" típus
-- amibe belerakjuk a feldolgozandó halmazt
SELECT *
BULK COLLECT INTO v_tab
FROM v_incoming_trx_per_customer;
--majd végigmegyünk rajta egy FOR ciklusban és a segítségével beállítjuk a megfelelő értékeket
-- a másik táblán, ami a mi esetünkben épp az ügyfelek egyenlegét tárolja
FORALL i IN v_tab.first .. v_tab.last
EXECUTE IMMEDIATE
'UPDATE seasonal_balance
SET balance = :1
WHERE cust_id = :2'
USING v_tab(i).points_per_cust,
v_tab(i).cust_id;
--ha kész akkor DROP TABLE a temp táblára, hogy ne foglalja a helyet és ennyi
v_stmt_num := 70;
EXECUTE IMMEDIATE
'DROP TABLE temp_' || '(:1)' || '_' || '(:2);'
USING v_act_season, v_year;
EXCEPTION
WHEN OTHERS THEN
/*Ez a statement number-es hibajelzés is használatos bár kicsit fapadosabb mint a
"Kezdeti ügyfél és tranzakciókezelés egy megvalósítása PL/SQL-ben" példámban használtnál.
De ha nem akarunk nagyon bohóckodni a különféle hibakezelésekkel, akkor lehet használni
de azzal számolni kell, hogy nagyobb kódoknál nehezen karbantartható és
nehezen található meg benne mi az adott hiba okozója, meg kicsit zavaró is hogy ott van*/
proc_error_log(c_proc_name, 'STMT: ' || to_char(v_stmt_num) || '; ' || v_error_msg);
END calc_seasonly_bal_proc;
--NOTE: mint szinte minden program ez is továbbfejleszthető, mégpedig akár rögtön azzal,
-- hogy a futtatni kívánt dinamikus parancsokat(stringeket) kirakjuk táblába, így az eljárásunk kódja leegyszerűsödik,
-- a végrehatjandó utasításaink karbantarthatóbbá válnak, hiszen nem az eljárás kódját kell módosítani, hanem
-- csak a táblában a megfelelő sorokat. Tulajdonképp egy minimális végrehatjási logikát kell csak implementálni
-- egy kurzor segítségével, ami végigmegy az utasításokat tartalmazó táblán és végrehajtja azokat.
/
--------------------------------------------------------
-- Function FUNC_GET_SEASON
--------------------------------------------------------
CREATE OR REPLACE FUNCTION func_get_season
RETURN VARCHAR2
AS
/****************************************************************/
/* Created: DARKMAN */
/* Purpuse: Return the name of the actual season from the SYSDATE*/
/* Date: 2014.06.12. */
/****************************************************************/
v_return VARCHAR2(6):='';
c_date CONSTANT DATE := SYSDATE;
c_func_name CONSTANT VARCHAR2(15) := 'FUNC_GET_SEASON';
BEGIN
IF EXTRACT(MONTH FROM c_date) IN (1,2,12) THEN
v_return := 'WINTER';
ELSIF EXTRACT(MONTH FROM c_date) IN (3,4,5) THEN
v_return := 'SPRING';
ELSIF EXTRACT(MONTH FROM c_date) IN (6,7,8) THEN
v_return := 'SUMMER';
ELSIF EXTRACT(MONTH FROM c_date) IN (9,10,11) THEN
v_return := 'AUTUMN';
END IF;
RETURN v_return;
EXCEPTION
WHEN OTHERS THEN
proc_error_log(c_proc_name);
END FUNC_GET_SEASON;
/
------------------------------
--** DDL scripts
------------------------------
CREATE TABLE seasonal_balance --szezononkénti egyenleg
(
year NUMBER,
season NUMBER,
cust_id NUMBER,
balance NUMBER,
created DATE
);
CREATE TABLE incoming_transactions -- mindeféle bejövő trx
(
id NUMBER,
cust_id NUMBER,
tran_date DATE,
prod_id NUMBER,
quantity NUMBER,
created DATE
);
CREATE TABLE seasons_rules --pontszámítási szabályok
(
year NUMBER,
season NUMBER,
prod_id NUMBER,
factor FLOAT,
valid_from DATE,
valid_to DATE,
min_limit NUMBER,
created DATE
--PRIMARY KEY az első 3 oszlop+a 2 date
);
CREATE OR REPLACE VIEW v_incoming_trx_per_customer AS
SELECT SUM(points_per_trx) as points_per_cust, cust_id
FROM (SELECT (trx.quantity * rules.factor) as points_per_trx , trx.cust_id as cust_id
FROM incoming_transactions trx
INNER JOIN seasons_rules rules
ON trx.prod_id = rules.prod_id
WHERE tran_date BETWEEN valid_from
AND valid_to
AND quantity >= min_limit)
GROUP BY cust_id;
--process közben létrejövő temp tábla szerkezete
/* temp_"season"_ "year"_transactions
(
year NUMBER
season NUMBER,
prod_id NUMBER,
cust_id NUMBER,
tran_date DATE,
quantity NUMBER,
points FLOAT
); */
A példa Oracle SQLDeveloperrel készült, Oracle 12c adatbázison.
(Tóth Gábor, 2014, II. félév)