Wykład 5

Zaawansowane programowanie na serwerze bazy danych

 

Streszczenie

Zaprezentujemy szereg dodatkowych możliwości programowania na serwerze bazy danych Oracle.

Do tej pory wykonywane instrukcje SQL musiały być znane z góry dla całej skompilowanej jednostki programowej. Oracle dostarcza możliwości ustalania tekstu wykonywanej instrukcji dynamicznie. Dwie dostępne opcje są przedstawione w punkcie 5.1.

Sytuacją, którą trudno zrealizować za pomocą konwencjonalnych środków bazodanowych, jest konieczność planowania i wykonywania w określonym czasie wcześniej przygotowanych fragmentów kodu. Zadania takie są realizowane w ramach automatycznie otwieranych sesji na koncie użytkownika, który zlecił te zadania. Określanie terminowych zadań do wykonywania na bazie danych jest realizowane za pomocą pakietu DBMS_JOB omawianego w punkcie 5.2.

Przy wykonywaniu aplikacji po stronie serwera jest ona ograniczona do korzystania z danych zapisanych w bazie danych, do których aplikacja ma uprawnienia. Nie ma ona możliwości innej komunikacji niż przez same dane w bazie danych z innymi aplikacjami działającymi na tym samym serwerze bazy danych bądź ogólniej na tym samym komputerze. Może się więc okazać wygodna możliwość komunikacji z innymi sesjami otwartymi na tym samym serwerze bazy danych, jak również z innymi programami poprzez pliki w systemie plików komputera, na którym znajduje się serwer bazy danych. Do realizacji tych celów służą odpowiednio pakiety DBMS_PIPE i UTL_FILE omawiane odpowiednio w punktach 5.3 i 5.4.

W punkcie 5.5 pokażemy, w jaki sposób można w aplikacjach po stronie serwera korzystać z bibliotek podprogramów kompilowanych niezależnie. Do tego celu służy w Oracle obiekt bazy danych o nazwie biblioteka (ang. Library).

W punkcie 5.6 wspomnimy o możliwości przechowywania kodu języka programowania Java w bazie danych i wykonywania go na serwerze bazy danych.

Zawartość punktów 5.7 do 5.9 stanowi dodatek wyjaśniający zagadnienie dostępu do bazy danych z programów zewnętrznych za pomocą odpowiednio metody osadzania instrukcji SQL oraz metody korzystania z pomocniczych bibliotek (jak JDBC) zapewniających dostęp do bazy danych.
 


5.1 Dynamiczny SQL

Do tej pory mieliśmy do czynienia wyłącznie z sytuacją, w której instrukcje SQL są w całości znane w chwili kompilacji jednostki programowej. W praktyce są jednak sytuacje, gdy w chwili kompilacji programu nie znamy pełnej postaci instrukcji SQL. Szczegóły mogą być ustalane w trakcie działania programu, jak np. z której tabeli wyświetlić wiersze.

Oto prosty przykład ze Standardu SQL, w którym realizowana jest nie określona w chwili kompilacji programu zewnętrznego instrukcja SQL:

SQLsource VARCHAR2(5000);
SQLprepped VARCHAR2(5000);
.....
/* Wczytaj na zmienną SQLsource tekst instrukcji SQL, którą chcesz wykonać */
EXEC SQL PREPARE SQLprepped FROM SQLsource;  -- skompiluj
EXEC SQL EXECUTE SQLprepped;                 -- wykonaj
......

Zmienna SQLsource zawiera tekst instrukcji do wykonania. Natomiast zmienna SQLprepped służy do przechowania skompilowanej postaci instrukcji SQL. Instrukcja SQL PREPARE kompiluje ustaloną dynamicznie instrukcję SQL. Natomiast instrukcja SQL EXECUTE wykonuje ją.

Po wykonaniu instrukcji SQL na systemowych zmiennych SQLSTATE i SQLCODE znajduje się informacja o statusie wykonania tej instrukcji.

Standard języka SQL dostarcza zbioru instrukcji jak SQL PREPARE i SQL EXECUTE noszących razem nazwę dynamicznego SQL.

Oracle ma swoją wersję dynamicznego SQL zrealizowaną za pomocą pakietu PL/SQL o nazwie DBMS_SQL. Najpierw rozważymy prosty przykład. Załóżmy, że chcemy wykonać instrukcję

DROP TABLE nazwa_tabeli;
w której nazwa_tabeli jest wyznaczana w sposób dynamiczny, np. po analizie wartości przekazanych przez perspektywę słownika danych User_tables. Najpierw na zmiennej polecenie tworzymy instrukcję SQL do wykonania, a następnie kolejno wywołujemy:
 
1. DBMS_SQL.OPEN_CURSOR - do otworzenia kursora,
2. DBMS_SQL.PARSE - do sparsowania wyznaczonej instrukcji,
3. DBMS_SQL.EXECUTE - do wykonania instrukcji,
4. DBMS_SQL.CLOSE_CURSOR - do zamknięcia kursora.
-- nazwa_tabeli jest parametrem
id_kursora INTEGER;
wart INTEGER;
polecenie VARCHAR2(250);
BEGIN
   polecenie := 'DROP TABLE '|| nazwa_tabeli;
   id_kursora := DBMS_SQL.OPEN_CURSOR;
   DBMS_SQL.PARSE(id_kursora, polecenie, NATIVE);
   wart := DBMS_SQL.EXECUTE(id_kursora);
   DBMS_SQL.CLOSE_CURSOR(id_kursora);
   ....
