Technologia JDBC w praktyce Cezary Bronny MSUI II Nr indeksu 227567 Spis treści 1. Wstęp ........................................................................................... 3 2. Sterowniki..................................................................................... 4 3. Ustanawianie połączenia z bazą danych .......................................... 5 4. Wykonywanie poleceń SQL............................................................. 7 4.1. Wykonywanie polecenia select..................................................... 7 4.2. Wykonywanie poleceń insert, update i delete ............................... 8 4.3. Wykonywanie DDL...................................................................... 9 4.4. Wykonywanie procedur bazodanowych ........................................ 9 5. Zarządzanie transakcjami ..............................................................10 6. Obsługiwanie wyjątków ................................................................12 7. Bibliografia...................................................................................14 2 1. Wstęp Java, jako uniwersalny język programowania, daje moŜliwość dostępu do baz danych. Przenośność aplikacji bazodanowych tworzonych w Javie zapewnia interfejs JDBC – opracowany przez firmę Sun Microsystems w 1996 roku. UmoŜliwia on konstruowanie i wykonywanie poleceń SQL’owych z poziomu kodu Javy. Dzięki JDBC aplikacje bazodanowe napisane w Javie są niezaleŜne od sprzętu oraz stosowanej bazy danych (niezaleŜność od systemu operacyjnego zapewnia sama Java). NaleŜy jednak zaznaczyć, iŜ nadal moŜliwe jest stworzenie specyficznych wywołań lub uŜycie typów danych, występujących tylko w przypadku konkretnej bazy danych. Technologia JDBC znajduje zastosowanie między innymi w: programach aplikacyjnych napisanych w Javie apletach, czyli programach osadzonych na stronie WWW i uruchamianych przez przeglądarkę internetową serwletach osadzonych po stronie serwera w tzw. kontenerach serwletów/serwerach aplikacyjnych procedurach i funkcjach napisanych przy uŜyciu języka Java i składowanych w bazie danych W niniejszej pracy przedstawiono praktyczne informacje dotyczące interfejsu JDBC, tj. jak przygotować środowisko programistyczne do korzystania z interfejsu, jak ustanawiać połączenie z bazą danych, jak wykonywać polecenia SQL oraz w jaki sposób moŜna zarządzać transakcjami. Materiał zawiera równieŜ opis wyjątków zgłaszanych przez interfejs. 3 2. Sterowniki Fizycznie JDBC jest zbiorem klas i interfejsów, które umoŜliwiają dostęp do bazy danych z programów napisanych w języku Java. Są one zawarte w pakietach java.sql oraz javax.sql. Aby moŜliwe było ich uŜycie, najpierw naleŜy załadować odpowiedni sterownik JDBC, którego klasy implementują interfejsy oraz przedefiniowują klasy znajdujące się w tychŜe pakietach. Co waŜne, sterowniki JDBC nie są uniwersalne – współpracują z jednym konkretnym źródłem danych. Jeśli tym źródłem jest baza danych to sterownik jest zazwyczaj przygotowany przez jej producenta (ale nie jest to regułą). Ze względu na fakt, Ŝe nie wszyscy producenci systemów bazodanowych przygotowali implementacje swojego interfejsu JDBC, a niektórzy przygotowali rozwiązania hybrydowe, moŜna wyróŜnić cztery rodzaje sterowników. Są nimi: JDBC-ODBC Bridge Driver – sterownik tego typu wykorzystuje sterownik ODBC do komunikacji z bazą danych. Sterownik JDBC tego typu pełni jedynie rolę pośrednika tłumaczącego kod napisany w Javie na język sterownika ODBC. Native-API Partly Java Driver – sterownik korzysta z bibliotek napisanych w innych językach. Dla Oracle, biblioteki kodu własnego mogłyby bazować na bibliotekach OCI, które były pierwotnie zaprojektowane dla programistów C/C++. JDBC-ODBC Bridge po lewej i Native-API Partly Java Driver po prawej 4 Net – Protocoll All-Java Driver – ten sterownik komunikuje się z bazą danych za pośrednictwem komponentów poprzez protokół sieciowy. Komponenty pośrednie zapewniają dostęp do róŜnych baz danych. Sterowniki tego typu są w całości napisane w Javie i nie wymagają instalacji dodatkowego oprogramowania po stronie klienta. Native – Protocoll All Java Driver – sterownik jest w całości napisany w Javie i komunikuje się bezpośrednio z serwerem bazodanowym za pomocą protokołów sieciowych. Native po lewej i Net – Protocoll All-Java Driver po prawej 3. Ustanawianie połączenia z bazą danych Ustanawianie połączenia z bazą danych zawsze odbywa się w dwóch krokach. Najpierw naleŜy załadować sterowniki JDBC a następnie nawiązać połączenie z bazą. Krok pierwszy wykonujemy wywołując polecenie: Class.forName("nazwa_sterownika"); W przypadku ładowania sterownika – mostu JDBC-ODBC, wykonamy: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); a w przypadku sterownika do bazy Oraclowej: Class.forName("oracle.jdbc.driver.OracleDriver"); 5 W następnym kroku moŜemy przejść do nawiązania połączenia z bazą danych. Do tego celu moŜemy uŜyć metody getConnection z klasy DriverManager. Parametrami metody są: URL do bazy danych oraz nazwa uŜytkownika i hasło. Adres URL identyfikuje bazę danych w sposób specyficzny dla określonego sterownika. RóŜne sterowniki mogą potrzebować róŜnych informacji zawartych w URL do określenia hosta bazy danych. Adresy URL zazwyczaj zaczynają się od jdbc:subprotokół:subnazwa. Subprotokół określa nazwę wykorzystywanego sterownika, a subnazwa identyfikuje bazę danych. Na przykład, sterownik Oracle JDBC-Thin wymaga URL w formie jdbc:oracle:thin:@dbhost:port:sid, a JDBCODBC Bridge uŜywa jdbc:odbc:datasourcename:odbcoptions. PoniŜszy fragment kodu nawiązuje połączenie z bazą danych: Class.forName(„sun.jdbc.odbc.JdbcOdbcDriver”); Connection con; con=DriverManager.getConnection("jdbc:odbc:bazaCB", "uzykownik", "haslo"); Innym sposobem na nawiązanie połączenia jest skorzystanie z obiektu DataSource. Aby to zrobić, naleŜy najpierw zarejestrować go w usłudze nazewniczej JNDI (czyli Java Naming and Directory Interface). Dzięki temu aplikacja nie będzie musiała znać Ŝadnych szczegółów dotyczących sterownika jak to miało miejsce w przypadku obiektu DriverManager. Wystarczy jej nazwa logiczna, która zostanie odwzorowana dzięki usłudze JNDI w fizyczny obiekt DataSource reprezentujący konkretne źródło danych. Dodatkowo DataSource umoŜliwia utrzymanie tzw. puli połączeń do bazy. Mechanizm puli połączeń (ang. connection pooling) moŜe mieć zasadniczy wpływ na wydajność aplikacji gdyŜ ponownie wykorzystuje zwrócone wcześniej połączenia (połączenia nie są zamykane lecz zwracane do puli). Zamiast za kaŜdym razem fizycznie otwierać nowe połączenie, obiekt DataSource przekazuje po prostu referencję do aktywnego, nawiązanego juŜ wcześniej połączenia. PoniŜej kod pozyskujący połączenie z puli połączeń. 6 Context ctx = new InitialContext(); //inicjalizacja środowiska dla JNDI DataSource ds; ds = (DataSource) ctx.lookup("jdbc/bazaCB"); /* jdbc/bazaCB to logiczna nazwa obiektu reprezentującego źródło danych, który został wcześniej zarejestrowany w usłudze JNDI */ Connection con = ds.getConnection("cb227567", " haslo"); //pobranie połączenia 4. Wykonywanie poleceń SQL Po uzyskaniu połączenia z bazą danych moŜna zacząć operować na bazie danych. Interfejs Statement reprezentuje podstawowe medium, słuŜące do transmisji wszelkich zleceń do bazy danych. Statement nie posiada samodzielnego konstruktora – jest tworzony za pomocą metody createStatement wcześniej pozyskanego obiektu Connection. Statement stmt = con.createStatement(); 4.1. Wykonywanie polecenia select Do wykonania polecenia select słuŜy metoda executeQuery obiektu Statement. Wynikiem jej wykonania jest obiekt typu ResultSet, który jest listą zwróconych rekordów. ResultSet rs = stmt.executeQuery("SELECT * FROM KLIENCI"); UŜycie metody next obiektu ResultSet pozwala na przemieszczanie się pomiędzy wierszami danych reprezentowanych przez ResultSet. Metoda ta w wyniku wykonania zwraca wartość typu boolean w zaleŜności czy udało się jej wykonać przejście do następnego rekordu. Odczytanie danych z bazy moŜe zatem wyglądać następująco: 7 // deklaracja zmiennych String piwo, bar; float cena; // wywołujemy zapytanie ResultSet rs= stmt.executeQuery("SELECT nazwa_baru, nazwa_piwa, cena_piwa” + “ FROM PIWO_W_BARACH"); while( rs.next() ) { // w petli przechodzimy po kolejnych rekordach uzyskanych z zapytania bar = rs.getString("nazwa_baru"); // przypisujemy na zmienną bar wartość z kolumny nazwa_baru piwo = rs.getString("nazwa_piwa"); // podobnie jw cena = rs.getFloat("cena_piwa"); //wypisanie informacji na standardowe wyjście System.out.print(”W barze ” + bar + ” jest piwko ” + piwo); System.out.println(” w cenie ” + cena + ” za kufel.”); } NaleŜy pamiętać, Ŝe obiekt ResultSet jest związany z obiektem Statement. Jeśli zatem obiekt Statement zostanie zamknięty lub wykorzystany do obsługi innego zapytania to powiązany z nim obiekt ResultSet równieŜ zostanie zamknięty. 4.2. Wykonywanie poleceń insert, update i delete Wykonanie operacji insert, update i delete jest bardzo podobne do select z poprzedniego podrozdziału. RóŜnica polega na tym, Ŝe zamiast wywoływania metody executeQuery na obiekcie Statement, wołamy executeUpdate. RównieŜ i w tym przypadku parametrem metody jest polecenie SQL. String sql = ”INSERT INTO PIWO_W_BARACH VALUES ('Bar u Ździśka', 'Dębowe Mocne', '2.8')”; int insertedRows = stmt.executeUpdate(sql); 8 Zwracaną wartością jest w tym przypadku ilość rekordów które zostały wstawione. W przypadku DELETE wartością zwróconą będzie ilość usuniętych rekordów. Natomiast wykonanie UPDATE zwróci liczbę zmodyfikowanych rekordów. 4.3. Wykonywanie DDL Polecenia DDL są instrukcjami SQL umoŜliwiającymi manipulowanie strukturami przechowującymi dane (w odróŜnieniu od DML, które realizują operacje na danych). NaleŜą do nich polecenia CREATE, ALTER oraz DROP. Zlecenie wykonania ich przez serwer bazy danych zasadniczo nie róŜni się od wywołania polecenia INSERT czy UPDATE. Realizujemy je np. tak: Statement stmt = con.createStatement(); String sql = "drop table piwo_w_barach"; stmt.executeUpdate(sql); 4.4. Wykonywanie procedur bazodanowych Serwery bazodanowe często posiadają swój własny język programowania, który umoŜliwia definiowanie tzw. procedur bazodanowych. Utworzone procedury mogą być wywoływane przez zewnętrzne aplikacje. Jednym z przykładów jest język PL/SQL dla bazy Oracle. Bazodanowe języki programowania są często lepiej przystosowane do wykonania pewnych akcji na bazie danych. Interfejs JDBC umoŜliwia wywoływanie takich procedur. Realizowane to jest przez obiekt implementujący interfejs CallableStatement. Bazodanową procedurę o nagłówku: uzupelnij_zapasy_piwa(id IN INTEGER, ile IN INTEGER, nazwa OUT VARCHAR2) moŜna wywołać w następujący sposób: CalableStatement ctmt = con.prepareCall("{call uzupelnij_zapasy_piwa (?,?,?)}"); // w miejsce znaków ? zostaną wstawione parametry dla wołanej procedury 9 ctmt.setInt(1, idZapasuPiwa); // pierwszym parametrem będzie wartość zmiennej idZapasuPiwa ctmt.setInt(2, dodatkowyZapasPiwa); // drugim dodatkowyZapasPiwa ctmt.registerOutParameter(3, java.sql.Types.VARCHAR); // trzecim parametrem procedury będzie parametr typu OUT VARCHAR ctmt.execute(); System.out.println("Zwiększono zapas piwa " + ctmt.getString(3)); // pobranie wartości parametru nr 3 i wyświetlenie informacji na standardowe wyjście Metoda prepareCall obiektu Connection słuŜy do tworzenia obiektów CallableStatement. W miejsce znaków zapytania (parametr metody prepareCall) wstawiane są określone później wartości. Do określania tych wartości słuŜą metody setXXX obiektu CallableStatement, gdzie XXX to nazwa wstawianego typu. I tak np. ctmt.setInt(1, idZapasuPiwa) wstawi pod pierwsze wystąpienie znaku zapytania wartość zmiennej typu integer – idZapasuPiwa. Warto zwrócić uwagę na metodę registerOutParameter. Ustawia ona typ parametru wyjściowego procedury bazodanowej (OUT). Po zleceniu systemowi zarządzania bazą danych wykonania procedury (metoda execute), i faktycznym jej wykonaniu, parametry wyjściowe procedury moŜna pobrać metodą getXXX (XXX – nazwa typu) obiektu CallableStatement. 5. Zarządzanie transakcjami W JDBC domyślnie kaŜda pojedyncza operacja jest transakcją i jest zatwierdzana automatycznie (ang. autocommit) po wywołaniu metod execute/executeUpdate i pomyślnym wykonaniu jej przez serwer bazy danych. Gdy chcemy Ŝeby ciąg jakiś operacji wykonał się w całości albo wcale (np. przelew pieniędzy w banku z konta na konto), tryb taki nie będzie odpowiedni. MoŜna go zmienić wywołując metodę setAutoCommit obiektu klasy Connection z parametrem false. Do zatwierdzania transakcji słuŜy metoda commit a do wycofań rollback. Przykład wykorzystania: 10 try { // załoŜenie ze obiekt con klasy Connection został wcześniej zainicjalizowany con.setAutoCommit(false); //wyłączenie automatycznego zatwierdzania transakcji Statement stmt = con.createStatement(); stmt.executeUpdate("UPDATE MAGAZYN SET ILOŚĆ = (ILOŚĆ - 10) WHERE PRODUCTID = 7"); // wykonanie modyfikacji danych tabeli MAGAZYN stmt.executeUpdate("UPDATE WYSYŁKA SET WYSŁANE = (WYSŁANE + 10) WHERE PRODUCTID = 7"); // wykonanie modyfikacji danych tabeli WYSYŁKA con.commit(); // zatwierdzamy wykonanie transakcji System.out.println("Operacja zamówienia zakończona sukcesem”); } catch(Exception e) { // wystąpił błąd try { con.rollback(); // wycofujemy transakcje } catch (SQLException ignored) {} System transakcyjny zaimplementowany w JDBC moŜe działać w róŜnych poziomach izolacji. Do wyboru mamy: TRANSACTION_NONE TRANSACTION_READ_COMMITTED TRANSACTION_READ_UNCOMMITTED TRANSACTION_REPEATABLE_READ TRANSACTION_SERIALIZABLE Odpowiedni poziom moŜemy ustawić za pomocą metody: Connection.setTransactionIsolation (odpowiednia stała z listy powyŜej); Warunkiem moŜliwości zastosowania wybranego poziomu izolacji jest konieczność wsparcia przez system zarządzania bazą danych. 11 Czasami moŜe być przydatny rollback części transakcji a nie całej. Specyfikacja JDBC w wersji 3.0 określa taką moŜliwość – nosi ona nazwę transaction savepoints. Savepoint to nazwany punkt wewnątrz transakcji. Ustawia się go metodą setSavepoints(nazwa_savepointu) naleŜącą do obiektu klasy Connection. MoŜliwość cofnięcia transakcji do savepointu realizowane jest za pomocą metody rollback(nazwa_savepointu). …. con.setAutoCommit(false); //wyłączenie automatycznego zatwierdzania transakcji Statement stmt = con.createStatement(); Savepoint save1 = con.setSavepoint("INSERT_POINT"); // ustawiamy savepoint stmt.executeUpdate(“INSERT INTO ...”); //wykonujemy operacje na bazie danych Savepoint save2 = con.setSavepoint("UPDATE_POINT"); // ustawiamy kolejny savepoint stmt.executeUpdate(“UPDATE ...”); //ponownie wykonujemy operacje na bazie .... con.rollback(save2); // rollback do savepointa save2 .... con.commit(); // zatwierdzenie transakcji Tak jak w poprzednim przykładzie dotyczącym izolacji transakji, i tu, zarówno baza danych jak i sterowniki JDBC muszą wspierać savepointy. 6. Obsługiwanie wyjątków W trakcie działania aplikacji bazodanowej mogą zaistnieć róŜnego rodzaju sytuacje powodujące, Ŝe określone operacje na bazie zakończą się niepowodzeniem (np. próba utworzenia tabeli o istniejącej juŜ nazwie albo próba wstawienia nieunikatowej wartości do kolumny klucza głównego tabeli). Dlatego waŜne jest, aby nasza aplikacja potrafiła obsłuŜyć tego typu zdarzenia. Pomocny 12 okazuje się mechanizm obsługi wyjątków. W takich przypadkach wykorzystujemy klasę SQLException. Dziedziczy ona bezpośrednio z klasy Exception. Wyjątki SQL’owe obsługujemy tak, jak wszystkie inne, z którymi spotkać moŜemy się w Javie. try { // operacje na bazie danych } catch (SQLException e) { // tu obsługujemy przechwycony wyjątek } 13 7. Bibliografia JDBC Technology – http://java.sun.com/products/jdbc/index.jsp Java Server Programming - Jason Hunter, William Crawford Java Enterprise in a Nutshell, 3rd Edition - William Crawford, Jim Farley Introduction to JDBC – http://www-db.stanford.edu/~ullman/fcdb/oracle/orjdbc.html Wikipedia – http://pl.wikipedia.org JDBC – Marek Misiowiec – http://stencel.mimuw.edu.pl/abwi/20011218.b.JDBC/ 14