r09 05 (10)


W tym rozdziale:

  • Relacyjne bazy danych

  • Interfejs JDBC

  • Powtórne użycie obiektów bazy danych

  • Transakcje

  • Serwlet Księga gości

  • Zaawansowane techniki JDBC

  • Co dalej?

Rozdział 9.

Łączność z bazą danych

W dzisiejszych czasach trudno znaleźć profesjonalną witrynę, która nie ma powiązania z bazą danych. Administratorzy WWW korzystają z baz danych w wyszukiwarkach internetowych, aplikacjach sklepów internetowych i programów wysyłających wiadomości online. Współpraca aplikacji sieciowych z bazami danych ma swoją cenę: zorientowane bazodanowo witryny internetowe mogą być trudne do tworzenia i często powodują ograniczenie wydajności. Korzystanie z baz danych ma jednak więcej zalet niż wad co sprawia, że bazy danych coraz bardziej „sterują” siecią.

W niniejszym rozdziale przedstawiono relacyjne bazy danych, język zapytań SQL i interfejs JDBC. Serwlety, dzięki długiemu czasowi istnienia, oraz JDBC, dobrze zdefiniowany interfejs łączności z bazami danych, są skutecznym rozwiązaniem dla administratorów WWW pragnących wykorzystać bazy danych w swych witrynach. Wprawdzie w książce założono, że czytelnik zna Javę, ale ten rozdział zaczyna się krótkim kursem wprowadzającym interfejs programistyczny JDBC.

Największą zaletą serwletów jest ich czas istnienia (szeroko opisany w rozdziale 3,”Cykl Życia Serwletu”), który pozwala na zarządzanie pulą otwartych połączeń. Takie otwarte połączenie pozwala na szybszą komunikację aplikacji z bazą danych. Korzystając ze skryptów CGI musimy pamiętać, że przy każdym wywołaniu trzeba ponownie otwierać połączenie (co może zająć sporo czasu).

Przewagą serwletów nad skryptami CGI jest niezależność interfejsu JDBC od typów baz danych. Serwlet obsługujący np. bazę danych Sybase może korzystać z bazy Oracle po niewielkiej zmianie w pliku właściwości lub modyfikacji kilku linii (zakładając, że dostawca nie określił wywołania konkretnego typu bazy danych). Przykłady w tym rozdziale są napisane w ten sposób, by przedstawić sposób zapewnienia dostępu do różnorodnych baz danych, łącznie z bazami danych ODBC, takimi jak MS Access, Oracle, czy Sybase.

Serwlety w warstwie pośredniej

Warstwa pośrednia służy do połączenia aplikacji klienckich z aplikacjami serwerowymi (np. aplety z bazami danych). Gromadzi głównie serwlety, które wykorzystują bazy danych.

Powinniśmy umieścić warstwę pośrednią pomiędzy aplikacją klienta a naszym krańcowym źródłem danych, ponieważ oprogramowanie warstwy pośredniej (nazywane middleware) zawiera logikę biznesową. Logika biznesowa oddziela skomplikowane zadania niskiego poziomu (takie jak aktualizacja tabel baz danych) od zadań wysokiego poziomu (np. składanie zamówienia), co sprawia, że operacja wykonania zamówienia jest łatwiejsza i bezpieczniejsza.

Aplikacja klienta, składająca zamówienia bez użycia middleware, musi łączyć się bezpośrednio z serwerem bazy danych, który gromadzi zapisy rozkazów i adekwatnie do nich zmienia pola bazy.

Jakakolwiek zmiana w serwerze (przeniesienie do innej maszyny, zmiana struktury tabel bazy danych, itp.) może spowodować przerwanie połączenia z aplikacją klienta. Jeśli dokonamy zmiany w kliencie (zamierzonej lub przypadkowej), baza danych może błędnie zapisać informacje o zamówieniach złożonych za pośrednictwem aplikacji klienta.

Do Middleware przesyłane są informacje o zamówieniu (np. nazwisko, adres, towar, ilość i numer karty kredytowej) i za pomocą tych informacji dokonywana jest weryfikacja użytkownika składającego zamówienie (np. sprawdzana jest ważność karty kredytowej). Jeśli informacje są prawdziwe — zostaną wprowadzone do bazy danych. Jeśli baza danych ulegnie zmianie, middleware aktualizuje się bez konieczności modyfikacji aplikacji klienta. Nawet, gdy zamówienia bazy danych są tymczasowo zapisane w postaci pliku jednorodnego, middleware może przekazać klientowi informacje odczytane z takiego pliku.

Korzystając z middleware można podnieść wydajność systemu poprzez rozproszenie procesu zapisywania informacji w bazie danych na kilka wewnętrznych serwerów. Middleware potrafi wykorzystać przepustowość sieci: zamiast powolnego połączenia klient-serwer, klient może przekazać wszystkie informacje middleware, który użyje szybkiego połączenia sieciowego z puli połączeń i skomunikuje się z bazą danych.

Warstwy pośrednie w sieci są często tworzone przy użyciu serwletów. Serwlety dostarczają odpowiednie sposoby łączenia klientów z wewnętrznym serwerem za pomocą apletów lub formularzy HTML. Klient przesyła żądania do serwletu za pomocą HTTP, a logika biznesowa w serwlecie przekazuje żądanie do wewnętrznego serwera (więcej informacji na temat komunikacji aplet-serwlet znajduje się w rozdziale 10, „Komunikacja aplet-serwlet”).

Serwlety często używają dodatkowej warstwy pośredniej poza serwerem sieciowym (takiej jak Enterprise Java Beans) do łączenia się z bazą danych. Jeśli przeglądarka prześle formularz HTML z zamówieniem do serwletu, to może on przekształcić tą informację i wykonać wywołanie do EJB innej maszyny odpowiedzialnej za obsługę wszystkich zamówień — pochodzących zarówno z serwletów, jak i niezależnych programów. W omówionych przypadkach mamy do czynienia ze strukturą czterowarstwową.

Relacyjne bazy danych

We wcześniejszych przykładach omówiono serwlety, które przechowywały dane w postaci pliku umieszczonego na lokalnym dysku. Użycie jednorodnego pliku jest świetnym rozwiązaniem dla małych ilości informacji, ale można szybko stracić nad nim kontrolę. W miarę wzrostu ilości danych umieszczonych w pliku dostęp do nich staje się coraz wolniejszy. Wyszukanie odpowiednich informacji może stać się nie lada wyzwaniem: wyobraźmy sobie wyszukiwanie w pliku tekstowym nazwisk, miast i adresów e-mail wszystkich naszych klientów. Takie podejście jest dobre dla nowo otwartej firmy, ale nie sprawdza się w przypadku korporacji obsługującej setki tysięcy klientów. Wyszukanie na przykład listy klientów z Bostonu, których adresy e-mail są zakończone aol.com z dużego pliku tekstowego stanowiłoby ogromny problem.

Jednym z najlepszych rozwiązań tego problemu jest skorzystanie z systemu zarządzania relacyjnymi bazami danych (RDBMS). W najprostszej postaci RDBMS umieszcza dane w tabelach. Tabele te podzielone są na wiersze i kolumny podobnie do tabel arkusza kalkulacyjnego. Pomiędzy poszczególnymi wierszami i kolumnami jednej tabeli może zachodzić określona relacja (czyli powiązanie) — stąd termin relacyjne.

Jedna tabela relacyjnej bazy danych może zawierać informacje o klientach, druga o ich zamówieniach, a trzecia o przedmiotach zamówienia. Poprzez włączenie unikatowych identyfikatorów (powiedzmy, takich jak numery klientów i zamówień), te trzy tabele mogą być wzajemnie powiązane. Rysunek 9.1 pokazuje, w jaki sposób wygląda ta relacja.

Dane w tabeli mogą być odczytywane, aktualizowane, dopisywane i kasowane za pomocą poleceń języka SQL. JDBC API języka Java wprowadzony w JDK 1.1 używa pewnej odmiany SQL znanej jako ANSI SQL-2 Entry Level. W przeciwieństwie do większości języków programowania SQL jest językiem deklaracyjnym: interpreter SQL wykonuje polecenia wpisywane przez użytkownika. Inne języki programowania takie jak C/C++, Java wymagają użycia procedur, czyli określenia kolejnych kroków wykonania pewnego zadania. SQL nie jest szczególnie złożony, ale jest zbyt szerokim tematem do opisu w ramach tej książki. Więcej informacji na temat relacyjnych baz danych i języka SQL znajduje się w książkach: SQL dla Opornych Allena Tayllora i SQL in a Nutshell Kevina i Daniela Kline.

0x01 graphic

Rysunek 9.1. Powiązane tabele

Najprostszym i najbardziej pospolitym wyrażeniem SQL jest SELECT, które przeszukuje bazę danych i zwraca zestaw wierszy pasujących do kryterium. Na przykład, poniższe wyrażenie zaznacza wszystkie wiersze tabeli KLIENCI:

SELECT * FROM KLIENCI

Słowa kluczowe SQL takie jak SELECT i FROM oraz obiekty takie jak KLIENCI nie są wrażliwe na wielkość liter. Uruchomienie interpretera SQL SQL*PLUS dla Oracle powinno wygenerować na wyjściu:

KLIENT_ID NAZWISKO TELEFON

---------------------------------------------------

1 Bob Copier 617-555-1212

2 Janet Stapler 617 555-1213

3 Joel Laptop 507 555-7171

4 Larry Coffee 212 555-6225

Bardziej zaawansowane wyrażenia mogłyby ograniczać szukanie do wybranych kolumn albo według określonych kryteriów, np.:

SELECT ZAMÓWIENIE_ID, KLIENT_ID, SUMA FROM ZAMÓWIENIA

WHERE ZAMÓWIENIE_ID = 4

Za pomocą tego wyrażenia zaznaczono kolumny ZAMÓWIENIE_ID, KLIENT_ID i SUMA ze wszystkich rekordów, gdzie w polu ZAMÓWIENIE_ID jest wartość 4. Oto przykładowy rezultat:

ZAMÓWIENIE_ID KLIENT_ID SUMA

------------------------------------

4 1 72.19

Instrukcja SELECT może również łączyć kilka tabel na podstawie zawartości poszczególnych pól. Może to być związek jeden-do-jednego albo częściej używany jeden-do-wielu, np. jeden klient do kilku zamówień:

SELECT KLIENCI.NAZWISKO, ZAMÓWIENIA.SUMA FROM KLIENCI, ZAMÓWIENIA