Oto sygnatury użytych podprogramów pakietu DBMS_SQL:
DBMS_SQL.OPEN_CURSOR RETURN INTEGER; -- przekazuje identyfikator tworzonego kursora.
DBMS_SQL.PARSE(
  c IN INTEGER,
  statement IN VARCHAR2,
  language_flag IN INTEGER);
-- sparsuj instrukcję statement w kursorze c.
DBMS_SQL.EXECUTE(c IN INTEGER); -- wykonaj instrukcję w kursorze c.
DBMS_SQL.CLOSE_CURSOR(c IN INTEGER); -- zamknij kursor o identyfikatorze c.
gdzie:

Następująca procedura powoduje wykonanie instrukcji SQL podanej jako jej parametr napis. Może to być dowolna instrukcja SQL, a więc na przykład instrukcja CREATE TABLE.

CREATE OR REPLACE PROCEDURE Wykonaj(napis IN varchar2) AS
kursor INTEGER;
ret INTEGER;
BEGIN
   kursor := DBMS_SQL.OPEN_CURSOR;

   DBMS_SQL.PARSE(kursor, napis, DBMS_SQL.native);
   ret := DBMS_SQL.EXECUTE(kursor);
   DBMS_SQL.CLOSE_CURSOR(kursor);
END;
A oto bardziej złożony przykład. Załóżmy, że chcemy wykonać instrukcję INSERT, w której nazwa tabeli, jak i wstawiane do tabeli wartości są wyznaczane w sposób dynamiczny. Do zastosowanych uprzednio instrukcji dochodzą wywołania procedury BIND_VARIABLE dostarczającej wartości do zmiennej wiązania występującej w instrukcji SQL (jednym z argumentów jest otworzony kursor).
CREATE PROCEDURE Insert_into_table (
table_name VARCHAR2,
deptnumber NUMBER,
deptname VARCHAR2,
location VARCHAR2) IS
cur_hdl INTEGER;
stmt_str VARCHAR2(200);
rows_processed BINARY_INTEGER;
BEGIN
   stmt_str := 'INSERT INTO ' || table_name ||' VALUES (:deptno, :dname, :loc)';
   -- wyznaczenie instrukcji SQL do wykonania
   cur_hdl := DBMS_SQL.OPEN_CURSOR;   -- otwarcie kursora
   DBMS_SQL.PARSE(cur_hdl, stmt_str, DBMS_SQL.native);   -- sparsowanie instrukcji INSERT
  DBMS_SQL.BIND_VARIABLE (cur_hdl, ':deptno', deptnumber);
  DBMS_SQL.BIND_VARIABLE (cur_hdl, ':dname', deptname);
  DBMS_SQL.BIND_VARIABLE (cur_hdl, ':loc', location);
  -- związanie zmiennych w instrukcji INSERT
  rows_processed := DBMS_SQL.EXECUTE(cur_hdl);   -- wykonanie INSERT
  DBMS_SQL.CLOSE_CURSOR(cur_hdl);   -- zamknięcie kursora
END;
Oto sygnatura procedury DBMS_SQL.BIND_VARIABLE:
DBMS_SQL.BIND_VARIABLE(
   c IN INTEGER,
   name IN VARCHAR2,
   value IN typ danych)
gdzie

Przy wykonywaniu zapytań schemat postępowania jest trochę bardziej skomplikowany. Mianowicie obejmuje on:

  1. określenie kolumn, którym mają zostać przypisane wartości przekazywane z bazy danych przez SELECT (dla każdej wynikowej kolumny wywołanie procedury DEFINE_COLUMN);
  2. wykonanie instrukcji SELECT (procedura EXECUTE);
  3. sprowadzenie wierszy (procedura FETCH_ROWS);
  4. odczytanie wartości w kolumnie (procedura COLUMN_VALUE).

Oto sygnatury tych podprogramów:

DBMS_SQL.DEFINE_COLUMN(
   c IN INTEGER,
   pozycja IN INTEGER,
   kolumna IN typ danych);

DBMS_SQL.COLUMN_VALUE(
   c IN INTEGER,
   pozycja IN INTEGER,
   wart OUT typ danych);

DBMS_SQL.EXECUTE(c IN INTEGER) RETURN INTEGER;

DBMS_SQL.FETCH_ROWS(c IN INTEGER) RETURN INTEGER;

gdzie

Oto procedura zawierająca pętlę LOOP wypisująca na ekranie wiersze tabeli przekazanej jako parametr tabela zawierającej kolumny id, name i birthdate.

PROCEDURE Process(tabela IN VARCHAR2) AS
id NUMBER;
name VARCHAR2(30);
birthdate DATE;
kursor INTEGER;
ignore INTEGER;
BEGIN
  kursor := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(source_cursor, 'SELECT Id, Name, Birthdate FROM ' || tabela, DBMS_SQL.NATIVE);
  DBMS_SQL.DEFINE_COLUMN(kursor, 1, id);
  DBMS_SQL.DEFINE_COLUMN(kursor, 2, name);
  DBMS_SQL.DEFINE_COLUMN(kursor, 3, birthdate);
  ignore := DBMS_SQL.EXECUTE(kursor);
  LOOP
    IF DBMS_SQL.FETCH_ROWS(kursor)>0 THEN
       DBMS_SQL.COLUMN_VALUE(kursor, 1, id);
       DBMS_SQL.COLUMN_VALUE(kursor, 2, name);
       DBMS_SQL.COLUMN_VALUE(kursor, 3, birthdate);
       DBMS_OUTPUT.PUT_LINE(TO_CHAR(id)||’,’||name||’,’||TO_CHAR(birth_date));
    ELSE
      EXIT;
    END IF;
  END LOOP;
  DBMS_SQL.CLOSE_CURSOR(kursor);
