JDBC Agenda: Wprowadzenie JDBC VS ODBC Fundament innego API Zgodność z SQL Sterowniki (rodzaje) Użycie Wersje Połączenie Zapytania Procedury Transakcje Przykłady Wprowadzenie Java DataBase Connectivity (JDBC) jest interfejsem programistycznym, skonstruowanym w i przeznaczonym dla języka Java, który umożliwia ustandaryzowany dostęp do większości obecnych na rynku systemów bazodanowych. Wraz z powstaniem nowego, przenośnego, niezależnego od platformy i architektury języka programowania pojawiła się idea skonstruowania nowego interfejsu bazodanowego, który spełniałby podobne wymagania jak sam język. Naturalną konsekwencją takiego rozumowania było oderwanie się od już istniejących API, w szczególności wprowadzonego przez Microsoft standardu Open DataBase Connectivity (ODBC), i opracowanie nowej technologii, wykonanej w pełni w Javie. Jakkolwiek nie oznacza to, że programiści używający dotąd ODBC musieli przestawić się na zupełnie nowy tok myślenia. Przeciwnie, JDBC funkcjonalnie przypominało technologię ODBC, zwiększono jedynie możliwości interfejsu oraz przystosowano go do odmiennej specyfiki języka Java. JDBC VS ODBC Stworzenie nowego standardu dostępu do systemów zarządzania bazami danych (SZBD) w przeciwieństwie do zaadoptowania ODBC na potrzeby Javy wynikało między innymi z następujących spostrzeżeń: ODBC było technologią stworzoną i stosowaną w środowiskach powstałych w oparciu o języki C i C++. Aplikacje tworzone z ich użyciem są zależne od systemu operacyjnego, co kłóci się z jednym z fundamentów języka Java - przenośnością oprogramowania. Przepisanie ODBC w Javie nie miało sensu, z uwagi na różnice językowe (konstrukcyjne) pomiędzy Javą a C. ODBC, w oczach twórców Javy, było zbyt skomplikowane. Nowo powstały interfejs miał być znacznie prostszy w użyciu, a jednocześnie bardziej funkcjonalny. Fundament innego API JDBC jest wykorzystywane nie tylko bezpośrednio, jako zunifikowana technologia dostępu do dowolnej bazy danych, ale także jako budulec dla aplikacji/interfejsów wyższego rzędu, które umożliwiają dostęp do bazy danych z wyższego poziomu. Przykładami takich przedsięwzięć są m.in: SQLJ - SQL zaszyty w kodzie Javy. Kod ten jest następnie preprocesowany, tak by wydobyć zeń właściwe komendy SQL, za wykonanie których odpowiada JDBC. Zgodność z SQL JDBC określa pewien poziom zgodności z dotychczasowymi standardami SQL. Główne założenie mówi, iż każdy sterownik JDBC musi odpowiadać co najmniej wersji ANSI SQL-92 standardu SQL. Ponadto pomysłodawcy interfejsu poczynili inne założenia, które spełniają kolejne wersje standardu JDBC: Zapytania SQL przesyłane są do odpowiedniego SZBD bez względu na możliwość ich realizacji. W przypadku, gdy dany SZBD nie potrafi obsłużyć zlecenia przekazanego przez JDBC, aplikacja podnosi określony wyjątek. JDBC udostępnia informacje o SZBD i jego cechach szczególnych (ustawieniach, możliwościach itd.). Informacje te przekazywane są użytkownikowi w postaci tzw. metadanych. Sterowniki Sterownik JDBC jest zbiorem skompilowanych klas, które implementują wszystkie interfejsy zawarte w java.sql oraz przedefiniowują (ponownie implementują) pozostające tam klasy. Implementacja bezpośrednio zależy od systemu zarządzania bazą danych, z którą będzie się łączyć aplikacja wykorzystująca sterownik. Stąd każdy sterownik JDBC służy do komunikacji z konkretną bazą danych i nie jest wykorzystywany w przypadku baz innych producentów. Zastosowane rozwiązanie, choć podobne do technologii używanych w przypadku C++ lub innych języków, w przypadku Javy nabiera szerszego znaczenia. Ponieważ Java jest językiem w pełni przenośnym, producenci sterowników muszą przygotowywać tylko jeden pakiet dla każdej wersji standardu JDBC. Tym samym, w odróżnieniu np. od wspomnianych interfejsów dla języka C++, z jednego binarium korzystamy w systemie Linux i w Windows. Rodzaje sterowników 1. 2. Ponieważ nie wszyscy producenci systemów bazodanowych przygotowali javowe implementacje standardu JDBC, a niektórzy przygotowali rozwiązania hybrydowe, przyjęło się rozróżniać cztery zasadnicze typy sterowników JDBC: Mosty JDBC-ODBC (JDBC-ODBC bridge) korzystamy ze sterownika ODBC, z którym komunikuje się nasz most. Zapytania formułowane w Javie są tłumaczone na język sterownika ODBC. Wszelka komunikacja z bazą danych odbywa się poprzez sterownik ODBC. Java do API SZBD (Native-API partly-Java) sterowniki wykorzystują biblioteki napisane w C/C++. Ich używanie jak w przypadku powyżej wymaga dodatkowego oprogramowania. Rodzaje sterowników cd. 3. 4. Pośrednie JDBC (JDBC-Net pure Java) sterownik JDBC komunikuje się z serwerem pośredniczącym, za pomocą protokołu niezależnego od SZBD. Serwer tłumaczy polecenia na protokól konkretnego SZBD i przesyła do niego otrzymane zapytania. Serwer może pośredniczyć w wymianie danych pomiędzy wieloma klientami i wieloma różnymi SZBD. Tym samym jest to najbardziej ogólne i heterogeniczne rozwiązanie, obciążone względami bezpieczeństwa. Bezpośrednie JDBC (Native-protocol pure Java) sterownik JDBC komunikuje się bezpośrednio z bazą danych przy pomocy jej protokołu sieciowego. Rozwiązanie najbardziej ogólne, z powodzeniem stosowane w sieciach wewnętrznych. Sterowniki cd. Poniższy rysunek obrazuje wspomniane rozwiązania ujęte w modelu warstwowym: Użycie Przed skorzystaniem z klas java.sql, należy pobrać odpowiedni pakiet zawierający dany sterownik JDBC, a następnie umieścić go (zgodnie z hierarchią katalogów zawartą w archiwum) w katalogu widocznym dla maszyny wirtualnej Javy (zmienna CLASSPATH). Po wykonaniu tych operacji sterownik JDBC jest gotów do pracy. Każda aplikacja może go dynamicznie załadować. Do tego celu służy polecenie: Class.forName("pełna_nazwa_sterownika") gdzie, w przypadku bazy Oracle, pełna_nazwa_sterownika, to oracle.jdbc.driver.OracleDriver. Sterownik w pełni zgodny ze standardem JDBC, powinien wtedy stworzyć także instancje klasy. Ponieważ nie wszystkie sterowniki spełniają ten wymóg, warto samodzielnie powołać do życia obiekt klasy: Class.forName("oracle.jdbc.driver.OracleDriver").newInstance() Inną metodą na załadowanie sterownika jest ustalenie własności jdbc.drivers na nazwę ładowanej klasy: java -Djdbc.drivers="oracle.jdbc.driver.OracleDriver" program Wersje JDBC JDBC 1.0 Pierwsza wersja standardu JDBC. Pojawiła się na rynku wraz z wypuszczeniem JDK 1.0 (Java Development Kit). Zawierała podstawowy zestaw narzędzi umożliwiających dostęp do bazy danych. W pakiecie java.sql znalazły się następujące interfejsy: Connection DatabaseMetaData Driver DriverManager ResultSet ResultSetMetaData Statement CallableStatement PreparedStatement Types Wersja JDBC Standard JDBC 2.0 pojawił się wraz z Java SDK 1.2. Stanowi on funkcjonalne rozwinięcie wersji pierwszej, tak by sprostać wymaganiom stawianym przez pozostałe produkty Suna. Jego powstanie było w szczególności związane z wprowadzeniem przez Suna nowych technologii: Java Transaction Service (JTS), Java Naming and Directory Interface (JNDI), JavaBeans, czy też Enterprise JavaBeans (EJB). Do opublikowania nowej wersji JDBC przyczynił się także rozwój obsługi standardów wielojęzycznych. JDBC 2.0 podzielony jest na dwie części: JDBC 2.0 core API - zawarta w java.sql - posiada pełny zestaw mechanizmów dostępu do baz danych. Faktycznie składa się z części JDBC 1.2 uzupełnionej o szereg nowych funkcji. Połączenie Klasa Connection odpowiada pojedynczemu połączeniu z wybranym Systemem Zarządzania Bazą Danych. Po załadowaniu sterownika JDBC, użytkownik może połączyć się z bazą danych. Do tego celu służy metoda getConnection(...) z klasy DriverManager. W wyniku jej wywołania: Connection con = DriverManager.getConnection (url, username, password) dostajemy obiekt klasy Connection. Parametrami metody są: url - np: "jdbc:oracle:http://adres_hosta_z_baza_danych/nazwa_tabeli", czy też: "jdbc:mysql://localhost:3306/bazka". username - identyfikator użytkownika bazy danych password - hasło użytkownika bazy danych Zapytania Klasa Statement reprezentuje medium, służące do transmisji wszelkich zleceń do bazy danych (operacji SQL). Obiekt klasy Statement tworzony jest w oparciu o wsześniej ustanowione połączenie z bazą danych. Statement nie posiada samodzielnego konstruktora, nowy obiekt jest zwracany przez metodę createStatement(): Connection con = DriverManager.getConnection (...) Statement stmt = con.createStatement(); Od tego momentu wszystkie operacje związane z bazą danych wykonuje się poprzez utworzony obiekt. Oczywiście można utworzyć więcej niż jeden obiekt klasy Statement. Co więcej, jest to jedyna metoda, by złożyć kolejne zapytanie podczas przeglądania wyników poprzedniego. JDBC rozróżnia trzy odmienne kategorie zleceń do bazy danych. Kategorie te reprezentuje wspomniana klasa Statement i jej dwie podklasy: PreparedStatement oraz CallableStatement Zapytania cd. Do wykonywania operacji na bazie danych służą bezpośrednio trzy metody klasy: executeQuery(query), executeUpdate(query), execute(query), gdzie query jest treścią (String) zapytania. W szczególności: executeQuery(query) używana jest do składania zwykłych zapytań SQL rozpoczynających się od słowa select. W wyniku wykonania metody otrzymujemy listę wierszy opakowaną w obiekt klasy ResultSet. executeUpdate(query) używana jest do wykonywania operacji: insert, update i delete, a także operacji DDL (Data Definition Language): create table, drop table, czy też alter table. W wyniku zwraca pojedynczą liczbę, określającą liczbę wierszy tabeli, której dotyczyło zapytanie. execute(query) używana jest rzadko, wszędzie tam, gdzie w wyniku otrzymujemy więcej niż jedną listę wierszy lub więcej niż jedną liczbę zmodyfikowanych wierszy Zapytania - ResultSet Klasa ta reprezentuje podstawową strukturę danych wynikowych dla zapytań SQL. Intuicyjnie kojarzona z bazodanowym kursorem (iterator), udostępnia szereg metod pozwalających na przetwarzanie otrzymanych danych. Pojedynczy element iteratora odpowiada jednemu wierszowi wynikowej tabeli. Obiekt klasy ResultSet otrzymujemy w wyniku wykonania metody: executeQuery(). Rodzina metod getXXX (i) służy do uzyskiwania wartości i-tej kolumny bieżącego wiersza wynikowej tabeli - np. getInt(1) oznacza pytanie o wartość całkowitą znajdującą się w pierwszej kolumnie aktualnego wiersza. Do następnego wiersza przesuwamy się korzystając z metody next(), uprzednio sprawdziwszy, czy w kursorze pozostały jeszcze wiersze (hasNext()). ScrollableResultSet Klasa rozszerza funkcjonalność ResultSet o możliwość elastycznego poruszania się po otrzymanej liście wierszy oraz pozwala na modyfikacje tabeli w trakcie przeglądania wyników zapytania. O rodzaju zwracanego obiektu, a więc wyborze pomiędzy klasą ResultSet a ScrollableResultSet decyduje metoda tworząca kontener Statement. Użycie bezparametrowego wywołania createStatement () jest rozwiązaniem standardowym. Natomiast zastosowanie createStatement (int jaki_rezultat, int jaka_wspolbieznosc) tworzy kontener zdolny do tworzenia elastycznych wyników. Pierwszy argument decyduje o możliwości dwukierunkowego przewijania kursora oraz reakcji na zmiany dokonane w bazie danych po pobraniu danych. Dopuszczalne są stałe: TYPE_FORWARD_ONLY Klasyczne rozwiązanie, brak możliwości cofania kursora TYPE_SCROLL_INSENSITIVE Po kursorze możemy się dowolnie poruszać, włącznie z przemieszczaniem do dowolnego wiersza. Zmiany zaistniałe w bazie danych po wykonaniu zapytania nie wpływają na zawartość kursora.(TYPE_SCROLL_SENSITIVE) ScrollableResultSet Drugi argument metody określa poziom współbieżności. Możliwe są dwie stałe: CONCUR_READ_ONLY Rozwiązanie klasyczne - działa w parze z opcją TYPE_FORWARD_ONLY CONCUR_UPDATABLE Pozwala użytkownikowi na wykonywanie tzw. programowych operacji zapisu/modyfikacji/usunięcia danych znajdujących się bezpośrednio w bazie danych. Jest to istotne rozszerzenie w stosunku do wersji 1.0 JDBC. Pozwala m.in. na obudowywanie kursorów warstwą prezentacyjną (formatka). Do programowych modyfikacji odpowiednich kolumn bazy danych służą metody postaci updateXXX (pozycja, wartosc) oraz insertRow () i deleteRow (). PrepareStatement Dla usprawnienia procesu przesyłania danych wprowadzono mechanizm wstępnego przetwarzania zapytań. Zapytanie, po przesyłaniu do bazy danych, jest prekompilowane i od tego momentu użytkownik nie musi go przesyłać po raz kolejny. Oczywiście zastosowania tego mechanizmu ograniczają się do wąskiego zakresu przypadków: treść zapytania nie zmienia się w czasie - zapytanie jest wielokrotnie ponawiane (procedura bezparametrowa) zapytanie jest przetwarzane bardzo często, jego treść można rozsądnie sparametryzować W obu przypadkach warto rozważyć użycie osadzonych w SZBD, prekompilowanych zapytań. Parametry wejściowe procedur kodujemy jako znaki zapytania ("?"). Ustawienie odpowiednich wartości dla brakujących argumentów odbywa się poprzez metody z rodziny setXXX (pozycja, wartosc), gdzie pozycja jest kolejnym (licząc od lewej, począwszy od 1) argumentem zapytania. PrepareStatement - przykład pstmt = con.prepareStatement ( "update nazwa_tabeli set kol_1 = ? and kol_2 = ? where jakas_wartosc = ?" ); pstmt.setString (1, "cos"); pstmt.setInt (2, 123); pstmt.setInt (3, 8395); pstmt.executeUpdate (); Procedury JDBC umożliwia korzystanie z funkcji skalarnych wbudowanych w SZBD. Sterowniki spełniające warunki zgodności z JDBC (JDBC Compliant) muszą zapewniać poprawność działania z funkcjami skalarnymi, o ile odpowiadający im SZBD funkcje te udostępnia. Z uwagi na różnorodność wywołań funkcji u różnych producentów baz danych, JDBC wprowadziło tzw. sekwencje rozszerzające (escape sequences) - składniowe ramy dla wywołań funkcji i procedur składowanych, przekazywania daty, czasu itp. W szczególności, wywołanie funkcji opatrzone jest następującą klauzulą: {fn <nazwa_funkcji ()>} np. Select {fn concat (string, "napis")} From... Procedury składowane Poza funkcjami skalarnymi, JDBC umożliwia wykorzystanie procedur składowanych (stored procedures) - procedur i funkcji zgromadzonych po stronie SZBD. Wołanie procedur oraz przekazywanie parametrów określają reguły składniowe sekwencji rozszerzających. Nośnikiem dla zapytań zawierających wywołania procedur są obiekty klasy CallableStatement, stanowiącej rozszerzenie klasy Statement. Po utworzeniu obiektu wspomnianej klasy należy przygotować treść zapytania: ? = call nazwa_procedury ( ? ? ) Procedury składowane cd. ? = call nazwa_procedury ( ? ? ) Znaki zapytania odpowiadają zmiennym przekazywanym procedurze. JDBC dopuszcza zmienne trzech typów: IN Wartości zmiennych przekazywane są procedurze. Do ustawiania argumentu typu IN stosujemy metody postaci setXXX (pozycja, wartosc), gdzie pozycja, określa do którego znaku "?" przyporządkowana będzie wartosc OUT Z argumentem wiążemy zmienną programu - po wykonaniu procedury zmienna będzie miała odpowiednio zmodyfikowaną wartość. Do związania zmiennej używa się polecenia: registerOutParameter (pozycja, typ), gdzie pozycja jest określona jak dla argumentu typu IN, a typ odpowiada jednemu z typów JDBC. INOUT Argument spełnia obie powyższe role. Dla argumentu określonego jako INOUT musi określić wysyłaną wartość (setXXX (p, w)), jak i związać z nim zmienną Javy (registerOutParameter (p, t)) Procedury składowe - przykład CallableStatement cs = con.prepareCall (" {? = call nazwa_procedury (? ?)} "); cs.registerParameterOut (1, Types.INTEGER); cs.setString (2, "arg_napisowy"); cs.setString (3, "2_arg_napisowy"); cs.registerParameterOut (3, Types.STRING); cs.execute (); int wynik = cs.getInt (1); String arg_2 = cs.getString (3); Transakcje JDBC domyślnie ogranicza pojęcie transakcji do pojedynczej operacji. Jej pomyślne wykonanie oznacza zatwierdzenie efektów operacji - jest to tzw. tryb autocommit. Dokładniej, efekty operacji zostają zatwierdzone dopiero po pobraniu ostatniego wiersza wynikowego kursora (ResultSet) lub po jego zamknięciu. Aby posłużyć się rozszerzonym pojęciem transakcji, należy wyłączyć tryb samozatwierdzania, wywołując metodę klasy Connection: con.setAutoCommit(false); Zatwierdzenie ciągu operacji wykonanych od ostatniego zatwierdzania transakcji odbywa sie poprzez wywołanie metody: Connection.commit(); Transakcje wycofujemy wywołaniem: Connection.rollback(); Przykład Statement stm = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet. CONCUR_READ_ONLY); String sql = „select * from tabela”; ResultSet rs = stm.executeQuery(sql); rs.afterLast(); while(rs.previous()){ System.out.println(„Wiek” + rs.getInt(„wiek”)); Przykład Wybrane medody obiektu ResultSet: rs.Next(); rs.previous(); rs.afterLast(); rs.beforeFirst(); rs.getXXX(); //numer argumentu zapytanie lub nazwa kolumny rs.absolute(int a); rs.relative(int a); rs.first(); rs.last(); rs.updateXXX(„nazwa_kol”,wartosc); rs.updateRow(); rs.insertRow(); rs.deleteRow(); rs.refreshRow(); Przykład Aktualizacja za pomocą obiektu ResultSet: String sql = „Select * from osoba”; ResultSet rs = stm.executeQuery(sql); rs.first(); String nazwisko = rs.getString(„Nazwisko”); rs.updateString(„Nazwisko”, „Pan” + nazwisko); rs.updateInt(„Wiek”, 22); rs.updateRow(); Usuwanie za pomocą obiektu ResultSet: rs.absolute(4); rs.deleteRow(); Przykład Wstawianie rekordu za pomocą ResultSet: String sql = „select * from osoba”; ResultSet rs = stm.executeQuery(sql); Rs.moveToInsertRow(); rs.updateInt(„Wiek”, 34); rs.updateString(„Programista Java”); rs.insetRow(); Odświerzanie danych za pomocą obiektu ResultSet: rs.absolute(5); rs.refreshRow(); Modyfikacja a bezpieczeństowo transakcji: String sql = „Select from osoba FOR UPDATE”; Przykład Batch update: con.setAutoCommit(false); try{ Statement stm = con.createStatement(); stm.addBatch(insert into osoba(imie,nazwisko) values(„Jan”,Kowalski”); stm.addBatch(delete from osoba where nazwisko=‘Nowak’”); stm.addBatch(update osoba set imie=„Witold” where nazwisko=‘Kowalski’”); stm.executeBatch(); //stm.clearBatch(); }catch(BatchUpdateException e){ int[] count = e.getUpdateCounts(); } Przykład Metadane: Statement stm = con.createStatement(); String sql = „select * from osoba”: ResultSet rs = stm.executeQuery(sql); ResultSetMetaData meta = rs.getMetaData(); int col = meta.getColumnCount(); for(int i = 0;i < col;i++){ System.out.println(meta.getColumnLabel()): } Przykład CREATE TYPE OsobaProp( waga Number, Wiek Numer); Struktury: public class DaneOsoboweObj implements SQLData{ private String sql_type; int waga; int wiek; DaneOsobowe(){} DaneOsobowe(String sql_type, int waga, int wiek){ this.sql_type = sql_type; this.waga = waga; this.wiek=wiek; } public String getSQLTypeName() throws SQLException{ return sql_type; } public void readSQL(SQLInput stream, String typeName) thows SQLException{ sql_type = typeName; waga = stream.getInt(); wiek = stream.getInt(); } public void writeSQL(SQLOutput stream) throws SQLException{ stream.writeInt(waga); stream.writeInt(wiek); } Przykład struktury cd. Map map = con.getTypeMap(); map.put(„OsobaProp”,Class.forName(„DaneOsoboweObj”)); Statement stm = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet rs = stm.executeQuery(„select Dane from Osoba where nazwisko = ‘Nowak’”); rs.absolute(1); DaneOsoboweObj daneOs = (DaneOsoboweObj)rs.getObject(„Dane”); System.out.println(„Nazwisko” + daneOs.nazwisko); Aktualizacja: daneOs = new DaneOsoboweObj(„Adam”, „Mickiewicz”); rs.exectueQuery(„Update Osoba set Dane = daneOs where nazwiko=„Nowak”); stm.execute(); KONIEC