WHERE ZAMÓWIENIA.KLIENT_ID = KLIENCI.KLIENT_ID AND ZAMÓWIENIA.ZAMÓWIENIE_ID = 4

Powyższa instrukcja spaja tabelę KLIENCI z tabelą ZAMÓWIENIA za pomocą pola KLIENT_ID. Należy zauważyć, że obie tabele posiadają to pole. Zapytanie zwraca informacje z dwóch tabel: nazwę użytkownika, który wykonał zamówienie nr 4 i całościowy koszt zamówienia. Oto przykładowy wynik tej operacji:

NAZWISKO SUMA

-------------------------------------

Bob Copier 72,19

Język SQL jest również używany do aktualizacji baz danych. Na przykład:

INSERT INTO KLIENCI (KLIENT_ID, NAZWISKO, TELEFON)

VALUES(5, "Bob Smith", "555 123-3456")

UPDATE KLIENCI SET NAZWISKO = "Robert Copier" WHERE KLIENT_ID = 1

DELETE FROM KLIENCI WHERE KLIENT_ID = 2

Pierwsza instrukcja tworzy nowy rekord w tabeli KLIENCI, zapełniając pola KLIENT_ID i TELEFON pewnymi wartościami. Druga aktualizuje istniejący rekord, zmieniając pole NAZWISKO określonego użytkownika. Ostatnia kasuje wszystkie rekordy, gdzie KLIENT_ID = 2. Należy ostrożnie posługiwać się tymi instrukcjami, a w szczególności instrukcją DELETE. Użycie tej instrukcji bez warunku WHERE usunie wszystkie rekordy z tabeli!

JDBC API

Wcześniej zakładaliśmy, że czytelnik posiada ogólną wiedze dotyczącą Java API. Ponieważ nawet wprawieni programiści Javy mogą mieć małe doświadczenie z bazami danych, ten podpunkt wprowadza JDBC na podstawie najpopularniejszej wersji JDBC 1.2. Pod koniec rozdziału dodano fragment na temat JDBC 2.0.

Jeśli czytelnik po raz pierwszy ma do czynienia z bazami danych, powinien przeczytać książki o ogólnych założeniach baz danych i JDBC, takie jak: Database Programming with JDBC and Java George'a Reese'a i JDBC Database Access with Java Grahama Hamiltona, Ricka Cattela, Maydene Fisher. Krótki przegląd znajduje się w Java Enterprise in a Nutshel Davida Flangana. Oficjalna specyfikacja JDBC znajduje się na stronie http://java.sun.com/products/jdbc

JDBC jest interfejsem programowym poziomu SQL, który pozwala wykonywać instrukcje SQL i odczytywać otrzymane wyniki. API jest zbiorem interfejsów i klas zaprojektowanym tak, aby możliwa była współpraca z jakąkolwiek bazą danych. Rysunek 9.2 pokazuje schemat struktury JDBC.

0x01 graphic

--> Rysunek 9.2. Java i baza danych[Author:PG]

Sterowniki JDBC

JDBC API, znajdujący się w pakiecie java.sql zawiera zestaw klas służących do implementacji określonych interfejsów. Większa część API jest rozprowadzana jako klasy interfejsów neutralnych dla baz danych, a współpracę z konkretnymi bazami danych zapewniają interfejsy dostarczone przez producentów baz danych.

Indywidualny system baz danych jest dostępny poprzez sterownik JDBC, który implementuje interfejs java.sql.Driver. Sterowniki istnieją prawie dla wszystkich popularnych systemów RDBMS, ale nie wszystkie są dostępne za darmo. Firma Sun dodaje darmowy sterownik wykorzystujący technikę pomostową JDBC-ODBC w JDK, aby umożliwić dostęp do danych z baz standardu ODBC, takich jak baza danych Microsoft Access. Jest to bardzo prosty sterownik, którego można używać w bardzo prostych aplikacjach. Twórcy serwletów powinni w szczególności zwrócić uwagę na to ostrzeżenie, ponieważ jakikolwiek problem w treści kodu własnego sterownika JDBC-ODBC może zniszczyć cały serwer.

Sterowniki JDBC są dostępne dla większości platform baz danych tworzonych przez wielu producentów. Istnieją cztery kategorie sterownika:

Wykaz obecnie dostępnych sterowników JDBC znajduje się na stronie http://industry.java.sun.com/products/jdbc/drivers.

Połączenie z bazą danych

Pierwszym krokiem, jaki trzeba wykonać, aby użyć sterownika JDBC do połączenia z bazą danych jest załadowanie określonej klasy sterownika do aplikacji JVM (Java Virtual Machine). Dzięki temu sterownik będzie dostępny przy otwieraniu połączenia z bazą danych. Prostym sposobem załadowania klasy sterownika jest użycie metody Class.forName():

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

Załadowany sterownik sam rejestruje się wewnątrz klasy java.sql.DriverManager, jako dostępny sterownik bazy danych.

Następnym krokiem jest użycie klasy DriverManager do otwarcia połączenia z bazą danych, gdzie za pomocą URL określa się żądaną bazę danych. Metodą używaną do otwierania połączenia jest DriverManager.getConnection, która zwraca klasę implementującą interfejs java.sql.Connection:

Connection con =

DriverManager.getConnecton("jdbc:odbc:somedb","user","passwd");

Podany 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 JDBC-ODBC Bridge używa jdbc:odbc:datasourcename:odbcoptions.

Podczas wywołania metody getConnection(), obiekt DriverManager rozpoznaje odpowiedni sterownik za pomocą URL przeszukując bazę dostępnych sterowników. Jeśli wymagany sterownik istnieje w bazie, zostanie użyty do stworzenia obiektu Connection. Poniżej znajduje się fragment kodu, jakiego można użyć w serwlecie do ładowania sterownika bazy danych JDBC-ODBC Bridge i realizacji połączenia:

Connection con = null;

try{

// ładujemy (i w ten sposób rejestrujemy) sterownik

//możliwość wystąpienia wyjątku ClassNotFoundException

Class.forName(„sun.jdbc.odbc.JdbcOdbcDriver”);

//otwieramy połączenie z bazą danych

// możliwość wystąpienia wyjątku SQLException

con=DriverManager.getConnection("jdbc:odbc:somedb", "user", "passwd");

// reszta kodu źródłowego ma tu swoje miejsce

}

catch(ClassNotFoundException e){

// obsługa wyjątku ClassNotFoundException

}

catch(SQLException e){

// obsługa wyjątku SQLException

}

finally {

// zamknięcie połączenia z bazą danych

try{

if (con!=null) con.close();

}

catch (SQLException ignored) {}

}

Właściwie dostępne są trzy formy metody getConnection(). Najprostszą z nich jest ta, która pobiera tylko adres URL: getConnection (String url). Ta metoda ma zastosowanie w przypadku, gdy nie przewidziano konieczności zalogowania się (podania nazwy użytkownika i hasła) lub umieszczenia informacji logujących w adresie URL. Istnieje jeszcze jedna forma, która pobiera adres URL i obiekt Properties: getConnection(String url, Properties props). Ta metoda zapewnia największą elastyczność. Obiekt Properties (tablica mieszająca zawierająca klucze i wartości typu String) zawiera standardowo nazwę użytkownika i hasło, a także dodatkowe informacje przekazywane odpowiedniemu sterownikowi bazy danych. Na przykład, niektóre sterowniki respektują właściwość cacherows, która określa jak dużo wierszy ma trafić do pamięci cache w jednostce czasu. Używanie tej metody ułatwia otwarcie połączenia z bazą danych w oparciu o plik z rozszerzeniem .properties.

Sterownik, adres URL i potrzebne do zalogowania się dane mogą być określone w następującym pliku sql.properties. Format nazwa=wartość jest formatem standardowym w plikach właściwości Javy:

connection.driver=sun.jdbc.odbc.JdbcOdbcDriver

connection.url=jdbc:odbc:somedb

user=user

password=passwd

Za pomocą kodu przedstawionego w przykładzie 9.1 otwiera się połączenie z bazą danych używając wartości zgromadzonych wewnątrz pliku sql.properties. Należy zauważyć, że nazwa użytkownika i hasło to informacja standardowo wymagana do zalogowania się, natomiast właściwości connection.driver i connection.url są specjalnymi nazwami użytymi w poniższym kodzie do rozpoznania sterownika i odpowiadającego mu URL, a wszelkie dodatkowe własności będą przekazane do wybranego sterownika.

Przykład 9.1.

Użycie odpowiedniego pliku do otwarcia połączenia bazodanowego.

// pobierz właściwości połączenia bazodanowego

Properties props = new Properties();

InputStream in = new FileInputStream("sql.properties");

props.load(in);

in.close(); ta instrukcja powinna znaleźć się w bloku finally

// załadowanie sterownika

Class.forName(props.getProperty("connection.driver");

//otwarcie połączenia

con = DriverManager.getConnection("connection.url"),props);

Utworzony obiekt Properties jest wypełniany wartościami odczytanymi z pliku sql.properties, a następnie zawarte w tym pliku właściwości są użyte do otwarcia połączenia z bazą danych. Wykorzystanie pliku właściwości pozwala na zmianę wszystkich informacji o połączeniach z bazą danych bez zbędnego przekompilowania kodu Javy.

Otwieranie połączenia z serwletu

Serwlet może użyć przedstawionego powyżej sposobu ładowania informacji o połączeniu z bazą danych z pliku właściwości znajdującego się w katalogu WEB-INF. Metoda getResourceAsStream() pobiera zawartość tego pliku:

Properties props = new Properties();

InputStream in = getServletContext().getResourceAsStream(

"/WEB-INF/sql.properities");

props.load(in);

in.close();

Ponieważ serwlety są najczęściej wdrażane przy użyciu graficznych narzędzi, możemy użyć tych samych narzędzi do skonfigurowania bazy danych. Jednym ze sposobów osiągnięcia tego celu jest umieszczenie połączeń z bazą danych wewnątrz serwera JNDI, gdzie połączenia są rozpoznawane przez serwlety za pomocą określonych nazw. Ten sposób jest wykorzystywany przez serwlety pracujące wewnątrz serwerów J2EE. Rozdział 12 omawia szczegółowo to zagadnienie. Innym zadaniem, które nie wymaga serwera JNDI, a wykorzystuje graficzne narzędzia rozmieszczenia, jest użycie parametrów inicjacji kontekstu do zachowania informacji o konfiguracji. Te parametry inicjujące mogą zostać skonfigurowane za pomocą graficznych narzędzi rozmieszczeń, a potem można je zapisać w pliku web.xml. (Więcej informacji na ten temat jest w rozdziale 4).