END;
Oprócz pakietu DBMS_SQL jest jeszcze druga opcja tzw. naturalnego dynamicznego SQL, którego instrukcje można bezpośrednio umieszczać w kodzie PL/SQL.

Instrukcja:

EXECUTE IMMEDIATE instrukcja_SQL
[USING lista_parametrów_wiązania];
powoduje wykonanie instrukcji podanej jako wartość napisową VARCHAR2 instrukcja_SQL, ewentualnie dostarczając do zmiennych wiązania w instrukcja_SQL wartości parametrów wiązania.

Dwa poniższe przykłady ilustrują użycie naturalnego dynamicznego SQL. Pierwszy przykład dotyczy wykonywania instrukcji innych niż SELECT.

DECLARE
sql_stmt VARCHAR2(100);
plsql_block VARCHAR2(200);
my_deptno NUMBER(2) := 50;
my_dname VARCHAR2(15) := 'PERSON';
my_loc VARCHAR2(15) := 'DALLAS';
emp_rec emp%ROWTYPE;
BEGIN
  sql_stmt := 'INSERT INTO Dept VALUES (:1, :2, :3)';
  EXECUTE IMMEDIATE sql_stmt
  USING my_deptno, my_dname, my_loc;

  sql_stmt := 'SELECT * FROM Emp WHERE Empno = :id';
  EXECUTE IMMEDIATE sql_stmt INTO emp_rec USING 7788;
  EXECUTE IMMEDIATE 'DELETE FROM Dept WHERE Deptno = :n' USING my_deptno;
  EXECUTE IMMEDIATE 'CREATE TABLE Bonus (Id NUMBER, Am NUMBER)';
END;
W przypadku instrukcji SELECT stosuje się zmienną kursorową.  Najpierw definiuje się typ kursowy, potem deklaruje się zmienną kursorową a następnie otwiera się kursor dla instrukcji SELECT podanej w klauzuli FOR jako wartość zmiennej napisowej (z ewentualnym przekazaniem wartości zmiennych wiązania).
DECLARE
TYPE EmpCurTyp IS REF CURSOR;
emp_cv EmpCurTyp;
emp_rec emp%ROWTYPE;
sql_stmt VARCHAR2(100);
my_job VARCHAR2(15) := 'CLERK';
BEGIN
  sql_stmt := 'SELECT * FROM Emp WHERE Job = :j';
  OPEN emp_cv FOR sql_stmt USING my_job;
  LOOP
    FETCH emp_cv INTO emp_rec;
    EXIT WHEN emp_cv%NOTFOUND;
    ... -- process record
  END LOOP;
  CLOSE emp_cv;
END;
Do zalet naturalnego dynamicznego SQL należą łatwość użycia, większa szybkość działania niż DBMS_SQL, wspomaganie typów danych definiowanych przez użytkownika oraz wspomaganie sprowadzania wierszy do rekordów.

Do zalet DBMS_SQL należą możliwość jego użycia w programach po stronie klienta, możliwość użycia dowolnej liczby zmiennych wiązania, wspomaganie wykonywania instrukcji SQL, których rozmiar jest większy niż 32kB oraz możliwość wielokrotnego użycia instrukcji SQL po początkowym przygotowaniu instrukcji przez procedurę DBMS_SQL.Parse.
 


5.2  Zlecanie zadań do wykonania - pakiet DBMS_JOB

Na ogół, kod PL/SQL jest wykonywany w ramach pewnej otwartej sesji. Można także zaplanować periodyczne wykonywanie kodu PL/SQL na serwerze bazy danych poza jakąkolwiek sesją. Tego typu złożone do wykonania zadania są realizowane na serwerze bazy danych za pomocą specjalnych procesów działających w tle o nazwie SNP.
 
DBMS_JOB.SUBMIT(
  job OUT BINARY_INTEGER,
  what IN VARCHAR2,
  next_date IN DATE DEFAULT SYSDATE,
  interval IN VARCHAR2 DEFAULT 'null',
  no_parse IN BOOLEAN DEFAULT FALSE)
gdzie:

Przykład

Generowanie statystyk dla optymalizatora (raz na dzień).

VARIABLE jobno number;
BEGIN
  DBMS_JOB.SUBMIT(:jobno,
        'dbms_ddl.analyze_object(''TABLE'', ''KASA'', ''KONTA'', ''ESTIMATE'', NULL, 50);',
          SYSDATE, 'SYSDATE + 1');
  COMMIT;
END;
/
PRINT jobno
Aby móc skorzystać z procesów SNP, należy ustawić parametr określający ich liczbę w pliku inicjalizacyjnym instancji (domyślnie jest ustawiona na 0):
JOB_QUEUE_PROCESSES = n
gdzie n jest liczbą naturalną między 1 a 36.

 


5.3 Komunikacja z innymi sesjami - pakiet DBMS_PIPE

Pakiet DBMS_PIPE umożliwia komunikację między dwiema lub więcej sesjami w tej samej instancji Oracle poprzez dostęp w obu sesjach do wspólnego potoku (ang. pipe). Potoki nie są objęte mechanizmem transakcji. Potok jest albo prywatny albo publiczny; albo definiowany jawnie albo niejawnie – gdy pojawi się do niego odwołanie.

