Podstawowym sposobem korzystania z bazy danych w aplikacjach napisanych w Javie jest użycie JDBC. Niesie to ze sobą konieczność pisania zapytań w języku SQL, co z kolei może okazać się dużym problemem w sytuacji, gdy aplikacja powinna móc działać na wielu różnych systemach bazodanowych. Dodatkowo korzystanie z JDBC utrudnia w pełni obiektowe programowanie – przecież o wiele wygodniej i poprawniej byłoby, gdyby miały także reprezentację obiektową.
Dlatego dla Javy powstało kilka rozwiązań
mających na celu rozwiązanie powyższych
problemów. Wśród nich między innymi JDO
– standard, który się jednak za bardzo nie przyjął,
iBATIS – prosty sposób na mapowanie bazy
danych do obiektów, oraz bohater tego artykułu,
Hibernate – potężne narzędzie, cieszące się
ogromną popularnością (jakiś czas temu powstała
także wersja Hibernate dla platformy .NET – NHibernate).
Hibernate ma tak olbrzymie możliwości,
że nie sposób omówić ich w jednym artykule,
czy nawet w całej serii artykułów ukazujących się
przez rok (wystarczy wspomnieć, że podstawowa
książka na jego temat, \”Java Persistence with
Hibernate\” autorstwa Christiana Bauera i Gavina
Kinga, to niemal 900 stron). Dlatego artykuł ten
potraktuj jako wstęp do tej technologii i zachętę
do dalszej pracy.
Instalacja
Hibernate jest tworzone przez firmę JBoss.
Bibliotekę tę znajdziesz na stronie http://www.hibernate.org. Hibernate został podzielony na
kilka części. W celu uruchomienia przykładu z tego
artykułu będziesz potrzebował dwóch z nich:
- Hibernate Core – podstawowa część biblioteki, wykonująca większość pracy,
- Hibernate Annotations – element pozwalający zastąpić większą część konfiguracji Hibernate,
budowanej do tej pory w XML, poprzez adnotacje
w kodzie napisane z użyciem Javy 5.
W celu łatwiejszego i szybszego budowania
aplikacji powinieneś także pobrać Hibernate Tools,
w którego skład wchodzą między innymi zadania
dla Anta.
Instalacja Hibernate sprowadza się do rozpakowania
archiwum z plikami oraz przegrania bibliotek
.jar do odpowiedniego katalogu zawierającego
twój projekt.
Poza Hibernate potrzebna będzie ci także baza
danych. Mimo że Hibernate oferuje wsparcie
dla większości mniej i bardziej popularnych
relacyjnych baz danych, a twoja aplikacja bez
problemów powinna działać na każdej z nich,
to polecam użycie HSQLDB. Jest to mała baza,
składająca się z jednego pliku JAR. Została ona
napisana w Javie. Świetnie nadaje się do celów
demonstracyjnych oraz testowych. Najnowszą
wersję bazy HSQLDB możesz pobrać ze strony
http://hsqldb.org/.
Na koniec pobierz i zainstaluj Anta, czyli
narzędzie automatyzujące proces kompilacji
i wdrożenia (a dzięki zadaniom Hibernate Tools
także nieco więcej).
Projekt
W ramach tego artykułu zbudujesz w oparciu
o Hibernate prosty program, obsługujący newsy.
Nie będzie to jednak program typowo webowy,
a aplikacja konsolowa. Jest to działanie celowe
– cała otoczka webowa niepotrzebnie zaciemniałaby
kod związany bezpośrednio z Hibernate. Gdy
już będziesz wiedział, na czym to wszystko polega,
sam dasz sobie radę z obudowaniem Hibernate
wybranym przez siebie frameworkiem MVC – np.
wspomnianym już Struts.
Nasz projekt będzie miał dwie główne klasy:
- News – klasę odpowiedzialną za przechowywanie
podstawowych informacji o newsie, - User – klasę przechowującą informacje o użytkowniku
(tworzącym newsy).
Jak łatwo się domyślić, klasa News będzie miała
odwołanie do klasy User, czyli do użytkownika,
który danego newsa utworzył.
Dodatkowo będziesz potrzebował jeszcze klasy
wykonującej przykładowe działania na dwóch
powyższych, łącznie z zachowaniem ich stanu
w bazie – nazwiemy ją Run, a także specjalnej
klasy narzędziowej HibernateUtil.
Klasa User
Klasa ta odpowiada za przechowywanie informacji
o użytkowniku, który jest, bądź też może być
autorem newsów. Klasa ta jest bardzo prosta:
package news;
import javax.persistence.*;
public class User {
private Long id;
private String name;
private String surname;
private String email;
private String password;
public User() {
}
public User(String name, String surname, String email, String password) {
this.name = name;
this.surname = surname;
this.email = email;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Zwróć uwagę na domyślny, bezparametrowy
konstruktor. Jest on wymagany przez Hiberante
do tego, aby klasa mogła być utrwalana w bazie.
Przyjrzyj się również typowym getterom oraz
setterom, czyli metodom, dzięki którym masz
z zewnątrz dostęp do prywatnych pól klasy, takich
jak name czy surename.
Aby taka klasa mogła być zmapowana przez Hibernate
do tabeli relacyjnej bazy danych, Hibernate
musi być poinformowany o tym, jak zbudowana
jest odpowiednia tabela. Konfigurację możesz
zapisać w pliku XML bądź, jak już wcześniej
wspomniałem, za pomocą adnotacji w kodzie Java.
Ten drugi sposób jest zdecydowanie wygodniejszy,
szczególnie gdy nie przewidujesz częstych zmian
w schemacie tabel.
Pierwsze adnotacje należy dodać przed nazwą
klasy:
@Entity
@Table(name = \"USERS\")
public class User {
Informują one o tym, że klasa User ma zostać
zmapowana do tabeli USERS. Odpowiednie
adnotacje należy także dodać przy poszczególnych
polach klasy. Pole id jest kluczem głównym:
@Id @GeneratedValue
@Column(name = \"USER_ID\")
private Long id;
{stala}@Id{/stala} oznacza, że pole jest kluczem głównym,
{stala}@GeneratedValue{/stala} informuje że poszczególne
wartości pola mają być automatycznie generowane,
a {stala}@Column{/stala} definiuje nazwę kolumny
w tabeli (USERS), z którą dane pole ma być
powiązane.
W przypadku pozostały pól określamy juz tylko
nazwy kolumn w tabeli:
@Column(name = \"NAME\")private String name;
@Column(name = \"SURNAME\")private String surname;
@Column(name = \"EMAIL\")private String email;
@Column(name = \"PASSWORD\")private String password;
Klasa News
Klasa News jest bardzo podobna do klasy User.
Również zawiera dwa konstruktory, pola prywatne
określające zawartość newsa, a także gettery i settery
pozwalające na zarządzanie tymi polami.
package news;
import javax.persistence.*;
public class News {
private Long id;
private String title;
private String content;
private User author;
public News() {
}
public News(String title, String content, User author) {
this.title = title;
this.content = content;
this.author = author;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public User getAuthor() {
return author;
}
public void setAuthor() {
this.author = author;
}
}
Same adnotacje też niewiele się różnią od tych
z klasy User:
@Entity
@Table(name = \"NEWS\")
public class News {
@Id @GeneratedValue
@Column(name = \"NEWS_ID\")
private Long id;
@Column(name = \"TITLE\")
private String title;
@Column(name = \"CONTENT\")
private String content;
//...
}
jednak w przypadku pola author są całkowicie
inne:
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = \"USER_ID\")
private User author;
Ustanawiają one połączenie klasy News z klasą
User poprzez pole author. Określają także sposób
usuwania rekordów na kaskadowy, czyli w przypadku
usunięcia użytkownika, automatycznie
usunięte zostaną wszystkie napisane przez niego
newsy. Nazwa kolumny łączącej klasę News z klasą
User ustalana jest za pomocą adnotacji {stala}@JoinColumn{/stala}.
I tak większość pracy już masz za sobą!
Pozostałe klasy
Dla wygody pracy z Hibernate zbudujemy
jeszcze klasę pomocniczą – HibernateUtil. Oto kod,
który zaraz przeanalizujemy:
package presistence;
import org.hibernate.*;
import org.hibernate.cfg.*;
public class HibernateUtil {
private static SessionFactory sessionFactory;
static {
try {
sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
} catch(Throwable ex) {
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
public static void shutdown() {
getSessionFactory().close();
}
}
Klasa ta tworzy w bloku statycznym statyczną
zmienną {stala}sessionFactory{/stala}, która jest zwracana przez
statyczną metodę {stala}getSesionFactory{/stala}. Zmienna ta
wskazuje na fabrykę sesji, która w typowym przypadku
aplikacji korzystającej z Hibernate powinna
być tylko jedna na całą aplikację – stąd jest ona
statyczna i tworzona przy pierwszym odwołaniu do
klasy HibernateUtil.
Sesje w Hibernate określają niejako połączenie
z bazą. Ta sesja posiada API używane do nadawania
trwałości obiektom. Także za pośrednictwem
sesji tworzone są transakcje.
Klasa HibernateUtil w przypadku naszej prostej
aplikacji jest poniekąd zbędna – wszystko, co
się tu znajduje, można było zawrzeć w klasie Run.
Jeśli jednak aplikacja byłaby bardziej złożona,
a jednocześnie chciałbyś pracować z obiektami
wykonywanymi w wielu klasach, klasa HibernateUtil
okaże się bardzo pomocna.
Dzięki niej
możesz zaoszczędzić sobie pisania ciągle tego samego
kodu oraz zapewnić istnienie tylko jednego
obiektu klasy {stala}SessionFactory{/stala} na aplikację.
W naszym przykładzie tylko jedna klasa korzysta
z klas News oraz User. Jest to klasa Run:
package news;
import java.util.*;
import org.hibernate.*;
import presistence.*;
class Run {
public static void main(String[]args) {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.begin-Transaction();
User user = new User(\"Jan\",\"Kowalski\", \"jan@kowalski.pl\",\"123456\");
News news = new News(\"Startujemy\",\"Dziµ startujemy\", user);
Long newsId = (Long)session.save-(news);
tx.commit();
session.close();
session = HibernateUtil.getSessionFactory().openSession();
tx = session.beginTransaction();
List newsList = session.createQuery(\"from News n where n.author= :a\").setParameter(\"a\", user).list();
System.out.println(\"Znaleziono\"+newsList.size() + \" newsy\");
for (News n : newsList) {
System.out.println(n.getId()+\".\"+n.getTitle());
System.out.println(n.getContent());
System.out.println();
}
tx.commit();
session.close();
session = HibernateUtil.getSessionFactory().openSession();
tx = session.beginTransaction();
List
Jak widzisz, klasa ta już na samym początku
korzysta z klasy {stala}HibernateUtil{/stala} w celu utworzenia
nowej sesji, a w niej transakcji:
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.begin-Transaction();
Następnie, tak jak zawsze w Javie, tworzone są
obiekty klas {stala}User{/stala} oraz News:
User user = new User(\"Jan\", \"Kowalski\",\"jan@kowalski.pl\", \"123456\");
News news = new News(\"Startujemy\",\"Dziś startujemy\", user);
Zauważ, że obiekt klasy News tworzony jest
z odwołaniem do obiektu klasy {stala}User{/stala}, czyli z informacją,
kto jest autorem danego newsa. Gdy oba obiekty są już stworzone, utrwalamy
je:
Long newsId = (Long)session.save-(news);
oraz zatwierdzamy transakcję i zamykamy sesję:
tx.commit();
session.close();
Pewnie zastanawiasz się, gdzie podziało się zapisanie
obiektu user. Jest ono niepotrzebne! Obiekt
news powstał z odwołaniem do obiektu user, tak
więc do jego pełnego utrwalenia konieczne jest
utrwalenie obu obiektów. W dodatku o wszystko
dba tu Hibernate!
Następnie klasa Run już w nowej sesji i transakcji
odczytuje informacje o wszystkich newsach
napisanych przez autora user:
session = HibernateUtil.getSession-Factory().openSession();
tx = session.beginTransaction();
List newsList = session.createQuery(\"from News n where n.author= :a\").setParameter(\"a\", user).list();
Rozbijmy ostatnią linijkę i omówmy każdy jej
element z osobna:
session.createQuery(\"from News nwhere n.author = :a\")
Ten kod tworzy zapytanie w języku HQL. Jest to
wewnętrzny język zapytań Hibernate, który mimo
podobieństwa do SQL-a, SQL-em nie jest. Już na
samym początku widać różnicę: brak informacji
o kolumnach, które nas interesują.
I nic dziwnego,
w końcu tutaj działamy na obiektach, nie na
rekordach, tak więc możemy pobrać albo cały
obiekt, albo nic. Samo zapytanie, szczególnie jeśli
znasz SQL-a, nie powinno sprawić ci problemu
– odczytujemy tu wszystkie obiekty klasy News,
których autorem jest… no właśnie. Ciąg :a zostaje
podmieniony odpowiednią wartością za pomocą
metody {stala}setParameter(){/stala}:
setParameter(\"a\", user)
Funkcja ta binduje (łączy) parametr z zapytania
poprzedzony dwukropkiem (:) z odpowiednią,
następującą po dwukropku nazwą – tutaj jest to a.
Dzięki temu parametr z zapytania traktowany jest
tak, jak gdyby był podpiętym obiektem.
Na końcu wywoływana jest metoda {stala}list(){/stala}.
Odpowiada ona za pobranie wszystkich obiektów
zwróconych przez zapytanie i zwrócenie ich w postaci
listy. Stąd wynik wywołania całości przypisany
jest do listy obiektów klasy News:
List newsList
Samo wyświetlenie informacji o newsach to już
typowa pętla Javy:
for (News n : newsList) {
System.out.println(n.getId()+\".\"+n.getTitle());
System.out.println(n.getContent());
System.out.println();
}
Nieco bardziej złożone jest wydobywanie
informacji na temat dwóch i więcej klas. Przyjrzyj
się poniższemu fragmentowi:
session = HibernateUtil.getSession-Factory().openSession();
tx = session.beginTransaction();
List
Zapytanie:
from News n, User u where n.author =u.id
wybiera powiązane ze sobą obiekty klas News
i User, czyli pobiera newsa i użytkownika będącego
jego autorem. Jako że zapytania zwracają obiekty,
a nie rekordy, tak jak w przypadku bazy, może się
wydawać, że wynik będzie mocno skomplikowany.
W rzeczywistości wynikiem takiego zapytania jest
tablica obiektów.
Jako że tablica ta przechowuje obiekty różnych
typów – w naszym przypadku News i User
– otrzymujemy w wyniku listę tablic obiektów typu
Object:
List
Odczytanie danych z takiej tablicy też nie
powinno nastręczać zbyt wielu problemów. Istotne
jest aby pamiętać, że obiekty w tablicy umieszczane
są w kolejności wystąpienia w zapytaniu:
for (Object[] p : pairs) {
News n = (News)p[0];
User u = (User)p[1];
Po zakończeniu pracy zamykamy fabrykę sesji
drugą metodą statyczną z klasy pomocniczej
HibernateUtil:
HibernateUtil.shutdown();
Konfiguracja
Aplikacja jest już niemal gotowa. Pozostała
tylko jej konfiguracja, która przechowywana
jest w pliku o nazwie {stala}hibernate.cfg.xml{/stala}. Musi
się on znajdować na ścieżce CLASS_PATH,
najlepiej jednak umieścić ten plik w katalogu ze
źródłami aplikacji (a po kompilacji w tym samym
miejscu w katalogu z klasami), poza wszystkimi
pakietami.
Plik z konfiguracją jest plikiem XML o dość
prostej budowie:
org.hsqldb.jdbcDriver
jdbc:hsqldb:hsql://localhost
sa
org.hibernate.dialect.HSQLDialect
5
20
300
50
3000
Definicja DOCTYPE jest konieczna i jest używana
do sprawdzania poprawności budowy pliku
konfiguracyjnego przez parser. Cała konfiguracja
zawiera się w znaczniku hibernate-configuration.
Wewnątrz znacznika session-factory znajduje
się konfiguracja fabryki sesji, w tym połączenia
z bazą.
Za pomocą znaczników property ustawia się
wartości poszczególnych parametrów:
- {stala}hibernate.connection.driver_class{/stala} – klasa sterownika
JDBC bazy danych, - {stala}hibernate.connection.url{/stala} – adres określający
połaczenie z bazą, - {stala}hibernate.connection.username{/stala} – nazwa użytkownika
bazy, - {stala}hibernate.dialect{/stala} – dialekt, jakim ma się posługiwać
Hibernate podczas generowania zapytań
SQL. W naszym przypadku jest to dialekt bazy
HSQLDB – HSQLDialect.
Parametry hibernate.c3p0 odpowiadają
za konfigurację puli połączeń z bazą danych.
Pula określa liczbę połączeń, jakie mogą zostać
nawiązane, dodatkowo odpowiada za zarządzanie
połączeniami. Nie są one zamykane przy kończeniu
połączenia, lecz są wykorzystywane w momencie
próby ponownego nawiązania połączenia z bazą.
Dzięki temu zyskuje się na prędkości działania
aplikacji, gdyż niejednokrotnie to połączenie jest
najdłużej trwającą operacją na bazie danych.
Parametry puli to:
- {stala}hibernate.c3p0.min_size{/stala} – minimalna liczba
przechowywanych połączeń, - {stala}hibernate.c3p0.max_size{/stala} – maksymalna liczba
połączeń, - {stala}hibernate.c3p0.timeout{/stala} – czas, po jakim usuwane
są nieczynne połączenia (w sekundach), - {stala}hibernate.c3p0.idle{/stala}_test_period – czas, po
jakim nieaktywne połączenia są sprawdzanie
(w sekundach).Na koniec za pomocą znacznika mapping ustalane
są klasy, które mają być zmapowane do tabel
relacyjnej bazy danych – czyli te, którym chcesz
zapewnić trwałość. Pamiętaj, że musisz tu podać
pełną nazwę wraz z pakietem.
Uruchomienie
Pozostało skompilować i uruchomić naszą aplikację.
Można co prawda zrobić to ręcznie, jednak
o wiele lepiej nadaje się do tego Ant. Niestety brak
tu miejsca na opisanie skryptu Ant. Gotowy skrypt
znajdziesz w źródłach dołączonych do płytki CD,
którą otrzymałeś razem z magazynem.
Skrypt ten szuka źródeł aplikacji w katalogu src,
a następnie umieszcza skomplikowaną aplikację
w katalogu bin.
Biblioteki (pliki jar) powinny znajdować
się w katalogu lib. Skrypt dostarcza między
innymi następujących zadań (tak zwanych tasków):
- {stala}compile{/stala} – kompilacja aplikacji,
- {stala}run{/stala} – uruchomienie aplikacji, a w razie potrzeby
także jej kompilacja, - {stala}schemaexport{/stala} – eksport schematu bazy danych
do pliku news-ddl.sql. Schemat generowany jest
automatycznie na podstawie klas zmapowanych
w pliku konfiguracyjnym, - {stala}dbmanager{/stala} – uruchomienie managera bazy
HSQLDB.
Polecenia podane niżej wykonuj z linii poleceń
(menu Start > Uruchom, w nowym oknie wpisz
cmd i wciśnij enter).
Uruchom bazę danych, wykonując polecenie
(zobacz rysunek 1):
java -classpath lib/hsqldb.jar org.hsqldb.Server
Pamiętaj, że ścieżka podana za parametrem
-classpath musi wskazywać na plik hsqldb.jar. Gdy
baza się uruchomi, powinieneś zobaczyć to, co
przedstawiono na rysunku 1.
Okno z bazą musisz
pozostawić otwarte tak długo, jak długo baza ma
być uruchomiona. Zatrzymanie bazy sprowadza
się do zamknięcia okna bądź wciśnięcia w oknie
z uruchomioną bazą kombinacji klawiszy CTRL+C.
Nadszedł czas na założenie odpowiednich tabel
w bazie. Będąc w katalogu ze skryptem {stala}build.xml{/stala},
wydaj polecenie:
{stala}ant schemaexport{/stala}
Po zakończeniu działania tego polecenia w katalogu
pojawi się plik {stala}news-ddl.sql{/stala} z instrukcjami
zakładającymi potrzebne tabele w bazie.
Następnie ponownie z poziomu linii poleceń
wydaj polecenie:
{stala}ant dbmanager{/stala}
Spowoduje to uruchomienie menedżera bazy
HSQLDB (zobacz rysunek 2). W oknie menedżera
wklej kod z wygenerowanego pliku {stala}news-ddl.sql{/stala}
i kliknij przycisk Execute SQL.
Teraz możesz już uruchomić program:
{stala}ant run{/stala}
I co dalej?
Jeśli zainteresował cię temat Hibernate, musisz
pogodzić się z tym że w języku polskim niewiele
o nim znajdziesz. Ostatnio ukazało się tłumaczenie
książki \”Beginning Hibernate: From Novice to
Professional\” autorstwa Jeffa Linwooda i Dave\’a
Mintera, wydanej pod polskim tytułem \”Hibernate\”.
Od nowicjusza do Profesjonalisty\”. I to właściwie
wszystko. Jeśli jednak język angielski nie jest dla
ciebie problemem, wybór książek jest o wiele większy,
wliczając w to najlepszą pozycję na ten temat,
czyli \”Java Persistence With Hibernate\” , napisanej
przez Christiana Bauera i Gavina Kinga.
Książka ta
w obszerny sposób omawia zarówno Hibernate,
jak i JPA, nowy standard ORM wspierany przez
Hibernate.