Składowane procedury i funkcje Procedury i funkcje są zestawem poleceń SQL, które są przechowywane na serwerze (tak jak dane w tablicach). Istnieją sytuacje, kiedy procedury i funkcje są szczególnie przydatne. Kiedy wiele aplikacji klienckich jest pisanych w różnych językach lub/i pracuje na różnych platformach, jednak każda z nich musi wykonać podczas swojej pracy ten sam zestaw poleceń SQL (na przykład wykonać listę klientów banku, którzy są zameldowani w pewnym mieście, lub na przykład zmienić dane w polu X na podstawie bieżącej wartości z pola Y w pewnej tablicy, lub, najczęściej, całą gamę poleceń SQL zebranych dodatkowo w jednej transakcji). Kiedy bezpieczeństwo jest najważniejsze. Na przykład banki używają procedur dla prawie wszystkich powszednich operacji wykonywanych przez operatorów. Takie postępowanie zapewnia spójność operacji jak również zapisywanie ich przebiegu w specjalnych tablicach, które przechowują informacje o historii operacji.
Procedury ograniczają liczbę danych, które są przesyłane między serwerem bazy danych a klientem. Ograniczają więc również obciążenie serwera bazy danych, gdyż serwer taki realizuje mniej połączeń. Patrząc na serwer baz danych z punktu widzenia programisty, to możliwość definiowania i przechowywania procedur i funkcji pozwala na tworzenie całych bibliotek funkcji i procedur, i wykorzystywania ich w dowolnym momencie pisania aplikacji, a przy tym pisania mniejszej ilości dość strategicznej części kodu programu, który jest bezpośrednio związany z interfejsem bazodanowym. Jak definiujemy składowaną procedurę? Aby móc zdefiniować procedurę w mysql trzeba posiadać odpowiednie przywileje (CREATE ROUTINE, ALTER ROUTINE, EXECUTE ). Jeśli nie posiadamy takich przywilejów (można to łatwo sprawdzić poleceniem SHOW GRANTS FOR 'user'@'localhost';) wówczas nie pozostaje nam nic innego jak udać się do administratora bazy danych i poprosić o takie przywileje.
shell> mysql -u root -p mysql> create database bank; Query OK, 1 row affected (0.00 sec) mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, ON bank.* TO 'user'@'localhost' IDENTIFIED BY 'haslo'; Query OK, 0 rows affected (0.00 sec) mysql> SHOW GRANTS FOR 'user'@'localhost'; Grants for user@localhost GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE ROUTINE, ALTER ROUTINE ON `bank`.* TO 'user'@'localhost' IDENTIFIED BY 'haslo'
Po takiej interwencji administratora możemy być pewni, że możemy używać składowanych procedur i funkcji w bazie bank. Spróbujmy więc wykonać pierwszy najprostszy test. Na początek utworzymy tablicę, do której wstawimy wartość 5. shell> mysql -u user -p bank mysql> show tables; Empty set (0.00 sec) mysql> CREATE TABLE tab (s1 INT); Query OK, 0 rows affected (0.02 sec) mysql> INSERT INTO tab VALUES (5); Query OK, 1 row affected (0.00 sec)
Następnie zdefiniujemy najprostszą procedurę. Potrzebny jednak do tego będzie jakiś charakterystyczny i unikalny ciąg znaków, który zapewni, że koniec procedury zostanie łatwo rozpoznany. Takim znakiem, który określa koniec wyrażenia jest normalnie średnik ';', jednak średnik nie może określać końca definicji procedury, gdyż występuje na końcu każdego wyrażenia SQL. Najczęściej stosuje się podwójny ukośnik '//'. mysql> DELIMITER // A następnie już definiujemy procedurę. mysql> CREATE PROCEDURE p1() SELECT * FROM tab; // Query OK, 0 rows affected (0.00 sec) Po takiej definicji procedury warto wrócić do powszechnie przyjętego i zestandaryzowanego znaku końca polecenia, którym jest ';' mysql> DELIMITER ;//
Jak widzimy, procedurę definiuje się używając polecenia CREATE PROCEDURE, po którym następuje nazwa procedury (tutaj p1), lista argumentów w nawiasach okrągłych, a następnie ciało procedury i na końcu znak kończący definicję procedury. Nie można używać dwóch procedur o tej samej nazwie w tej samej bazie danych. Można używać notacji kropkowej przy definicji procedury, wskazując nazwę bazy danych, z którą związana jest procedura, np. bank.p1. Jak wywołuje się procedurę? Procedurę wywołuje się poleceniem CALL procedura(); Na przykład mysql> CALL p1(); +------+ s1 +------+ 5 +------+ 1 row in set (0.00 sec)
Jakich poleceń możemy używać w ciele procedury? Wewnątrz procedur możemy używać wszystkich poleceń języka DML, a zatem legalne jest używanie wewnątrz procedur poleceń INSERT, UPDATE, DELETE, SELECT, REPLACE itd. Możemy też wewnątrz procedur stosować transakcje, a zatem polecenia COMMITT i ROLLBACK są też dozwolone. W MySQL dozwolone są też polecenia języka DDL, a zatem polecenia CREATE, ALTER, DROP.
Parametry Po nazwie procedury, podczas jej definicji, można zdefiniować listę parametrów w nawiasach okrągłych. 1. CREATE PROCEDURE p5 ()... pusta lista argumentów 2. CREATE PROCEDURE p5 ([IN] nazwa typ_danych)... jeden parametr wejściowy. Słowo IN jest opcjonalne, gdyż parametry domyślnie są wejściowe. 3. CREATE PROCEDURE p5 (OUT nazwa typ_danych)... jeden parametr wyjściowy. Tutaj już słowo OUT nie jest tylko opcjonalne. 4. CREATE PROCEDURE p5 ([INOUT] nazwa typ_danych)... jeden parametr wejściowy i wyjściowy jednocześnie.
Przykład użycia parametru wejściowego mysql> CREATE PROCEDURE p5 (p INT) SET @x = p // Query OK, 0 rows affected (0.00 sec) mysql> CALL p5(12345)// Query OK, 0 rows affected (0.00 sec) mysql> SELECT @x// +-------+ @x +-------+ 12345 +-------+ 1 row in set (0.00 sec) Jak widać w ciele procedury wstawiamy do zmiennej wartość argumentu wejściowego i używamy jej później po definicji procedury.
Przykład użycia parametru wyjściowego mysql> CREATE PROCEDURE p6 (OUT p INT) SET p = -5 // Query OK, 0 rows affected (0.00 sec) mysql> CALL p6(@y)// Query OK, 0 rows affected (0.00 sec) mysql> SELECT @y// +-----+ @y +-----+ -5 +-----+ 1 row in set (0.00 sec) Tutaj p jest parametrem wyjściowym, i podczas wywołania procedury przypisujemy jego wartość do zmiennej @y.
Blok procedury mysql> CREATE PROCEDURE p7 () BEGIN SET @a = 5; SET @b = 5; INSERT INTO tab VALUES (@a); SELECT s1 * @a FROM tab WHERE s1 >= @b; END; // Bloki, jak widać, ograniczone są słowami BEGIN oraz END. Przydają się wówczas, gdy zamierzamy używać wewnątrz procedury pętli lub wyrażeń warunkowych.
Deklarowanie zmiennych Wewnątrz procedur możemy deklarować zmienne widoczne wewnątrz bloku (zmienne lokalne). mysql> CREATE PROCEDURE p8 () BEGIN DECLARE a INT; DECLARE b INT; SET a = 5; SET b = 5; INSERT INTO tab VALUES (a); SELECT s1 * a FROM tab WHERE s1 >= b; END; // Zmienne deklarujemy zaraz po słowie BEGIN.
Klauzula DEFAULT mysql> CREATE PROCEDURE p10 () BEGIN DECLARE a, b INT DEFAULT 5; INSERT INTO t VALUES (a); SELECT s1 * a FROM t WHERE s1 >= b; END; // Słowo DEFAULT umożliwia nadanie wartości początkowych zmiennym.
Zasięg zmiennych mysql> CREATE PROCEDURE p11 () BEGIN DECLARE x1 CHAR(5) DEFAULT 'outer'; BEGIN DECLARE x1 CHAR(5) DEFAULT 'inner'; SELECT x1; END; SELECT x1; END; //
Wywołajmy procedurę. mysql> CALL p11(); // +-------+ x1 +-------+ inner +-------+ 1 row in set (0.02 sec) +-------+ x1 +-------+ outer +-------+ 1 row in set (0.02 sec) Query OK, 0 rows affected (0.02 sec)
IF-THEN-ELSE Wewnątrz procedur składowanych możemy też używać wyrażeń warunkowych mysql> CREATE PROCEDURE p12 (IN par1 INT) BEGIN DECLARE v1 INT; SET v1 = par1 + 1; IF v1 = 0 THEN INSERT INTO tab VALUES (17); END IF; IF par1 = 0 THEN UPDATE tab SET s1 = s1 + 1; ELSE UPDATE tab SET s1 = s1 + 2; END IF; END; // Jest to prosty przykład użycia wyrażenia warunkowego SQL wewnątrz procedury. Właściwie nic tutaj nie trzeba komentować.
Wyrażenie CASE mysql> CREATE PROCEDURE p13 (IN par1 INT) BEGIN DECLARE v1 INT; SET v1 = par1 + 1; CASE v1 WHEN 0 THEN INSERT INTO tab VALUES (17); WHEN 1 THEN INSERT INTO tab VALUES (18); ELSE INSERT INTO tab VALUES (19); END CASE; END; // Jak widać zastąpiliśmy wyrażenie IF-THEN-ELSE trochę innym wyrażeniem warunkowym.
Sprawdźmy jak to działa: mysql> CALL p13(1)// Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM tab// +------+ s1 +------+ 6 6 19 +------+ 3 rows in set (0.00 sec) Ponieważ wywołując procedurę z parametrem wejściowym równym 1, zatem zmienna v1 uzyskała w ciele procedury wartość 2. A zatem do tablicy tab została rzeczywiście wstawiona wartość 19.
A co się stanie jeśli do procedury przekażemy parametr NULL: mysql> CALL p13(null)// Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM tab// +------+ s1 +------+ 6 6 19 19 +------+ 4 rows in set (0.00 sec) Oczywiście wstawiony zostanie nowy rekord z wartością 19 w polu s1. Jest to oczywiste, jeśli tylko zajrzy się do bloku procedury.
Pętle WHILE... END WHILE LOOP... END LOOP REPEAT... END REPEAT GOTO Istnieją trzy "normalne" sposoby używania pętli w SQL: pętle WHILE, pętle LOOP oraz pętle REPEAT, oraz jeden nie-standardowy sposób - GOTO.
WHILE... END WHILE mysql> CREATE PROCEDURE p14 () BEGIN DECLARE v INT; SET v = 0; WHILE v < 5 DO INSERT INTO tab VALUES (v); SET v = v + 1; END WHILE; END; //
Zobaczmy jak to działa: mysql> CALL p14()// Query OK, 1 row affected (0.00 sec) Nie ma się co martwić, że dostaliśmy 1 row affected, bo ta informacja dotyczy tylko ostatniego polecenia INSERT. mysql> SELECT * FROM tab; // +------+ s1 +------+... 0 1 2 3 4 +------+ 9 rows in set (0.00 sec)
REPEAT... END REPEAT mysql> CREATE PROCEDURE p15 () BEGIN DECLARE v INT; SET v = 0; REPEAT INSERT INTO tab VALUES (v); SET v = v + 1; UNTIL v >= 5 END REPEAT; END; // Pętla prawie niczym się nie różni od pętli WHILE... END WHILE poza faktem, że trzeba na końcu wyrażenia zawsze sprawdzić warunek zakończenia pętli.
Zobaczmy jak to działa: mysql> CALL p15()// Query OK, 1 row affected (0.00 sec) mysql> SELECT COUNT(*) FROM tab// +----------+ COUNT(*) +----------+ 14 +----------+ 1 row in set (0.00 sec) Widać 9 + 5 = 14 rekordów w tablicy tab.
LOOP... END LOOP mysql> CREATE PROCEDURE p16 () BEGIN DECLARE v INT; SET v = 0; znacznik: LOOP INSERT INTO tab VALUES (v); SET v = v + 1; IF v >= 5 THEN LEAVE znacznik; END IF; END LOOP; END; // Jak widać pętlę LOOP opuszcza się słowem LEAVE.
Znaczniki mysql> CREATE PROCEDURE p17 () zn1: BEGIN zn2: WHILE 0 = 1 DO LEAVE zn2; END WHILE; zn3: REPEAT LEAVE zn3; UNTIL 0 =0 END REPEAT; zn4: LOOP LEAVE zn4; END LOOP; END; // Znaczników możemy używać przed BEGIN, WHILE, REPEAT, LOOP. Są to jedyne miejsca w procedurach, w których rozpoczynanie wyrażeń znacznikami jest dozwolone. Można też używać znaczników na końcach tych wyrażeń. Mają wtedy znaczenie organizacyjne kodu; w zasadzie nic nie robią, jednak przy większych partiach kodu z większą ilością znaczników pozwalają się szybciej zorientować, gdzie zaczynał się dany znacznik.
mysql> CREATE PROCEDURE p18 () zn1: BEGIN zn2: WHILE 0 = 1 DO LEAVE zn2; END WHILE zn2; zn3: REPEAT LEAVE zn3; UNTIL 0 =0 END REPEAT zn3 ; zn4: LOOP LEAVE zn4; END LOOP zn4; END zn1 ; //
ITERATE Znaczniki są niezbędne do oznaczenia miejsca dla polecenia ITERATE. mysql> CREATE PROCEDURE p20 () BEGIN DECLARE v INT; SET v = 0; znacznik: LOOP IF v = 3 THEN SET v = v + 1; ITERATE znacznik; END IF; INSERT INTO tab VALUES (v); SET v = v + 1; IF v >= 5 THEN LEAVE znacznik; END IF; END LOOP; END; // ITERATE oznacza: rozpocznij pętlę raz jeszcze.
Jak definiujemy składowaną funkcję? Przykład mysql> CREATE FUNCTION fun (n DECIMAL(3,0)) RETURNS DECIMAL(20,0) BEGIN DECLARE wsp DECIMAL(20,0) DEFAULT 1; DECLARE licznik DECIMAL(3,0); SET licznik = n; wsp_petla: REPEAT SET wsp = wsp * licznik; SET licznik = licznik - 1; UNTIL licznik = 1 END REPEAT; RETURN wsp; END //
Definicja podobna jest, jak widać, do definicji procedury, przy czym dodatkowo musimy dodać klauzulę RETURN, gdyż funkcja zawsze coś zwraca. Musimy też zdefiniować jakiego typu dane zwraca funkcja. Przykład wywołania: INSERT INTO tab VALUES (fun(pi)) // SELECT s1, fun(s1) FROM tab // UPDATE tab SET s1 = fun(s1) WHERE fun(s1) < 5 // DROP PROCEDURE Do usunięcia procedury służy komenda DROP PROCEDURE, na przykład mysql> DROP PROCEDURE p6 //.