Każdy publiczny potok działa asynchronicznie. Dowolna liczba użytkowników może wprowadzać do niego komunikaty pod warunkiem posiadania uprawnienia EXECUTE do pakietu DBMS_PIPE. Dowolny użytkownik może pobrać komunikat z potoku i wówczas komunikat zostaje usunięty z potoku i nie może już w szczególności zostać odczytany przez innych użytkowników.

Oto funkcja tworząca nowy potok:

DBMS_PIPE.CREATE_PIPE(
   pipename IN VARCHAR2,
   maxpipesize IN INTEGER DEFAULT 8192,
   private IN BOOLEAN DEFAULT TRUE)
RETURN INTEGER;

Przed wprowadzeniem komunikatu do potoku sesja buduje pełny komunikat z części w swoim lokalnym buforze za pomocą procedury:

DBMS_PIPE.PACK_MESSAGE(item IN typ danych);
gdzie wśród dozwolonych typów danych występują VARCHAR2, NUMBER i DATE. Następnie wywołanie funkcji SEND_MESSAGE umieszcza komunikat w potoku.
DBMS_PIPE.SEND_MESSAGE(
   pipename IN VARCHAR2,
   timeout IN INTEGER DEFAULT MAXWAIT,
   maxpipesize IN INTEGER DEFAULT 8192)
RETURN INTEGER;

Proces odbiera komunikat z potoku do swojego lokalnego bufora za pomocą następującej funkcji:
DBMS_PIPE.RECEIVE_MESSAGE(
   pipename IN VARCHAR2,
   timeout IN INTEGER DEFAULT MAXWAIT)
RETURN INTEGER;

Parametr timeout określa jak długo odczytujący proces ma czekać na nadejście komunikatu - domyślnie MAXWAIT=86400000 sekund (1000 dni).

Następnie za pomocą instrukcji:

DBMS_PIPE.UNPACK_MESSAGE(item OUT typ danych);
proces może odczytać z lokalnego bufora kolejny element komunikatu. Przed odczytaniem może sprawdzić, czy został już odczytany cały komunikat oraz jakiego typu danych jest kolejny element w lokalnym buforze.
DBMS_PIPE.NEXT_ITEM_TYPE RETURN INTEGER;
gdzie wartością funkcji może być: 0 – brak elementów, 6 – VARCHAR2, 9 – NUMBER, 12 – DATE.

Usunięcie potoku następuje za pomocą funkcji:

DBMS_PIPE.REMOVE_PIPE(pipename IN VARCHAR2)RETURN INTEGER;

Przykład

Potoku można użyć do wprowadzania do niego komunikatów np. pochodzących z procesu debugowania. Zewnętrzny program w C może odczytywać te komunikaty i wyświetlać lub drukować.

CREATE OR REPLACE PROCEDURE Debug(msg VARCHAR2) AS
status NUMBER;
BEGIN
  DBMS_PIPE.PACK_MESSAGE(LENGTH(msg));
  DBMS_PIPE.PACK_MESSAGE(msg);
  status := DBMS_PIPE.SEND_MESSAGE('plsql_debug');
  IF status != 0 THEN
    Raise_application_error(-20099, 'Błąd debug');
  END IF;
END Debug;
Oto program napisany w rozszerzeniu języka C o PL/SQL o nazwie Pro*C (więcej informacji o tym rozszerzeniu znajduje się w dalszej części tego wykładu), który odbiera wprowadzone komunikaty.
-- Początek nieskończonej pętli:
FOR (;;)
{
EXEC SQL EXECUTE
DECLARE
len INTEGER;
typ INTEGER;
sta INTEGER;
chr VARCHAR2(2000);
BEGIN
  chr := '';
  sta := DBMS_PIPE.RECEIVE_MESSAGE('plsql_debug');
  IF sta = 0 THEN
    DBMS_PIPE.UNPACK_MESSAGE(len);
    DBMS_PIPE.UNPACK_MESSAGE(chr);
  END IF;
  :status := sta; :retval := chr; :msg_length := len;
END;
END-EXEC;
IF (status == 0)
  printf("\n%.*s\n", msg_length, retval);
ELSE
  printf("sytuacja wyjątkowa o wartości %d\n", status);
}
}
Potoki mogą być użyte do informowania o zajściu jakiegoś zdarzenia, np. poprzez odpalenie wyzwalacza AFTER INSERT/DELETE/UPDATE o zmianach wartości w tabeli można poinformować inne zainteresowane sesje np. aby sporządzić nowy, aktualny wykres danych. Jednak ponieważ wpisywanie do potoku nie jest objęte mechanizmem transakcji, mimo powiadomienia dane faktycznie mogły wcale nie zostać zmienione. W takim przypadku można użyć podobnego w działaniu pakietu DBMS_ALERT, w którym wpisanie do potoku wchodzi w skład transakcji i przy ROLLBACK zostaje anulowane.

 


5.4 Komunikacja przez system plików - pakiet UTL_FILE

Pakiet UTL_FILE umożliwia zapis i odczyt plików tekstowych z kodu PL/SQL wykonywanego przez serwer bazy danych. Wynikiem funkcji otwarcia pliku jest tak zwany uchwyt tego pliku będący rekordem następującego typu:

TYPE UTL_FILE.File_Type IS RECORD (id BINARY_INTEGER);
Podamy kilka podstawowych funkcji i procedur z tego pakietu. Oto sygnatura funkcji otwierającej plik o nazwie filename.
UTL_FILE.FOPEN(
   location IN VARCHAR2,
   filename IN VARCHAR2,
   open_mode IN VARCHAR2)