Klasa ContextProperties przedstawiona w przykładzie 9.2 udostępnia parametry inicjujące kontekst jako obiekt Properties. To pozwala na przekazanie wszystkich par nazwa-wartość metodzie DriverManager.getConnection().

Przykład 9.2.

Klasa ContextProperties

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class ContextProperties extends Properties {

public ContextProperties(ServletContext context) {

Enumeration props = context.getInitParameterNames();

while (props.hasMoreElements()) {

String name = (String) props.nextElement();

String value = (String) context.getInitParameter(name);

put (name,value);

}

}

}

Serwlet otrzymuje informacje o połączeniu za pośrednictwem tej klasy (nie pobiera danych z sql.properties), co przedstawiono poniżej:

// pobierz kontekst init params jako obiekt properties

ContextProperties props = new ContextProperties(getServletContext());

//załaduj sterownik

Class.forName(props.getProperty("connection.driver"));

// itd.

Wystarczy przypisać parametry inicjujące kontekst konkretnym wartościom bazy danych w sposób ręczny w pliku web.xml lub używając narzędzia rozmieszczenia.

Wykonywanie zapytań SQL

Aby naprawdę używać bazy danych, musimy poznać sposób wykonywania zapytań. Najprostszym sposobem jest użycie klasy java.sql.Statement. Tego typu obiekty nigdy nie są tworzone bezpośrednio, lecz za pomocą metody createStatement() obiektu Connection, która dostarcza referencje do utworzonego w danej chwili obiektu typu Statement:

Statement stmt = con.createstatement();

Zapytanie zwracające dane może być wykonane przy użyciu metody executeQuery() obiektu Statement. Po wykonaniu tej metody dane zwracane są w java.sql.ResultSet w postaci hermetycznej:

ResultSet rs = stmt.executeQuery("SELECT*FROM KLIENCI");

ResultSet jest reprezentacją danych otrzymanych w wyniku wykonania zapytania. Obiekt ten zwraca za każdym razem jeden wiersz. Użycie metody next() obiektu ResultSet pozwala na przemieszczanie się pomiędzy wierszami danych reprezentowanych przez ResultSet. Istnieje wiele metod pozwalających na odczytywanie danych z danego wiersza obiektu ResultSet. Do najczęściej używanych należą metody getString() i getObject(), które służą do odczytywania wartości kolumn:

while(rs.next()) {

String event = rs.getstring("event");

Object count = (Integer) rs.getObject("count");

}

Należy wiedzieć, że obiekt ResultSet jest powiązany ze swym nadrzędnym (rodzicielskim) obiektem Statement. Jeśli obiekt Statement zostanie zamknięty lub wykorzystany do obsługi innego zapytania, wszystkie odpowiadające mu obiekty ResultSet zostaną również automatycznie zamknięte.

Przykład 9.3 pokazuje bardzo prosty serwlet, w którym użyto sterownika Oracle JDBC by wykonać proste zapytanie, zwracające nazwiska i numery telefonu wszystkich pracowników zawartych w tabeli bazy danych. Zakładamy, że baza danych zawiera tabelę PRACOWNICY z co najmniej dwoma polami, a także tabele NAZWISKO i TELEFON.

Przykład 9.3.

Serwlet JDBC-enabled

import java.io.*;

import java.sql.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class DBPhoneLookup extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

Connection con = null;

Statement stmt = null;

ResultSet rs = null;

res.setContentType("text/html");

Printwriter out = res.getWriter();

try{

//ładujemy sterownik oracle

Class.forName("oracle.jdbc.driver.OracleDriver");

// nawiązujemy połączenie z bazą danych

// udostępnioną na serwerze dbhost

// na porcie 1528

con = DriverManager.getConnection (

"jdbc:oracle:thin:@dbhost:1528:ORCL","user","passwd");

//tworzymy obiekt typu Statement

stmt = con.createStatement();

// wykonujemy zapytanie SQL, otrzymujemy wynik w ResultSet

rs=stmt.executeQuery("SELECT NAZWISKO, TELEFON FROM PRACOWNICY");

// przedstawiamy otrzymane dane w postaci listy

out.println("<HTML><HEAD><TITLE>Książka telefoniczna</TITLE></HEAD>");

out.println("<BODY");

out.println("<UL.");

while (rs.next()) {

out.println("<LI> + rs.getString("name") + ""

+ rs.getString("phone")");

}

out.println("</UL.>");

out.println("</BODY></HTML>");

}

catch (ClassNotfoundException e ) {

out.println("Nie można zaladowac sterownika bazy danych: "+e.getMessage());

}

catch (SQLException e) {

out.println("SQLException :" + e.getMessage());

}

finally {

// zawsze zamykamy połączenie z bazą danych

try {

if (con!=null) con.close();

}

catch (SQLException ignored){}

}

}

}

Dla każdego obiektu DBPhoneLookup nawiązywane jest połączenie z bazą danych, następnie wykonuje się zapytanie w celu przeszukania nazwisk i numerów telefonów wszystkich znajdujących się w tabeli pracowników i wynikowa lista jest dostarczana użytkownikowi.

Obsługa wyjątków SQL

DBPhoneLookup obejmuje większość kodu w bloku obsługi wyjątku try-catch. W tym blok wyłapuje dwa wyjątki: ClassNotFoundException i SQLException. Pierwszy z nich jest zgłaszany za pomocą metody Class.forName(), gdy nie można załadować klasy sterownika JDBC. Drugi jest zgłaszany przez którąkolwiek z metod JDBC, gdy wystąpi problem w trakcie jej wykonania. Obiekty klasy SQLException obok standardowych właściwości klas wyjątków mają dodatkową cechę: mogą tworzyć łańcuchy wyjątków. Klasa SQLException definiuje dodatkową metodę getNextException() pozwalającą na „hermetyzację” dodatkowych obiektów Exception. Nie przedstawiono tego w poprzednim przykładzie, ale użycie tej metody może wyglądać w następujący sposób:

catch (SQL Exception e){

out.println(e.getMessage());

while ((e=e.getNextException())!=null){

out.println(e.getMessage());

}

}

Powyższy kod wyświetla informację z pierwszego wyjątku, a następnie, za pomocą pętli, przechodzi poprzez kolejne wyjątki, wyświetlając informacje o konflikcie związanym z każdym z nich. W praktyce, pierwszy wyjątek będzie zawierał najbardziej znaczącą informację.

Zestawy wyników w szczegółach

Przyjrzyjmy się obiektowi ResultSet i obiektowi pokrewnemu ResultSetMetaData. W przykładzie 9.1 wiedzieliśmy w jaki sposób wykonać nasze zapytanie, jakiego wyniku należało się spodziewać, więc sformatowaliśmy wyjście w odpowiedni sposób. Jeśli chcielibyśmy wyświetlić wyniki zapytania w tabeli HTML, dobrze byłoby mieć taki program Javy, który tworzy tę tabelę automatycznie korzystając z obiektu ResultSet zamiast pisać kilkakrotnie tą samą część kodu. Dodatkowym atutem takiego programu byłaby możliwość zmiany zawartości tabeli wraz ze zmianą konstrukcji zapytania.

Obiekt ResultSetMetaData przekazuje programowi informacje na temat danego obiektu ResultSet. Możemy wykorzystać tę cechę do budowy obiektu, który będzie generował dynamicznie tabelę HTML z wykorzystaniem ResultSet, tak jak to pokazano w Przykładzie 9.4. Wiele narzędzi Javy wspomagających HTML ma podobne możliwości, co omówiono w rozdziałach 14 - 18.

Przykład 9.4.

Klasa generująca tabelę HTML z ResultSet przy użyciu ResultSetMetaData

import java.sql.*;

public class HtmlResultSet {

private ResultSet rs;

public HtmlResultSet(ResultSet rs) {

this.rs = rs;

}

public String toString() { // może być wywołany tylko raz

StringBuffer out = new StringBuffer();

// twórzymy tabelę aby wyświetlić zestaw wyników

out.append("<TABLE>\n");

try {

ResultSetMetaData rsmd = rs.getMetaData();

int numcols = rsmd.getColumnCount();

// Tytuł tabeli z etykietami kolumn zestawów wyników

out.append("<TR>");

for (int i = 1; i <= numcols; i++) {

out.append("<TH>" + rsmd.getColumnLabel(i));

}

out.append("</TR>\n");

while (rs.next()) {

out.append("<TR>"); // tworzymy nowy wiersz

for (int i = 1; i <= numcols; i++) {

out.append("<TD>"); // tworzymy nowy element danych

Object obj = rs.getObject(i);

if (obj != null)

out.append(obj.toString());

else

out.append("&nbsp;");

}

out.append("</TR>\n");

}

// Koniec tabeli

out.append("</TABLE>\n");

}

catch (SQLException e) {

out.append("</TABLE><H1>ERROR:</H1> " + e.getMessage() + "\n");

}

return out.toString();

}

}

Ten przykład pokazuje w jaki sposób użyć dwóch podstawowych metod pochodzących od obiektu ResultSetMetaData: getColumnCount() i getColumnLabel(). Pierwsza z nich zwraca ilość kolumn obiektu ResultSet, a druga dostarcza nazwę określonej kolumny w oparciu o jej indeks. Indeksowanie w obiektach ResultSet jest oparte na standardzie RDMBS zamiast C++/Java, co oznacza, że obiekty numerowane są od 1 do n, a nie od 0 do n-1.

W przykładzie użyto metody getObject() obiektu ResultSet, by odczytać zawartość każdej kolumny. Wszystkie metody getXXX() działają zarówno w oparciu o indeksy, jaki i nazwy kolumn. Dostęp do danych w ten sposób jest bardziej wydajny i przenośny, jeśli właściwie napiszemy wyrażenie SQL. Używamy tu getObject().toString() zamiast getString(), aby uprościć obsługę pól mających wartość null, co omówiono w następnym podrozdziale.

Tabela 9.1 pokazuje metody Javy, które można użyć do wyszukania najbardziej powszechnych typów danych SQL. Metoda getObject() obiektu ResultSet zwróci obiekt Javy którego typ zależy od typu obiektu SQL, co pokazano w tabeli. Można również użyć innej metody getXXX(). Metody te pokazano w trzeciej kolumnie, wraz ze zwracanymi typami danych. Należy pamiętać, że dostępne typy danych SQL są różne w zależności od bazy danych.

Tabela 9.1.

Metody otrzymywania danych z obiektu ResultSet