RETURN UTL_FILE.FILE_TYPE;
Parametr location określa katalog, a parametr filename nazwę otwieranego pliku. Parametr open_mode określa tryb otwieranego pliku. Dostępne wartości to: 'a' – tryb dołączania (append), 'w' – tryb do zapisu (write), 'r' – tryb do odczytu (read).

Procedura GET_LINE umieszcza w buforze buffer kolejny fragment pliku tekstowego o uchwycie file_handle.

UTL_FILE.GET_LINE(
   file_handle IN FILE_TYPE,
   buffer OUT VARCHAR2);
Z kolei procedura PUT_LINE przesyła tekst będący zawartością bufora do pliku tekstowego o uchwycie file_handle.
UTL_FILE.PUT_LINE(
   file_handle IN FILE_TYPE,
   buffer IN VARCHAR2);
Procedura FCLOSE zamyka plik o wskazanym uchwycie file_handle.
UTL_FILE.FCLOSE (file_handle IN OUT FILE_TYPE);
Pokażemy teraz zastosowanie podprogramów rozważanego pakietu w procedurze zapisującej ślad wykonywania kodu PL/SQL do pliku tekstowego. Procedura jest użyteczna przy testowaniu i usuwaniu błędów z kodu PL/SQL. Użyte zostały standardowe wyjątki związane z podprogramami pakietu UTL_FILE (ich nazwy określają sytuacje, w których mają one miejsce).
PROCEDURE Trace(nazwa_pliku VARCHAR2, komunikat VARCHAR2)
IS
dir VARCHAR2(200);
filename VARCHAR2(200);
vText VARCHAR2(200);
file UTL_FILE.FILE_TYPE;
BEGIN
  dir := 'd:\orant\debug';
  BEGIN
    filename := nazwa_pliku;
    file := UTL_FILE.FOPEN(dir, filename, 'a');
  /* gdy plik o podanej nazwie nie istnieje, tworzony jest nowy plik i otwierany w trybie 'w' */
  EXCEPTION /* wyjątki przy FOPEN */
  WHEN UTL_FILE.Invalid_path THEN
    Raise_application_error(-20001,'niepoprawny katalog');
  WHEN others THEN
    Raise_application_error(-20002,'błąd w fopen');
END;
vText := TO_CHAR(Sysdate, 'DD.MM HH24:MI:SS')||'->'||msg;
BEGIN
  UTL_FILE.PUT_LINE(file, vText);
  EXCEPTION /* wyjątki przy PUT_LINE */
  WHEN UTL_FILE.Write_Error THEN
    Raise_application_error(-20003,'błąd OS podczas zapisu do pliku');
  WHEN others THEN
    Raise_application_error(-20004, 'niespodziewany błąd w PUT_LINE');
END;
BEGIN
  UTL_FILE.FCLOSE(file);
  EXCEPTION /* wyjątki przy fclose */
  WHEN UTL_FILE.Write_Error THEN
    Raise_application_error(-20005, 'błąd OS podczas zamykania pliku');
  WHEN others THEN
    Raise_application_error(-20006, 'niespodziewany błąd w FCLOSE');
END;
EXCEPTION /* wyjątki głównego bloku */
WHEN others THEN
  Raise_application_error(-20007, 'niespodziewany błąd: ' || sqlerrm);
END Trace;
Uwaga  Aby móc korzystać z plików, należy nazwy katalogów zarejestrować w pliku inicjalizacyjnym instancji Oracle. Na przykład, jeśli podamy (każdy katalog w osobnej linii, w następujących po sobie liniach w pliku):
UTL_FILE_DIR = c:\orant\plsql\kowalski
UTL_FILE_DIR = c:\orant\plsql\nowacki
wszyscy użytkownicy bazy danych uzyskują dostęp do wszystkich plików w podanych katalogach (muszą być więc godni zaufania).

 


5.5 Biblioteki (obiekty typu LIBRARY)

W kodzie PL/SQL można wykorzystywać podprogramy zewnętrznych bibliotek. W tym celu trzeba utworzyć obiekt biblioteki – rejestrując zewnętrzną bibliotekę na serwerze Oracle’a, a następnie należy utworzyć funkcję PL/SQL odpowiadającą funkcji zdefiniowanej w zewnętrznej bibliotece. Na przykład, instrukcja:

CREATE LIBRARY Odbc AS 'c:\windows\system\odbc.dll';
tworzy obiekt biblioteki w bazie danych i wiąże go z istniejącą biblioteką DLL. Kolejna instrukcja tworzy obiekt funkcji zewnętrznej zdefiniowanej w oparciu o tę bibliotekę.
CREATE OR REPLACE FUNCTION SQL_Exec_direct(
-- wykonaj instrukcję SQL używając ODBC
   sql_handle BINARY_INTEGER,
   sql_stat VARCHAR2(2000),
   sql_length INTEGER)
RETURN VARCHAR2
AS EXTERNAL
LIBRARY ODBC
NAME SQLExecDirect
LANGUAGE C;
I oto jej użycie w kodzie PL/SQL.
... polecenie := 'DELETE FROM Customers';
kod_wynikowy := SQL_Exec_direct(1, polecenie, LENGTH(polecenie));
/* wykonanie funkcji */

 


5.6 Java składowana w bazie danych

Kod Javy może być zapisywany w bazie danych oraz może być realizowany na serwerze bazy danych tak jak kod języka SQL – w ramach tej samej sesji i tej samej transakcji. Tworzenie połączenia z bazą danych jest realizowane za pomocą wbudowanego sterownika JDBC:

Connection conn = new OracleDriver().defaultConnection();
Oto pełny przykład. Najpierw zapisujemy poniższy kod w pliku GenericDrop.java i kompilujemy go:
import java.sql.*;
import java.io.*;
import oracle.jdbc.driver.*;
public class GenericDrop {
public static void dropIt (String object_type, String object_name)
 throws SQLException {
   // Połącz się z bazą danych Oracle używając sterownika JDBC
   Connection conn = new OracleDriver().defaultConnection();
   // Zbuduj instrukcję SQL
   String sql = "DROP " + object_type + " " + object_name;
   try {
     Statement stmt = conn.createStatement();
     stmt.executeUpdate(sql);
     stmt.close();
    }
   catch (SQLException e) {System.err.println(e.getMessage());}
 }
}
Klasa GenericDrop ma jedną statyczną metodę o nazwie DropIt, która usuwa z bazy danych dowolny jej obiekt. Na przykład, jeśli załączy się argumenty 'TABLE' i 'Emp', to metoda DropIt usunie tabelę Emp z bieżącego schematu.

Następny krok to załadowanie utworzonej klasy do bazy danych:

loadjava -user scott/tiger GenericDrop
Tworzy się obiekt bazy danych o nazwie klasy czyli GenericDrop.

Aby móc skorzystać z metody DropIt (statycznej metody w klasie GenericDrop), trzeba utworzyć dla niej procedurę lub funkcję, czyli inaczej mówiąc podać specyfikację jej wywołania. Specyfikacja wywołania określa, jakich typów danych SQL są argumenty i jaki jest typ wyniku (w przypadku funkcji).

CREATE OR REPLACE PROCEDURE DropIt(obj_type VARCHAR2, obj_name VARCHAR2)
AS LANGUAGE JAVA
NAME 'GenericDrop.dropIt(java.lang.String, java.lang.String)';
Do wywołania funkcji lub procedury PL/SQL służy instrukcja CALL. Jej postać dla procedur jest następująca:
CALL wywołanie procedury z argumentami;
Jej postać dla funkcji jest następująca:
CALL wywołanie funkcji z argumentami INTO :zmienna wiązania;
Można więc ich użyć w każdym miejscu, gdzie można użyć instrukcji SQL:
CALL DropIt('TABLE', 'Emp');
Druga metoda włożenia klasy Javy do bazy danych polega na użyciu następującej instrukcji bezpośrednio na serwerze bazy danych:
CREATE AND COMPILE JAVA SOURCE NAMED "Hello" AS
public class hello {
   public static String world() {
      return "Hello World"; }
};
a następnie określeniu specyfikacji wywołania odpowiedniej funkcji SQL:
CREATE OR REPLACE FUNCTION HelloWorld RETURN VARCHAR2 AS
LANGUAGE JAVA NAME 'hello.world() return java.lang.string';
Następnie możemy użyć funkcji HelloWorld tak jak każdej innej funkcji zdefiniowanej w bazie danych:
myString VARCHAR2;
CALL HelloWorld() INTO :myString;
PRINT myString;
lub
SELECT HelloWorld()
 FROM Dual;

 


5.7 Osadzony SQL

Jedną z metod dostępu do bazy danych z programu zewnętrznego jest osadzanie instrukcji SQL wprost w tekście programu języka. Nazwy zmiennych języka zewnętrznego w instrukcji osadzonego SQL muszą być poprzedzone dwukropkiem.

EXEC SQL BEGIN DECLARE SECTION;
SQLSTATE CHAR(5);
num_prac NUMBER(4);
EXEC SQL END DECLARE SECTION;
. . . . . . . . .
EXEC SQL DELETE FROM Emp WHERE Emp.Empno = :num_prac ;
IF SQLSTATE = '00000'
THEN . . . . . . ; /* operacja usuwania powiodła się */
ELSE . . . . . . ; /* wystąpił wyjątek */
Instrukcje osadzonego SQL są poprzedzane słowem kluczowym EXEC SQL (więc mogą być łatwo wyróżnione spośród innych instrukcji języka zewnętrznego) i są kończone na ogół średnikiem. Wszystkie zmienne zewnętrzne, które są używane w instrukcjach SQL, muszą być zdefiniowane w sekcji deklaracji, ograniczonej słowami kluczowymi BEGIN DECLARE SECTION i END DECLARE SECTION.

Każdy program osadzonego SQL powinien zawierać zmienną zewnętrzną o nazwie SQLCODE lub SQLSTATE. Po wykonaniu każdej instrukcji SQL do programu na zmiennej SQLCODE lub SQLSTATE (lub obu) jest przekazywana wartość statusu wykonania instrukcji SQL. Zmienne zewnętrzne i kolumny SQL mogą mieć takie same nazwy.

Są dwie możliwości obsługi wyjątków:

1. Po każdym wykonaniu instrukcji SQL sprawdzać wartości zmiennych SQLSTATE i SQLCODE.

2. Użyć instrukcji:

EXEC SQL WHENEVER warunek akcja;
gdzie warunek to SQLERROR lub NOT FOUND, akcja to CONTINUE albo GO TO etykieta.

Każda kolejna instrukcja WHENEVER zastępuje poprzednią instrukcję WHENEVER dla danego warunku.

Wrócimy teraz do przykładu osadzonego SQL dla Oracle. Poniższy program jest napisany w języku Pro*C, czyli języku C rozszerzonym o instrukcje osadzonego SQL i PL/SQL. Deklarowane zmienne mogą być używane zarówno w instrukcjach C (bez dwukropków), jak i w instrukcjach SQL i PL/SQL (z dwukropkami). Wartości typu VARCHAR są w instrukcjach języka C traktowane w specjalny sposób – jako rekordy złożone z napisu i jego długości. Na przykład, deklaracja zmiennej