Typ danych SQL

Typ Java zwracany przez getObject()

Zalecana alternatywa do getObject()

BIGINT

Long

long getlong()

BINARY

byte[ ]

byte[ ] getBytes()

BIT

Boolean

Boolean getBoolean()

CHAR

String

String getString()

DATE

java.sql.Date

java.sql.Date getDate()

DECIMAL

java.math.BigDecimal

java.math.BigDecimal

getBigDecimal()

DOUBLE

Double

double getDouble()

FLOAT

Double

double getDouble()

INTEGER

Integer

int getInt()

LONGVARBINARY

byte[ ]

InputStream getBinaryStream()

LONGVARCHAR

String

InputStream getAsciiStream ()

InputStream getUnicodeStream

NUMERIC

java.math.BigDecimal

java.math.BigDecimal

getBigDecimal()

REAL

Float

float getFloat()

SMALLINT

Integer

short getShort()

TIME

Java.sql.Time

java.sql.Time getTime()

TIMESTAMP

Java.sql.Timestamp

java.sql.Timestamp

getTimestamp()

TINYINT

Integer

byte getByte()

VARBINARY

byte[ ]

byte[ ] getBytes()

VARCHAR

String

String getString()

Obsługa pól mających wartość null

Pole w bazie danych, podobnie do obiektu w Javie może mieć wartość null, co oznacza, że to pole jest niewypełnione. Obsługa takich pól bazy danych może stanowić pewien problem, bowiem nie jesteśmy w stanie stwierdzić, czy uzyskana wartość (0 lub -1 zwrócona np. przez metodę getInt()) jest faktyczną wartością wpisaną w to pole, czy też jest to informacja, że w dane pole nic nie wpisano. Z tego powodu JDBC zawiera metodę wasNull() w obiekcie ResultSet zwracającą true lub false w zależności od tego, czy ostatnia przeczytana kolumna w bazie danych miała wartość typu null czy nie. To oznacza, że należy wczytać dane z obiektu ResultSet do konkretnej zmiennej, wywołać metodę wasNull() i w zależności od wyniku postępować dalej. Oto przykład zastosowania tej metody:

int age = rs.getInt( "wiek");

if (!rs.wasNull())

out.println("Wiek: " + wiek);

Innym sposobem pozwalającym na odnajdywanie wartości typu null jest użycie metody getObject(). Jeśli kolumna ma wartość null, getObject() zawsze zwróci wartość null. Użycie metody getObject() eliminuje konieczność wywołania metody wasNull(), ale czasem lepiej skorzystać z pierwszej metody.

Aktualizacja baz danych

Większość aplikacji bazodanowych, oprócz obsługi zapytań, umożliwia modyfikację rekordów w tabelach bazy danych. Kiedy klient przesyła zamówienie lub dostarcza jakąś informację, dane muszą być wprowadzone do bazy danych. Gdy obsługiwana jest instrukcja SQL UPDATE, INSERT lub DELETE wiadomo, że nie wystąpi obiekt ResultSet i można użyć metodę executeUpdate() obiektu Statement. Zwraca ona liczbę oznaczającą ilość zmodyfikowanych wierszy w wyniku wywołania konkretnego wyrażenia. Tej metody używa się w następujący sposób:

int count =