VARCHAR emp_name(15);
jest przed kompilacją zamieniana na deklarację
struct{
unsigned short len;
unsigned char arr(15);
} emp_name;
Oto przykład programu w Pro*C.
EXEC SQL BEGIN DECLARE SECTION
VARCHAR username(20);
VARCHAR password(20);
VARCHAR emp_name(15);
float salary;
float commission;
EXEC SQL END DECLARE SECTION;
EXEC SQL INCLUDE SQLca;
main(){
  /* Wczytaj w C wartości username i password */
  ........
  EXEC SQL WHENEVER SQLERROR GOTO SQLerror;
  EXEC SQL CONNECT :username IDENTIFIED BY :password;
  EXEC SQL DECLARE Salespeople CURSOR FOR
  SELECT Ename, Sal, Comm
  FROM Emp
  WHERE Job LIKE 'SALES%';
  EXEC SQL OPEN Salespeople;
  EXEC SQL WHENEVER NOT FOUND GOTO end_of_fetch;
  printf("\nSalesperson Salary Commission\n");
  printf("-------------- ------- --------------\n");
  for ( ; ; ) {
    EXEC SQL FETCH salespeople
    INTO :emp_name, :salary, :commission;
    emp_name.arr(emp_name.len) = '\0';
    printf("%-11s%9.2f%13.2f\n",emp_name.arr,salary,commission);
  }
end_of_fetch: EXEC SQL CLOSE salespeople;
EXEC SQL COMMIT WORK RELEASE;
exit(0);
SQLerror: printf("\n\n% .70s \n\n", SQLca.SQLerrm.SQLerrmc);
EXEC SQL WHENEVER SQLERROR CONTINUE;
EXEC SQL ROLLBACK WORK RELEASE;
exit(1);
}

 


5.8 Interfejs poziomu wywołań

Standard określa interfejs poziomu wywołań w postaci sygnatury biblioteki funkcji do wykonywania operacji na bazie danych. Rozważane języki programowania to Ada, C, COBOL, FORTRAN, MUMPS, Pascal i PL/I. Oto przykład w języku C wykonania dynamicznie tworzonej w programie zewnętrznym instrukcji SQL za pomocą wywołania funkcji o nazwie SQLExecDirect:

char sqlsource[255];
........
/* Wpisz tekst instrukcji SQL, którą chcesz wykonać, na zmienną sqlsource */
rc = SQLExecDirect(hstmt, (SQLCHAR *) sqlsource, SQL_NTS);
Odpowiednikiem interfejsu poziomu wywołań w C jest w przypadku Oracle biblioteka o nazwie Oracle Call Interface czyli w skrócie OCI. Program OCI jest to program w C korzystający z biblioteki funkcji OCI do łączenia się z serwerem Oracle i wykonywania instrukcji SQL.

Oto spis czynności, które należy wykonać przy wykonywaniu instrukcji SQL za pomocą biblioteki funkcji OCI:

  1. Przygotowanie instrukcji do wykonania - OCIStmtPrepare();
  2. Wiązanie argumentów z parametrami - związać adres zmiennej wejściowej z każdym parametrem w instrukcji - OCIBindByPos(), OCIBindByName;
  3. Wykonanie instrukcji (również instrukcji DDL) OCIStmtExecute();
  4. Opisanie elementów wyniku (dla zapytań i gdy liczba tych elementów i ich typy danych nie są znane w czasie kompilacji programu) - PCIParamGet() i OCIAttrGet();
  5. Definiowanie zmiennych wyjściowych (dla zapytań) - OCIDefineByPos();
  6. Sprowadzanie wyniku zapytania - OCIStmtFetch().

Podstawowym pojęciem jest uchwyt (handle), który jest wskaźnikiem do struktury danych alokowanej i utrzymywanej przez bibliotekę OCI. Funkcje OCI mają dosyć skomplikowane sygnatury. Ich omówienie wykracza poza zakres tego wykładu. W charakterze przykładu podamy tylko wywołanie funkcji OCILogon, za pomocą którego nastąpi zalogowanie się do bazy danych określonej przez wartość zmiennej dbname na konto scott/tiger.

OCILogon(envhp, errhp, &svchp, "scott", nameLen, "tiger", passwdLen, dbname, dbnameLen)
Zauważmy, że w wywołaniu są używane trzy uchwyty:

 


5.9 JDBC – interfejs poziomu wywołań z języka Java

W związku z popularnością języka Java w dziedzinie aplikacji bazodanowych, zostały opracowane dwie wersje dostępu do bazy danych z programów języka Java, w postaci:

  1. języka osadzonego w Javie o nazwie SQLJ;
  2. interfejsu poziomu wywołań z języka Java o nazwie JDBC.

Szczególnie interfejs JDBC stał się bardzo popularny i jest używany między innymi w:

  1. programach aplikacyjnych napisanych w Javie po stronie klienta;
  2. appletach, czyli małych programach Javy realizowanych na przeglądarce użytkownika;
  3. serwletach, czyli programach Javy spełniających rolę małych serwerów aplikacyjnych, to jest programów realizujących po stronie serwera przez sieć zlecenia użytkowników;
  4. procedurach i funkcjach składowanych w bazie danych.

JDBC jest zbiorem klas i interfejsów w Javie, które umożliwiają dostęp do bazy danych z programów napisanych w Javie. Klasy te i interfejsy są częścią pakietu java.sql. W JDBC sterowniki źródeł danych są zarządzane przez klasę DriverManager, która utrzymuje listę wszystkich aktualnie załadowanych sterowników. Sterownik jest programem, który tłumaczy wywołania JDBC na wywołania specyficzne dla serwera baz danych. Klasa DriverManager ma metody RegisterDriver, DeregisterDriver i GetDrivers, które umożliwiają dynamiczne dodawanie i usuwanie sterowników.

Przed podłączeniem się do odpowiedniego źródła danych należy załadować odpowiedni sterownik JDBC. Realizuje się to albo używając metody RegisterDriver

DriverManager.RegisterDriver(new oracle.jdbc.driver.OracleDriver());
albo za pomocą mechanizmu Javy dynamicznego ładowania klas. Metoda statyczna ForName w klasie Class zwraca klasę Javy określoną w jej argumencie i wykonuje jej statyczny konstruktor. Statyczny konstruktor dynamicznie ładowanej klasy ładuje instancję klasy Driver i ten właśnie obiekt Driver rejestruje się sam w klasie DriverManager.
Class.ForName("oracle.jdbc.driver.OracleDriver");
Sesja z bazą danych rozpoczyna się od utworzenia obiektu Connection. Obiekt klasy Statement reprezentuje wykonywaną instrukcję SQL, a obiekt klasy ResultSet zwracany jeden po drugim wiersz zbioru wynikowego. Podobnie jak w przypadku zmiennej systemowej SQLSTATE, JDBC powoduje podniesienie wyjątku SQLException w przypadku wystąpienia błędu przy wykonywaniu instrukcji SQL.
// połącz się z bazą danych
Connection con=DriverManager.getConnection(url, "scott","tiger");
Statement stmt = con.createStatement();
// zbuduj obiekt reprezentujący instrukcję SQL
String query = "SELECT Ename, Sal FROM Emp";
ResultSet rs = stmt.executeQuery(query);
try { // obsłuż wyjątki
  // przejdź po wszystkich wierszach wynikowych
  while (rs.next()) {
    String s = rs.getString("Ename");
    Int n = rs.getFloat("Sal");
    System.out.println(s + ": " + n);
  }
} catch(SQLException ex) {
  System.out.println(ex.getMessage () + ex.getSQLState () + ex.getErrorCode ());
}
gdzie przykładowo
url = "jdbc:oracle:thin:@xeon.pjwstk.waw.pl:1521:ORCL"
lub
url = "jdbc:odbc:mojaBD"
gdzie sterownik jest typu mostu między JDBC a ODBC.

 


5.10 Podsumowanie

Wykład ten dokonał przeglądu możliwości programowania w samej bazie danych jak i w bezpośrednim środowisku związanym z bazą danych (na przykładzie serwera Oracle).
 


5.11 Słownik pojęć

dynamiczny SQL - konstrukcje języka SQL umożliwiające przygotowywanie i wykonywanie instrukcji SQL w trybie dynamicznym tzn. w trakcie realizacji kodu programu.

DBMS_JOB - pakiet służący do zlecania serwerowi bazy danych określonych zadań poza sesją zlecającego użytkownika.

DBMS_PIPE - pakiet umożliwiający zorganizowanie komunikacji między dwiema lub więcej sesjami w tej samej instancji Oracle poprzez dostęp w obu sesjach do wspólnego potoku.

UTL_FILE - pakiet umożliwiający zapis i odczyt plików tekstowych z kodu PL/SQL wykonywanego przez serwer bazy danych.

biblioteka - obiekt reprezentujący zewnętrzną bibliotekę programów na serwerze Oracle.

Java na serwerze bazy danych - kod Javy może być zapisywany w bazie danych oraz może być realizowany na serwerze bazy danych tak jak kod języka SQL – w ramach tej samej sesji i tej samej transakcji.

osadzony SQL - metoda dostępu do bazy danych z programu zewnętrznego przez osadzanie instrukcji SQL wprost w tekście programu języka programowania.

interfejs poziomu wywołań - metoda dostępu do bazy danych z programu zewnętrznego przez użycie specjalnej biblioteki funkcji do wykonywania operacji na bazie danych.

JDBC - standardowy interfejs poziomu wywołań z języka Java.
 


5.12 Sprawdzenie wiedzy

  1. Czy można w tekście programu dynamicznie przygotowywać instrukcje języka SQL i je wykonywać? Odpowiedź
  2. Jak się nazywa pakiet umożliwiający planowanie i wykonywanie w określonym czasie wcześniej przygotowanych fragmentów kodu? Odpowiedź
  3. Czy istnieje możliwość kontaktowania się w ramach sesji z innymi sesjami na tym samym serwerze bazy danych? Odpowiedź
  4. W jaki sposób można korzystać z kodu Javy w aplikacji bazodanowej? Odpowiedź
  5. Jakie są standardowe metody programistycznego dostępu do baz danych? Odpowiedź

 


5.13 Zadania

1. Zaznajom się z materiałem zawartym w materiałach Oracle (dostępne ze strony http://tahiti.oracle.com/):

  1. PL/SQL User's Guide and Reference, w szczególności z:
  2. Database Application Developer's Guide - Fundamentals, w szczególności z:

2. Opracuj aplikację działającą po stronie serwera bazy danych, która wykorzystuje co najmniej dwa pakiety opisane w tym wykładzie.



Strona przygotowana przez Lecha Banachowskiego, PJWSTK, 08/06/06 .