stmt.executeUpdate("DELETE FROM KLIENCI WHERE KLIENT_ID = 5);

Jeśli wyrażenie SQL może zwrócić obiekt ResultSet lub tylko zmodyfikować odpowiednie pole (czyli wykonać instrukcję typu DELETE, INSERT lub UPDATE) należy skorzystać z metody execute()obiektu Statement. Ta metoda zwraca wartość true, gdy wykonanie wyrażenia SQL utworzyło co najmniej jeden obiekt ResultSet lub false, gdy w wyniku wykonania zapytania zaktualizowano bazę danych.

boolean b = stmt.execute(sql);

Metody getResultSet() i getUpdateCount()zapewniają dostęp do wyników wywołania metody execute(). Przykład 9.5 demonstruje użycie tych metod w nowej wersji HtmlResultSet, nazwaną HtmlSQLResult, która tworzy tabelę HTML w wyniku wykonania zapytania SQL.

Przykład 9.5.

Klasa generująca tabelę HTML z ResultSet przy użyciu ResultSetMetaData

import java.sql.*;

public class HtmlSQLResult {

private String sql;

private Connection con;

public HtmlSQLResult(String sql, Connection con) {

this.sql = sql;

this.con = con;

}

public String toString() { // może być wywołany tylko raz

StringBuffer out = new StringBuffer();

// Poniższa linia służy do wyświetlenia komendy SQL na początku tabeli

// out.append("Wyniki zapytania SQL: " + sql + "<P>\n");

try {

Statement stmt = con.createStatement();

if (stmt.execute(sql)) {

//?????

ResultSet rs = stmt.getResultSet();

out.append("<TABLE>\n");

ResultSetMetaData rsmd = rs.getMetaData();

int numcols = rsmd.getColumnCount();

// Tytuł tabeli z etykietami kolumn zestawów wyników

out.append("<TR>");

for (int i = 1; i <= numcols; i++)

out.append("<TH>" + rsmd.getColumnLabel(i));

out.append("</TR>\n");

while (rs.next()) {

out.append("<TR>"); // rozpocznij nowy wiersz

for (int i = 1; i <= numcols; i++) {

out.append("<TD>"); // rozpocznij nowy element danych

Object obj = rs.getObject(i);

if (obj != null)

out.append(obj.toString());

else

out.append("&nbsp;");

}

out.append("</TR>\n");

}

// Koniec tabeli

out.append("</TABLE>\n");

}

else {

//Tu powinna wystąpić liczba

out.append("<B>Records Affected:</B> " + stmt.getUpdateCount());

}

}

catch (SQLException e) {

out.append("</TABLE><H1>ERROR:</H1> " + e.getMessage());

}

return out.toString();

}

}

Powyższy przykład wykorzystuje metodę execute(), aby wykonać wyrażenie SQL przesłane do konstruktora HtmlSQLResult. Potem, w zależności od zwróconej wartości, wywołuje getResultSet() lub getUpdateCount(). Należy zauważyć, że ani getResultSet(), ani getUpdateCount() nie powinny być wykonane więcej niż raz w trakcie jednego zapytania.

Użycie gotowych zapytań*

Obiekt PreparedStatement ma podobne właściwości do obiektu Statement. Ważna różnicą jest fakt, że wyrażenia SQL w obiekcie PreparedStatement są wcześniej zinterpretowane przez bazę danych, co przyspiesza ich wykonanie. Raz skompilowany obiekt PreparedStatement można dostosowywać w zależności od potrzeb poprzez korygowanie wcześniej zdefiniowanych parametrów. Preinterpretowane wyrażenia są użyteczne w aplikacjach, w których wymagane jest wielokrotnie wykonanie tych samych wyrażeń SQL.

Metoda preparedStatement(String) obiektu Connection służy do tworzenia obiektów PreparedStatement. W miejsce znacznika '?' zostaną umieszczone określone wartości, na przykład:

PreparedStatement pstmt = con.prepareStatement(

"INSERT INTO ZAMÓWIENIA (ZAMÓWIENIE_ID, KLIENT_ID, TOTAL) VALUES (?,?,?)");

// Inny kod

pstmt.clearParameters(); // czyści wszystkie poprzednie wartości parametru

pstmt.setInt(1, 2); // ustawia ZAMÓWIENIE_ID

pstmt.setInt(2, 4); // ustawia KLIENT_ID

pstmt.setDouble(3, 53.43); // ustawia TOTAL

pstmt.executeUpdate(); // wykonuje tak skonstruowane wyrażenie SQL

Metoda clearParametres() usuwa wszystkie wcześniej zdefiniowane wartości parametrów, a metody setXXX() służą do przypisania właściwych wartości do '?'. Po przypisaniu wartości parametrom należy wywołać metodę executeUpdate(), aby utworzyć obiekt PreparedStatement.

Podczas wpisywania tekstu przesłanego przez użytkownika przy użyciu obiektu Statement i dynamicznego SQL-a, należy uważać, aby przypadkowo nie wprowadzić jakiegoś znaku kontrolnego SQL (takiego jak ' lub ") w odniesieniu do bazy danych. Baza danych Oracle umieszcza łańcuchy tekstowe pomiędzy apostrofami, tak więc próba wstawienia łańcucha John d'Artagan w bazę danych spowoduje wystąpienie błędu SQL:

INSERT INTO MUSKETEERS (NAME) VALUES ('John d'Artagan')

Łańcuch w tym przykładzie „kończy się” w dwóch miejscach. Jednym z rozwiązań jest ręczna zamiana pojedynczego apostrofu (') na dwa (' '), czyli wykonanie sekwencji unikowej dla Oracle. Takie podejście wymaga unikania znaków specjalnych w sposób ręczny, co kłóci się z zasadą przenośności pisanego kodu. Dużo lepszym rozwiązaniem jest użycie obiektu PreparedStatement i wprowadzanie łańcucha za pomocą metody setString(), co pokazano poniżej. Metoda ta unika automatycznie znaków specjalnych, gdy zaistnieje taka potrzeba:

PreparedStatement pstmt = con.prepareStatement(

" INSERT INTO MUSKETEERS (NAME) VALUES (?)");

pstmt.setString (1, "John d'Artagan");

pstmt.executeUpdate();

Ponowne użycie obiektów bazy danych

We wprowadzeniu wspomnieliśmy o tym, że czas życia serwletu pozwala na bardzo szybki dostęp do bazy danych. Przy otwieraniu połączenia do bazy danych pojawia się problem wydajności związany z efektem „wąskiego gardła”. Problem ten nie ma wielkiego znaczenia dla aplikacji i apletów, gdyż kilka sekund opóźnienia, potrzebnego na utworzenie obiektu Connection, nie wpływa na funkcjonowanie programu. W przypadku serwletów ten problem ma znaczenie, ponieważ nowy obiekt Connection jest tworzony i usuwany na każde żądanie strony. Na szczęście czas życia serwletu pozwala na ponowne użycie tego samego połączenia dla wielu żądań, nawet konkurujących między sobą.

Ponowne użycie połączeń do baz danych

Serwlet może utworzyć wiele obiektów Connection za pomocą metody init() i ponownie ich użyć za pomocą metod service(), doGet() i doPost(). Przykład 9.6 przedstawia zmodyfikowany serwlet wyszukujący numery telefonów, w którym najpierw tworzony jest obiekt Connection, a później w trakcie programu otwierane jest połączenie z bazą danych. Używa także sterownika Sybase JDBC oraz klasy HtmlSQLResult z przykładu 9.5 do wyświetlania wyników.

Przykład 9.6.

Ulepszony serwlet z numerami telefonów.

import java.io.*;

import java.sql.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class DBPhoneLookup extends HttpServlet {

private Connection con = null;

public void init() throws ServletException {

try {

//załaduj i zarejestruj sterownik Sybase

Class.forName("com.sybase.jdbc.SybDriver");

con = DriverManager.getConnection (

"jdbc:sybase:Tds:dbhost:7678","user","passwd");

}

catch (ClassNotfoundException e ) {

throw new UnavailableException ("Nie można zaladowac sterownika bazy danych");

}

catch (SQLException e) {

throw new UnavailableException("Nie można połączyć się z bazą danych");

}

}

public void doGet(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

res.setContentType("text/html");

Printwriter out = res.getWriter();

out.println("<HTML><HEAD><TITLE>Książka telefoniczna</TITLE></HEAD>");

out.println("<BODY");

HtmlSQLResult result =

new HtmlSQLResult("SELECT NAME, PHONE FROM EMPLOYEES", con);

// Wyświetl wyniki przeszukiwania

out.println("<H2>Pracownicy:</H2>");

out.println(result);

out.println("</BODY></HTML>");

}

public void destroy() {

// Wyczyść

try {

if (con!=null) con.close();

}

catch (SQLException ignored){}

}

}

}

Ponowne użycie przygotowanych wyrażeń.

Można w łatwy sposób przyspieszyć wydajność serwletu przez utworzenie z wyprzedzeniem innych obiektów współpracujących z bazami danych. Obiekt PreparedStatement idealnie nadaje się do wykonania tego zadania, bo za jego pomocą można wcześniej skompilować wyrażenie SQL. Oszczędza to zaledwie kilka milisekund, ale taka oszczędność ma znaczenie w witrynach odwiedzanych przez kilkaset tysięcy użytkowników dziennie.

Współdzielenie obiektów może powodować wystąpienie błędów. Dostęp do obiektu PreparedStatement może wymagać wywołania 3 lub 4 metod. Jeśli jeden wątek wywołuje metodę clearParameters()na chwile przed tym, jak kolejny wątek wywoła execute(), wyniki tej drugiej operacji będą błędne. Istnieje także ograniczenie obsługi tylko jednego zapytania w danym czasie przez obiekt Statement. Rozwiązaniem tego problemu jest synchronizacja tego fragmentu kodu, który używa współdzielonych obiektów, co omówiono w rozdziale 3 i pokazano poniżej:

synchronized (pstmt) {

pstmt.clearParameters();

pstmt.setInt(1, 2);

pstmt.setInt(2, 4);

pstmt.setDouble(3, 53.43);

pstmt.executeUpdate();

}

Niestety, takie rozwiązanie ma również wady. Wprowadzenie bloku synchronizacji na pewnych platformach zajmuje dodatkowy czas, a zsynchronizowane obiekty mogą być użyte tylko przez jeden wątek w danej chwili. Mimo to niektóre serwlety wymagają użycia bloku synchronizacji. Dobrym wyjściem z tej sytuacji jest wcześniejsze utworzenie połączeń z obiektami (takimi jak PreparedStatement), które mogą być szybko użyte wewnątrz bloków synchronizacji.

Powyższe metody nie mają zastosowania w przypadku serwletów używających interfejsu SigleThreadModel, a ponadto ograniczają wydajność poprzez ładowanie naraz wielu kopii serwletu.

Transakcje

Transakcje są ważnym zagadnieniem związanym ze współczesnymi systemami relacyjnych baz danych. W wielu witrynach internetowych zachodzi potrzeba posługiwania się rozbudowanymi wyrażeniami i strukturami danych. Spójrzmy na aplikację banku online. Aby wykonać przelew 50000$ pomiędzy dwoma kontami, program musi wykonać operację składającą się z dwóch oddzielnych, ale powiązanych ze sobą akcji: skredytować jedno konto i obciążyć drugie. Wyobraźmy sobie, że z pewnych powodów powiodła się tylko operacja uznania pierwszego konta, a do obciążenia drugiego konta nie doszło. W wyniku takiego działania na jednym koncie jest o 50000$ więcej, a na drugim koncie nic się nie zmieniło.

Błędne wykonanie wyrażenia SQL w tym przykładzie powoduje, że w baza danych zawiera błędne informacje dotyczące ilości środków dostępnych na koncie. Prawdopodobieństwo takiego zdarzenia jest niewielkie, ale jego wystąpienie nie jest wykluczone. Ten rodzaj problemu jest zbliżony do zagadnienia synchronizacji omówionego w rozdziale 3. Tym razem, zamiast zajmować się ważnością danych zgromadzonych w serwlecie, omówimy ważność danych zapisywanych w bazie. Prosta synchronizacja nie jest dostatecznym rozwiązaniem omawianego problemu, ponieważ z zasobów jednej bazy danych może korzystać wiele serwletów. W aplikacjach bankowych prawdopodobne jest korzystanie z zasobów bazy danych przez wiele aplikacji nie napisanych w Javie. Na szczęście tym problemem zajęto się na długo przed zaistnieniem Javy. Większość głównych systemów RDMBS wspiera koncepcję transakcji. Technologia transakcji pozwala na grupowanie wielu wyrażeń SQL: transakcja to zbiór operacji wykonywanych przez serwer bazodanowy na bazie. Za pomocą systemu RDMBS, obsługującego tę technologię, można rozpocząć transakcję (czyli wykonać określoną liczbę operacji) i przesłać wyniki do bazy danych. Jeśli wykonanie wszystkich operacji wchodzących w skład transakcji jest niemożliwe, cała transakcja jest anulowana i żadne zmiany nie zachodzą w bazie danych. Gdybyśmy naszą aplikację bankową zbudowali w oparciu o transakcje, pobranie pieniędzy zostałoby anulowane, gdyby obciążenie konta nie powiodło się.

Transakcja podczas realizacji zadania jest odizolowana od bazy danych. Transakcje są niepodzielne (operacje wchodzące w skład transakcji są traktowane jako logiczna całość). To oznacza, że użytkownicy korzystający z bazy zawsze mają dostęp do aktualnych, choć nie widzą procesu aktualizacji. Jeśli użytkownik zażąda raportu na temat sprzedanych produktów w trakcie wykonywania transakcji sprzedaży, raport nie uwzględni wyników tej transakcji.

Użycie transakcji w JDBC

Obsługa transakcji w JDBC jest realizowana za pomocą obiektu Connection. Nowe połączenia domyślnie są otwierane w trybie autocommit (automatyczne zatwierdzenie), co oznacza, że każde wyrażenie SQL jest traktowane jako pojedyncza transakcja, a wyniki wykonania wyrażenia są natychmiast zapisywane w bazie danych. Aby mieć możliwość grupowania wyrażeń SQL w transakcje i samodzielnej kontroli wykonywania transakcji, należy wywołać metodę setAutoCommit() (wywoływanej na rzecz obiektu Connection) z parametrem false. Za pomocą metody getAutoCommit()możemy sprawdzić, w jakim trybie realizowane jest połączenie z bazą danych. Funkcja zwróci wartość true, gdy ustawiony jest tryb autocommit. Po zakończeniu wykonywania wszystkich wyrażeń SQL należy wywołać metodę commit(), aby zatwierdzić transakcję i zapisać wszystkie dokonane zmiany w bazie danych. Jeśli wystąpi błąd, wywołana jest metoda rollback(), odwołująca transakcję(powodująca cofnięcie wszystkich zmian).

W przykładzie 9.7 pokazano serwlet używający transakcji do przeprowadzenia podstawowej formy zamówienia. Dla uproszczenia zakładamy istnienie dwóch tabel w bazie danych ODBC: MAGAZYN (tabela zawiera identyfikator produktu i jego ilość w magazynie) oraz WYSYŁKA (w tej tabeli wpisano identyfikator produktu, numer zamówienia oraz ilość wysłanych produktów). Serwlet korzysta z metody chargeCard(), która obsługuje wystawianie rachunków i zgłasza wyjątek, jeśli karta kredytowa klienta jest nieważna.

Przykład 9.7.

Zarządzanie zamówieniem za pomocą transakcji

import java.io.*;

import java.sql.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class OrderHandler extends HttpServlet {

public void doPost(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

res.setContentType("text/html");

PrintWriter out = res.getWriter();

Connection con = null;

try{

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

con = DriverManager.getConnection("jdbc:odbc:ordersdb","user","passwd");

// włącz tryb transakcji

con.setAutoCommit(false);

Statement stmt = con.createStatement();

stmt.executeUpdate(

"UPDATE MAGAZYN SET ILOŚĆ = (ILOŚĆ - 10) WHERE PRODUCTID = 7");

stmt.executeUpdate(

"UPDATE WYSYŁKA SET WYSŁANE = (WYSŁANE + 10) WHERE PRODUCTID = 7");

chargeCard(); // metoda faktycznie nie istnieje ...

con.commit();

out.println("Operacja zamówienia zakończona sukcesem! Dziękujemy za współpracę");

}

catch(Exception e) {

// Jakikolwiek błąd jest podstawą do cofnięcia transakcji

try {

con.rollback();

}

catch (SQLException ignored){}

out.println("Operacja zamówienie nie powiodła się. Proszę skontaktować się z obsługą techniczną.");

}

finally {

// wyczyść

try {

if (con!=null) con.close();

}

catch (SQLException ignored){}

}

}

}

Uwagi do powyższego przykładu: po pierwsze, logika transakcji zamówienia znajduje się w doPost(), dopóki klient nie będzie mógł powtórzyć akcji. Po drugie, celem przykładu jest przedstawienie logiki transakcji, więc dla uproszczenia założono, że użytkownik kupuje 10 sztuk towaru nr 7 i nie wyświetla formularza zawierającego informacje o karcie kredytowej i zamówieniu. Ponadto, zgłoszenie jakiegokolwiek wyjątku podczas inicjacji sterownika, łączenia z bazą danych, wykonywania SQL lub obsługi karty kredytowej spowoduje wykonanie skoku do bloku catch(), gdzie wywoływana jest metoda rollback() anulująca całą transakcję.

Optymalizacja transakcji

W poprzednim przykładzie umieściliśmy obiekt Connection wewnątrz metody doPost(). Zrezygnowaliśmy więc ze zwiększonej wydajności aplikacji, jaką daje umieszczenie tego obiektu wewnątrz metody init(). Transakcje są ściśle związane z połączeniami, więc połączenia używające transakcji nie mogą być współdzielone. Gdyby połączenia mogły być współdzielone, w serwlecie z poprzedniego przykładu jednocześnie mogłyby być wykonane dwa polecenia: pobranie dziesięciu sztuk towaru i zatwierdzenie transakcji (wywołanie metody commit()). W tabeli MAGAZYN brakowałoby wówczas 10 elementów.

Istnieje kilka możliwości użycia transakcji bez konieczności łączenia się z bazą danych na każde żądanie strony:

0x01 graphic

Rysunek 9.3. Serwlety korzystające z puli połączeń bazy danych

Pule połączeń

Za pomocą puli połączeń możemy podwajać tylko te zasoby, których aktualnie potrzebujemy (w tym wypadku obiekty Connection). Pula połączeń może w sposób inteligentny zarządzać własnym rozmiarem i upewniać się, że każde połączenie jest ważne. Obecnie jest wiele dostępnych pakietów tworzących pule połączeń. Niektóre z nich, np. DbConnectionBroker są dostępne za darmo na stronie http://javaexchange.com. Pakiety te tworzą obiekty, których zadaniem jest wydawanie połączeń na żądanie. Są dostępne również sterowniki puli połączeń (pool drivers), które implementują nowy sterownik JDBC obsługujący połączenia innego (wcześniej załadowanego) sterownika. Użycie sterownika jest najprostszym sposobem na utworzenie puli połączeń w serwletach. Sterownik obsługujący pulę połączeń jest bardziej przeciążony niż sterownik obsługujący jedno połączenie.

Przykład 9.8 przedstawia prosty system puli połączeń. Najpierw tworzone są połączenia, które będą otwierane w miarę potrzeb. Gdy wszystkie połączenia zostaną otwarte i nie będzie już wolnych połączeń, serwlet utworzy nowe połączenia.

Nasz klasa ConnectionPool jest w pełni funkcjonalna, ale można stworzyć klasy korzystające z pakietów komercyjnych służących do tworzenia puli połączeń.

Przykład 9.8.

Klasa ConnectionPool

import java.sql.*;

import java.util.*;

public class ConnectionPool {

private Hashtable connections = new Hashtable();

private Properties props;

public ConnectionPool(Properties props, int initialConnections)

throws SQLException, ClassNotFoundException {

this.props = props;

initializePool(props, initialConnections);

}

public ConnectionPool(String driverClassName, String dbURL,

String user, String password,

int initialConnections)

throws SQLException, ClassNotFoundException {

props = new Properties();

props.put("connection.driver", driverClassName);

props.put("connection.url", dbURL);

props.put("user", user);

props.put("password", password);

initializePool(props, initialConnections);

}

public Connection getConnection() throws SQLException {

Connection con = null;

Enumeration con = connections.keys();

synchronized (connections) {

while (cons.hasMoreElements()) {

con = (Connection)cons.nextElement();

Boolean b = (Boolean)connections.get(con);

if (b == Boolean.FALSE) {

// Znaleźliśmy nieużywane połączenie.

// Należy sprawdzić jego integralność szybkim

// wywołaniem setAutoCommit(true).

// Do użytku komercyjnego powinno się wykonać

//więcej testów, takich jak wykonanie prostego zapytania

try {

con.setAutoCommit(true);

}

catch(SQLException e) {

// Jeśli jest problem z połączeniem należy je zamienić na nowe

connections.remove(con);

con = getNewConnection();

}

//Zaktualizuj tablicę mieszającą

//Zwróć połączenie

return con;

}

}

// Jeśli się tu dostaliśmy, to oznacza, że nie ma wolnych połączeń. Należy

utworzyć nowe połączenie.

//

//

con = getNewConnection();

connections.put(con, Boolean.FALSE);

return con;

}

}

public void returnConnection(Connection returned) {

if (connections.containsKey(returned)) {

connections.put(returned, Boolean.FALSE);

}

}

private void initializePool (Properties props, int initialConnections)

throws SQLException, ClasNotFoundException {

//Ładujemy sterownik

Class.forName(props.getProperty("connection.driver"));

// Umieszcamy nasz zbiór połączeń w tablicy mieszającej

// Wartość FALSE wskazuje na nieużywane połączenie

for (int i = 0; i < initialConnections; i++) {

Connection con = getNewConnection();

connections.put(con, Boolean.FALSE);

}

}

private Connection getNewConnection() throws SQLException {

return DriverManager.getConnection(

props.getProperty("connection.url"),props);

}

}

Klasa ConnectionPool korzysta z tablicy mieszającej (Hashtable) przy użyciu obiektów Connection jako kluczy i obiektów Boolean jako wartości. Wartości Boolean wskazują, czy połączenie jest używane (true), czy wolne (false). Program wywołuje metodę getConnection() obiektu ConnectionPool, aby wykorzystać obiekt Connection. Połączenie jest zwalniane za pomocą wywołania metody returnConnection().Przedstawiony tu model puli połączeń jest bardzo prosty, ale w praktyce często wymaga się bardziej rozbudowanego modelu puli połączeń, w którym jest możliwość sprawdzenia spójności połączenia.

Przykład 9.9 przedstawia ulepszoną wersję serwletu zamówieniowego, który korzysta z puli połączeń.

Przykład 9.9.

Serwlet transakcji zbioru połączeń

import java.io.*;

import java.sql.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class OrderHandlerPool extends HttpServlet {

private ConnectionPool pool;

public void init () throws ServletException {

try {

pool = new ConnectionPool("oracle.jdbc.driver.Oracledriver",

"jdbc:oracle:oci7:orders", "user", "passwd", 5);

}

catch(Exception e) {

throw new UnavailableException("Nie można utworzyć zbioru połączeń");

}

}

public void doPost(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

Connection con = null;

res.setContentType("text/html");

PrintWriter out = res.getWriter();

try {

con = pool.getConnection();

// Rozpocznij transakcję

con.setAutoCommit(false);

Statement stmt = con.createStatement();

stmt.executeUpdate(

"UPDATE MAGAZYN SET ILOSC = (ILOSC - 10) WHERE PRODUCTID = 7");

stmt.executeUpdate(

"UPDATE WYSYLKA SET WYSLANE = (WYSLANE + 10) WHERE PRODUCTID = 7");

changeCard(); // metoda faktycznie nie istnieje ...

con.commit();

out.println("Zamówienie powiodło się! Dziękujemy za współpracę!");

}

catch(Exception e) {

// Każdy błąd jest podstawą do cofnięcia transakcji

try {

con.rollback();

}

catch (SQLException ignored){}

out.println("Zamówienie nie powiodło się. Proszę skontaktować się z obsługą techniczną.");

}

finally {

if (con != null ) pool.returnConnection(con);

}

}

}

Połączenia jako część sesji

Śledzenie sesji, opisane dokładnie w rozdziale 7, pozwala na wykonywanie transakcji w inny sposób. Używając sesji, możemy stworzyć lub przydzielić określone połączenie z bazą danych indywidualnym użytkownikom witryny internetowej lub aplikacji intranetowej. W przykładzie 9.10 serwlet ConnectionPerClient przypisuje obiekt Connection każdemu klientowi HttpSession. To powoduje umieszczenie obiektu Connection wewnątrz obiektu ConnectionHolder, który jest odpowiedzialny za zapewnienie cyklu życia połączenia.

Przykład 9.10.

Skojarzenie Connection z Session.

/* Właściwy Serwlet*/

public class ConnectionPerClient extends HttpServlet {

public void init() throws ServletException {

try {

Class.forName("oracle.jdbc.driver.OracleDriver”);

}

catch (ClassNotfoundException e ) {

throw new UnavailableException ("Nie można zaladowac sterownika bazy Oracle");

}

}

public void doGet (HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

res.setContentType("text/html");

PrintWriter out = res.getWriter();

HttpSession session = req.getSession(true);

Connection con;

// zsynchronizuj: Bez tego dwa uchwyty mogą być utworzone dla jednego klienta.

synchronized (session) {

// spróbuj otrzymać dla tego klienta uchwyt połączenia

ConnectionHolder holder =

(ConnectionHolder) session.getAttribute("servletapp.connection");

// stwórz (i przechowuj) nowe połączenie i uchwyt

if ( holder = null) {

try {

holder = new ConnectionHolder(DriverManager.getConnection(

"jdbc:oracle:oci7:ordersdb", "user", "passwd"));

session.setAttribute("servletapp.connection", holder);

}

catch (SQLException e) {

log(" Nie można otrzymać połączenia db", e);

}

}

// Pobierz właściwe połączenie od uchwytu

con = holder.getConnection();

}

// użyj teraz połączenia

try {

Statement stmt = con.createStatement();

stmt.executeUpdate(

"UPDATE MAGAZYN SET ILOSC = (ILOSC - 10) WHERE PRODUCTID = 7");

stmt.executeUpdate(

"UPDATE WYSYLKA SET WYSLANE = (WYSLANE + 10) WHERE PRODUCTID = 7");

// Obsłuż kartę kredytową i potwierdź transakcję w kolejnym serwlecie

res.sendRedirect(res.encodeRedirectURL(

req.getContextPath() + "/servlet/CreditCardHandler"));

}

catch(Exception e) {

// Jakikolwiek błąd powoduje cofnięcie transakcji

try {

con.rollback();

session.removeAttribute("servletapp.connection");

}

catch (Exception ignored){}

out.println("Zamówienie sie nie powiodło. Skontaktuj się z serwisem technicznym.");

}

}

}

Zamiast bezpośrednio związania połączenia z sesją, stworzono prostą klasę przechowującą połączenia, która implementuje interfejs HttpSessionBindingListener. Takie działanie jest konieczne, gdyż połączenia z bazą danych są jednym z najbardziej ograniczonych zasobów w aplikacji JDBC i trzeba się upewnić, że każde zakończone połączenie zostanie od razu zwolnione. Klasa ta ponadto pozwala na cofnięcie wszystkich nie zatwierdzonych zmian. Jeśli użytkownik naszego hipotetycznego sklepu internetowego opuści system przed weryfikacją danych, jego transakcja zostanie automatycznie anulowana po zakończeniu sesji.

Przechowywanie połączeń w sesjach wymaga dokładnej analizy potrzeb aplikacji. Większość prostych serwerów bazodanowych ma możliwość obsługi maksymalnie ok. 100 połączeń; biurkowe bazy danych takie jak Microsoft Access mogą obsłużyć nawet mniej żądań.

Serwlet księgi gości

Aby zrozumieć koncepcję bazy danych, spójrzmy na prawdziwy serwlet. Przykład 9.11 przedstawia kod źródłowy typowego serwletu księgi gości współpracującego z bazą danych. Obsługuje on stronę www, której goście mają możliwość wpisywania komentarzy i czytania wpisanych wiadomości przez innych użytkowników odwiedzających stronę. Na rysunku 9.4 przedstawiono prezentację tej strony. Wszystkie komentarze gości są wprowadzane do bazy danych. Aby mieć dostęp do informacji zapisanych w bazie, serwlet korzysta z kilku przedstawionych w tym rozdziale technik (m.in. z puli połączeń, preinterpretowanych wyrażeń oraz z obiektu ContextProperties, służącego do odczytania informacji o konfiguracji bazy danych z parametrów kontekstu inicjującego). Serwlet jest rozszerzony o CacheHttpServlet z rozdziału 4, który optymalizuje wydajność wyjścia.

0x01 graphic

Rysunek 9.4.

Księga gości

Przykład 9.11.

Proszę, Wpisz Się

import java.io.*;

import java.sql.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.CacheHttpServlet;

public class Guestbook extends CacheHttpServlet {

static final String SELECT_ALL =

"SELECT name, email, cmt, id FROM guestlist ORDER BY id DESC";

static final String INSERT =

"INSERT INTO guestlist (id, name, email, cmt) " +

"VALUES (?,?,?,?)";

private long lastModified = 0; //

private ConnectionPool pool;

// pobierz wskaźnik do puli połączeń

public void init() throws ServletException {

try {

ServletContext context = getServletContext();

synchronized (context) {

// Pula może już istnieje, jako atrybut kontekstu

pool = (ConnectionPool) context.getAttribute("pool");

if (pool = null) {

// Skonstruuj pulę połączeń używając parametrów kontekstu inicjującego

// connection.driver, connection.url, user, password, itd.

pool= new ConnectionPool( new ContextProperties(context), 3);

context.setAttribute("pool", pool);

}

}

}

catch(Exception e) {

throw new UnavailableException("

Nie można pobrać puli połączeń z kontekstu: " + e.getMessage());

}

}

// Wyświetl istniejące w bazie danych komentarze, poproś o wpisanie nowego

public void doGet (HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

res.setContentType("text/html");

PrintWriter out = res.getWriter();

printHeader(out);

printForm(out);

printMessages(out);

printFooter(out);

}

// Dodaj nowy komentarz i wyślij go do doGet

public void doPost(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

handleForm (req, res);

doGet(req, res);

}

private void printHeader (PrintWriter out) {

out.println("<HTML><HEAD><TITLE>Księga gości</TITLE></HEAD>");

out.println("<BODY>");

}

private void printForm (PrintWriter out) {

out.println("<FORM METHOD=POST>"); // wysyła do siebie

out.println("<B>Prześlij swój komentarz:</B><BR>");

out.println("Twoje nazwisko: <INPUT TYPE=TEXT NAME=name><BR>");

out.println("Twój adres email: <INPUT TYPE=TEXT NAME=email><BR>");

out.println("Komentarz: <INPUT TYPE=TEXT SIZE=50 NAME=comment><BR>");

out.println("<INPUT TYPE=SUBMIT VALUE=\"Wyslij\"><BR>");

out.println("</FORM>");

out.println("<HR>");

}

// przeczytaj i wyświetl wiadomości z bazy danych

private void printMessages (PrintWriter out) throws ServletException {

String name, email, comment;

Connection con= null;

Statement stmt = null;

ResultSet rs = null;

try {

con = pool.get.Connection();

stmt = con.createStatement();

rs = stmt.executeQuery (SELECT_ALL);

while (rs.next()) {

name = rs.getString(1);

if (rs.wasNull()||name.length()==0) name = "Nieznany użytkownik";

email = rs.getString(2);

.if (rs.wasNull()||email.length()==0) name = "Nieznany użytkownik";

.comment = rs.getString(3);

if (rs.wasNull()||comment.length()==0) name = "Bez komentarzy";

out.println("<DL>");

out.println("<DT><B>" + name + "</B> (" + email + ") mówi ");

out.println("<DD><PRE>" + comment + "</PRE>");

out.println("</DL>");

}

}

catch (SQLException e) {

throw new ServletException(e);

}

finally {

try {

.if (stmt != null) stmt.close();

}

catch (SQLException ignored) {}

pool.returnConnection(con);

}

}

private void printFooter (PrintWriter out) {

out.println("</BODY>");

}

// zachowaj nowy komentarz w bazie danych

private HandleForm (HttpServletRequest req,

HttpServletResponse res) throws ServletException {

String name = req.getParameter("name");

String email = req.getParameter("email");

String comment = req.getParameter("comment");

Connection con = null;

PreparedStatement pstmt = null;

try {

con = pool.getConnection();

//Użyj przygotowanego wyrażenia aby automatycznie unikać znaków specjalnych

pstmt = con.prepareStatement(INSERT);

long time = System.currentTimeMillis();

pstmt.setString(1, Long.toString(time));

pstmt.setString(2, name);

pstmt.setString(3, email);

pstmt.setString(4, comment);

pstmt.executeUpdate();

}

catch (SQLException e) {

throw new ServletException(e);

}

finally {

try {

if (pstmt != null) pstmt.close();

}

catch (SQLException ignored) {}

pool.returnConnection(con);

}

//zauważ,że posiadamy nowo zmodyfikowany czas

lastModified = System.currentTimeMillis();

}

public long getLastModified(HttpServletRequest req) {

return lastModified; // wspiera CacheHttpServlet

}

}

Wyrażenia SQL, umożliwiające dostęp do bazy danych znajdują się w górnej części klasy Guestbook w zmiennych typu static final. Wydzielenie tych wyrażeń pozwala na łatwe dokonywanie zmian aplikacji w przyszłości.

Metoda init() pobiera pulę połączeń z obiektu ServletContext lub tworzy ją, gdy w obiekcie ServletContext nie ma zapisanej puli połączeń. Istniejąca pula połączeń jest zapisana jako atrybut ServletContext pod nazwą pool. Jeśli dany atrybut nie istnieje, metoda init() tworzy nową pulę połączeń za pomocą klasy ContextProperties i zapisuje pulę. Pula połączeń nie jest wprawdzie potrzebna temu serwletowi, bo nie obsługuje on transakcji, więc przepustowość przez pojedynczy obiekt Connection jest wystarczająca. Użycie puli połączeń pozwala tu na zarządzanie obiektu Connection, który jest używany jednocześnie przez wiele serwletów.

Metoda doGet() powoduje wyświetlenie nagłówka, formularza HTML z miejscem na wpisanie komentarza i wcześniejszych komentarzy odczytanych z bazy danych. Techniki wykonania bardziej eleganckiej strony HTML opisane są w dalszych rozdziałach, począwszy od rozdziału 14.

Metoda printMessages() używa obiektu Connection pobranego z puli połączeń, wykonuje zapytanie SELECT_ALL za pomocą obiektu Statement. Na początku każdego wiersza obiektu ResultSet powyższa metoda powoduje „dopisanie” znacznika <DL> w formularzu HTML. W końcowym bloku obiekt Connection jest zwalniany i powraca do puli połączeń.

Metoda doPost() jest wywoływana, gdy użytkownik przesyła komentarze za pomocą formularza generowanego za pomocą metody doGet(). Ta metoda wywołuje funkcję handleForm(), która gromadzi komentarze wewnątrz bazy danych, a następnie przesyła komentarze metodzie doGet(), która powoduje wyświetlenie ich na stronie. Metoda handleForm() odczytuje parametry nazwy, adresu email i komentarza oraz umieszcza je w bazie danych za pomocą instrukcji INSERT obiektu PreparedStatement. Używając obiektów PreparedStatement do zachowywania łańcuchów tekstowych automatycznie unikamy znaków specjalnych (zabronionych). Oprócz komentarza, w bazie danych znajduje się informacja o dacie wpisanych komentarzy.

W dolnej części metody handleForm() ustawiany jest parametr lastModified. Pozwala to metodzie getLastModified() zwrócić czas ostatniej aktualizacji bazy danych. Ponieważ ten serwlet jest rozszerzony o CacheHttpServlet, ostatnia zmodyfikowana funkcja będzie wykorzystana przez superklasę do zarządzania pamięcią wyjścia, która będzie zmieniana tylko po aktualizacji bazy danych.*

Zaawansowane techniki JDBC

Teraz, gdy poznaliśmy podstawy, możemy pomówić o kilku zaawansowanych technikach używających serwletów i JDBC. Najpierw sprawdzimy, w jaki sposób serwlety używają procedur bazy danych. Potem dowiemy się, w jaki sposób serwlety pobierają z bazy danych złożone typy danych, takie jak dane binarne(obrazy, aplikacje, itp.), duże ilości tekstu, a nawet wykonywalny kod manipulujący bazą danych.

Przechowywane procedury

Większość baz danych posiada pewien rodzaj wewnętrznego języka programowania służącego do tworzenia i przechowywania procedur działających wewnątrz bazy danych. Utworzone procedury mogą być wywoływane przez zewnętrzne aplikacje. Jednym z przykładów jest język PL/SQL dla Oracle. Bazodanowe języki programowania są często lepiej przystosowane do wykonania pewnych akcji w bazie danych. Wiele już istniejących baz danych posiada wiele zgromadzonych procedur gotowych do użycia, więc dobrze byłoby poznać pewne techniki wykorzystania tych procedur we własnych aplikacjach.

Poniżej przedstawiono kod procedury napisanej w Oracle PL/SQL:

CREATE OR REPLACE PROCEDURE sp_interest

(id IN INTEGER

BAL IN OUT FLOAT) IS

BEGIN

SELECT balance

INTO bal

FROM accounts

WHERE account_id = id;

bal := bal + bal * 0.03;

UPDATE accounts

SET balance = BAL

WHERE account_id = id;

END;

Ta procedura, oprócz instrukcji SQL, wykonuje tylko pewne obliczenie. Ten program jest łatwy do napisania w SQL (transakcja z wcześniejszego przykładu działa w podobny sposób) i stanowi tylko prosty przykład procedury przechowywanej. Istnieje kilka powodów, dla których warto używać procedur przechowywanych:

Procedura Oracle PL/SQL w tym przykładzie pobiera początkowe wartości, np. identyfikator konta i zwraca zaktualizowane saldo. Ponieważ każda baza danych ma swoją własną składnię dostępu do przechowywanych procedur, w JDBC utworzono interfejs pozwalający na korzystanie z przechowywanych procedur niezależnie od rodzaju bazy danych. Ten interfejs to java.sql.CallableStatement. Składnia wywołania procedury nie zwracającej wyniku to np. {call procedure_name(?,?)}, a zwracającej wynik to np.{?=call procedure_name(?,?)}. Parametry wewnątrz nawiasów są opcjonalne.

Sposób użycia klasy CallableStatement jest podobny do sposobu, w jaki używa się klasy PreparedStatement.

CalableStatement cstmt = con.prepareCall("{call sp_interest(?,?)}");

cstmt.registerOutParameter(2, java.sql.Types.FLOAT);

cstmt.setInt(1, accountID);

cstmt.execute();

out.println("Zaktualizowane saldo: " + cstmt.getFloat(2));

Ten program tworzy najpierw obiekt CallableStatement poprzez wywołanie metody prepareCall() obiektu Connection. Ponieważ nasza procedura przechowywana ma parametry wyjściowe, używa metody registerOutParameter obiektu CallableStatement, by zarejestrować wyjściowe parametry jako wartości typu FLOAT. Program wykonuje procedurę i używa metody getFloat() obiektu CallableStatement aby wyświetlić zaktualizowane saldo. Metody getXXX() interfejsu CallableStatement mają podobne właściwości do metod getXXX()interfejsu ResultSet.

Pliki binarne i księgi, czyli bardzo duże obiekty

Większość baz danych wspomaga obsługę dużych obiektów takich jak łańcuchy tekstowe o dużych rozmiarach (do kilku GB) czy plików binarnych, np. plików z obrazkami (*.gif). W zależności od typu bazy obsługa tych danych odbywa się w różny sposób, ale metody zapewniane przez JDBC do pobierania dużych obiektów są standardowe i mogą być stosowane dla różnych typów baz danych. Metoda getAsciiStream() obiektu ResultSet obsługuje duże łańcuchy tekstowe, a getBinaryStream() pracuje na dużych obiektach binarnych. Są to metody operujące na strumieniach, więc każda z tych metod zwraca obiekt InputStream.

Obsługa dużych obiektów stanowi główne źródło problemów pojawiających się podczas kożystania z JDBC. W trakcie projektowania aplikacji należy przetestować sterowniki przy użyciu największych obiektów dla niej przewidzianych. Sterownik JDBC dla Oracle ma szczególną skłonność do błędnej pracy podczas używania dużych obiektów.

Oto fragment kodu serwletu, w którym jest pobierany długi łańcuch ASCII. Zakładamy, że reszta serwletu jest już stworzona:

try {

ResultSet rs = stmt.executeQuery(

"SELECT TITLE, SENDER, MESSAGE FROM MESSAGES WHERE MESSAGE_ID = 9");

if (rs.next()) {

out.println("<H1>" + rs.getString("title") + "</H1>");

out.println("<B>From:</B> ") + rs.getString("sender") + "<BR>");

BufferedReader msgText = new BufferedReader(rs.getAsciiStream("message")));

while (msgText.ready()) {

out.println(msgText.readLine());

}

}

}

catch (SQLException e) {

// zdaj relację

}

Podczas odczytywania z InputStream, serwlet nie otrzymuje wartości z żadnej innej kolumny zbioru wyników. To bardzo ważne, bo wywołanie każdej innej metody getXXX() obiektu ResultSet zamyka poprzedni strumień InputStream.

Dane binarne mogą być pobrane w ten sam sposób używając ResultSet.getBinaryStream(). W tym wypadku należy ustalić właściwy typ danych i przesłać na wyjście w formie strumienia bajtowego. Przykład 9.12 pokazuje serwlet zwracający plik GIF pobrany z bazy danych.

Przykład 9.12.

Wczytywanie binarnego obrazka GIF z bazy danych

import java.io.*;

import java.sql.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class DBGifReader extends HttpServlet {

Connection con;

public void init() throws ServletException {

try {

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver”);

con = DriverManager.getConnection("jdbc:odbc:imagedb","user","passwd");

}

catch (ClassNotfoundException e ) {

throw new UnavailableException ("Nie można zaladowac sterownika JdbcOdbc");

}

catch (SQLException e) {

throw new UnavailableException("Nie można połączyć się z db");

}

}

public void doGet(HttpServletRequest req, HttpServletResponse res)

throws ServletException, IOException {

try{

res.setContentType("image/gif");

ServletOutputStream out = res.getOutputStream()

Statement stmt =con.createStatement();

ResultSet rs = stmt.executeQuery(

"SELECT IMAGE FROM PICTURES WHERE PID = " + req.getParameter("PID"));

if (rs.next()) {

BufferedInputStream gifData =

new BufferedInputStream (rs.getBinaryStream("image"));

byte[] buf = new byte[4 * 1024]; //bufor 4K

int len;

while(( len=gifdata.read(buf,0,buf.length))!=-1){

out.write(buf,0,len);

}

}

else {

res.sendError(res.SC_NOT_FOUND);

}

}

catch (SQLException e) {

// zdaj relację

}

Co dalej?

Poza metodami zawartymi standardowo w JDBC i technikami przedstawionymi w tym rozdziale istnieją dodatkowe interfejsy wspomagające pisanie skomplikowanych aplikacji umożliwiających dostęp do baz danych. Jednym z nich jest interfejs JDBC 2.0, następca omówionego w tym rozdziale JDBC 1.2.

JDBC 2.0 jest szeroko stosowanym narzędziem. Implementacja JDBC 2.0 wymaga bazy danych, która może obsłużyć zaawansowane metody zawarte w interfejsie oraz sterownika, który zapewnia dostęp do tych metod. Na szczęście, wsparcie JDBC 2.0 jest gwarantowane przez wszystkie serwery J2EE, bo każdy serwer zgodny z J2EE wymaga zapewnienia dostępu do sterownika i bazy danych współpracującej z JDBC2.0. Instrukcja implementacji firmy Sun zawiera dodatkowo ewaluacyjną wersję bazy danych Cloudscape.

JDBC 2.0 składa się z dwóch części. Jedna z nich znajduje się w pakiecie Javy 2 java.sql i wymaga bazy danych i sterownika zgodnych z JDBC 2.0. Druga część jest umieszczona w pakiecie javax.sql, który zawiera kilka dodatkowych metod (użycie tych metod nie jest wymagane).

Do udoskonaleń pakietu java.sql należy możliwość modyfikacji i przeglądania zbioru wyników. W obiekcie ResultSet kursor można przesuwać do przodu, do tyłu lub można ustawić go na dowolnym rekordzie (w interfejsie JDBC 1.2 można poruszać się tylko o jeden element do przodu). Dodano możliwość obsługi typów danych SQL3 (BLOB — dla dużych obiektów binarnych, CLOB dla dużych obiektów tekstowych i ARRAY dla tablic).

Opcjonalny pakiet javax.sql zawiera wiele metod szczególnie pomocnych w aplikacjach komercyjnych. Jedną z nich jest wsparcie wyszukiwania JNDI, które pozwala otrzymywać połączenie z bazą danych na podstawie nazwy z serwera JNDI. Kolejną cechą jest wbudowany mechanizm puli połączeń (to bardzo różni interfejs JDBC 2.0 od JDBC 1.2, ale nie wpływa na działanie aplikacji napisanych pod starszy interfejs). Bardzo ważną opcjonalną cechą w JDBC 2.0 jest wspomaganie transakcji rozproszonych. Pozwala to na rozmieszczenie transakcji w różnych oddzielnych bazach danych i aktualizowanie obu baz jedną niepodzielną operacją. W dodatku, JDBC 2.0 wspiera obiekty RowSet, które „opakowują”wiersze danych ResultSet. Ułatwia to zapamiętywanie danych, zapewnia prosty transfer danych przez sieć oraz standaryzowany dostęp do źródeł danych tabelarycznych (np. do arkusza kalkulacyjnego lub pliku jednorodnego).

Więcej informacji o cechach JDBC 2.0 można znaleźć w książce George'a Reese'a Database Programming with JDBC and Java (O'Reilly) i JDBC API Tutorial and Reference autorstwa Seth White oraz na stronie internetowej http://java.sun.com/products/jdbc.

Dla tych, którzy oczekują czegoś więcej niż oferuje JDBC, firma ClearInk sponsoruje otwartą bibliotekę z kodami źródłowymi interfejsu Village, który jest lepszy od JDBC 1.2 i łatwiej współpracuje z bazami danych. Bibliotekę stworzono w oparciu o produkt komercyjny dbKona firmy BEA/WebLogic. Na podstawie Village stworzono bardziej rozbudowaną wersję o nazwie Town. Oba produkty Village i Town są dostępne na stronie http://www.working-dogs.com (jak na razie nie ma żadnych informacji o pakiecie City).

* „Prepared statements” to w dosłownym tłumaczeniu „gotowe zapytania”, ale ponieważ te zapytania są interpretowane przed użyciem, lepszym ich określeniem jest „preinterpretowane zapytania”

* Należy pamiętać, że nie ma możliwości buforowania dla żądań POST. Strona jest odświeżana po każdym przesłanym komentarzu. Gdybyśmy w miejsce komentarza nie wpisali nic, to aplikacja spowoduje ponowne załadowanie całej strony, bo żądanie POST nie zostało zapamiętane.

* RDBMS — maszyna bazodanowa

Do Korekty merytorycznej: proszę o uzupełnienie tłumaczenia rysunków



Wyszukiwarka

Podobne podstrony:
2002 05 10
2001 05 10
Aneks nr 2 Prospekt PKO BP 05 10 2009
Nauka o państwie5 05 10
05 10 2009
loveparade 2010 anlage 11 interner entwurf sicherheitskonzept 20 05 10
Ekonomika Transportu(1)05 10 2011
019 HISTORIA SZTUKI WCZSNOCHRZŚCIJAŃŚKIEJ I BIZANTYJSKIEJ, WYKŁAD, 4 05 10
r11 05 (10)
hme 05 10 16 wykład05
05 10 2009r PRAWO UE
021 HISTORIA SZTUKI WCZESNOCHRZEŚCIJAŃSKIEJ I BIZANTYJSKIEJ, 05 10
mit 05 10
1 05 10 A
nauka o państwie 26.05.10 ....13, studia UMK, nauka o państwie
Gegra na# 05 10
05 10 86
W1 05-10

więcej podobnych podstron