2. JPA
2.1. Przykład 1 – Reprezentacja obiektowa pojedynczej tabeli
2.1.1. Tabela [personne]
Rozważmy bazę danych zawierającą jedną tabelę [personne], której zadaniem jest przechowywanie pewnych informacji o osobach:
![]() |
klucz podstawowy tabeli | |
wersja wiersza w tabeli. Za każdym razem, gdy osoba jest modyfikowana, jej numer wersji jest zwiększany. | |
nazwisko osoby | |
imię | |
data urodzenia | |
liczba całkowita 0 (nieżonaty) lub 1 (żonaty) | |
liczba dzieci tej osoby |
2.1.2. Entyteta [Personne]
Znajdujemy się w następującym środowisku wykonawczym:
![]() |
Warstwa JPA [5] musi stanowić pomost między światem relacyjnym bazy danych [7] a światem obiektowym [4] obsługiwanym przez programy Java [3]. Pomost ten tworzy się poprzez konfigurację i istnieją dwa sposoby, aby to zrobić:
- za pomocą plików XML. Był to praktycznie jedyny sposób postępowania aż do pojawienia się wersji JDK 1.5
- za pomocą adnotacji Java od wersji JDK 1.5
W niniejszym dokumencie będziemy korzystać niemal wyłącznie z drugiej metody.
Obiekt [Personne], będący odzwierciedleniem tabeli [personne] przedstawionej wcześniej, mógłby wyglądać następująco:
...
@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// konstruktorzy
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
setNom(nom);
setPrenom(prenom);
setDatenaissance(datenaissance);
setMarie(marie);
setNbenfants(nbenfants);
}
// toString
public String toString() {
...
}
// metody pobierające i ustawiające
...
}
Konfiguracja odbywa się za pomocą adnotacji Java @Annotation. Adnotacje Java są wykorzystywane albo przez kompilator, albo przez specjalistyczne narzędzia w czasie wykonywania. Poza adnotacją w wierszu 3, przeznaczoną dla kompilatora, wszystkie pozostałe adnotacje są tutaj przeznaczone dla używanej implementacji JPA, czyli Hibernate lub Toplink. Będą one zatem wykorzystywane w czasie wykonywania. W przypadku braku narzędzi zdolnych do ich interpretacji adnotacje te są ignorowane. W ten sposób powyższa klasa [Personne] mogłaby być wykorzystywana w kontekście poza JPA.
Należy rozróżnić dwa przypadki wykorzystania adnotacji JPA w klasie C powiązanej z tabelą T:
- tabela T już istnieje: adnotacje JPA muszą wówczas odzwierciedlać istniejący stan (nazwy i definicje kolumn, ograniczenia integralności, klucze obce, klucze główne itp.)
- tabela T nie istnieje i zostanie utworzona na podstawie adnotacji znalezionych w klasie C.
Przypadek 2 jest najłatwiejszy do obsługi. Za pomocą adnotacji JPA określamy pożądaną strukturę tabeli T. Przypadek 1 jest często bardziej złożony. Tabela T mogła zostać utworzona dawno temu, poza jakimkolwiek kontekstem JPA. Jej struktura może wówczas być nieodpowiednio dostosowana do mostu relacyjno-obiektowego JPA. Dla uproszczenia przyjmujemy przypadek 2, w którym tabela T powiązana z klasą C zostanie utworzona na podstawie adnotacji JPA klasy C.
Omówmy adnotacje klasy [Personne]:
- wiersz 4: adnotacja @Entity jest pierwszą niezbędną adnotacją. Umieszcza się ją przed wierszem deklarującym klasę i wskazuje, że dana klasa ma być zarządzana przez warstwę trwałości JPA. W przypadku braku tej adnotacji wszystkie pozostałe adnotacje JPA zostałyby zignorowane.
- wiersz 5: adnotacja @Table określa tabelę w bazie danych, której klasa jest reprezentacją. Jej głównym argumentem jest name, który określa nazwę tabeli. W przypadku braku tego argumentu tabela będzie nosiła nazwę klasy, w tym przypadku [Personne]. W naszym przykładzie adnotacja @Table jest zatem zbędna.
- wiersz 8: adnotacja @Id służy do wskazania pola w klasie, które jest odwzorowaniem klucza głównego tabeli. Ta adnotacja jest obowiązkowa. Wskazuje ona tutaj, że pole id z wiersza 11 jest odwzorowaniem klucza głównego tabeli.
- wiersz 9: adnotacja @Column służy do powiązania pola klasy z kolumną tabeli, której to pole jest odzwierciedleniem. Atrybut name określa nazwę kolumny w tabeli. W przypadku braku tego atrybutu kolumna nosi tę samą nazwę co pole. W naszym przykładzie argument name nie był zatem obowiązkowy. Argument nullable=false oznacza, że kolumna powiązana z polem nie może przyjmować wartości NULL, a zatem pole musi koniecznie posiadać wartość.
- wiersz 10: adnotacja @GeneratedValue określa sposób generowania klucza głównego, gdy jest on generowany automatycznie przez SGBD. Tak będzie we wszystkich naszych przykładach. Nie jest to obowiązkowe. Tak więc nasza osoba mogłaby posiadać numer studencki, który służyłby jako klucz główny i nie byłby generowany przez SGBD, lecz ustalany przez aplikację. W takim przypadku adnotacja @GeneratedValue byłaby nieobecna. Argument strategy określa sposób generowania klucza głównego, gdy jest on generowany przez SGBD. Nie wszystkie SGBD stosują tę samą technikę generowania wartości klucza głównego. Na przykład:
wykorzystuje generator wartości wywoływany przed każdym wstawieniem | |
pole klucza głównego jest zdefiniowane jako typ Identity. Uzyskujemy wynik podobny do generatora wartości w Firebirdzie, z tą różnicą, że wartość klucza jest znana dopiero po wstawieniu wiersza. | |
wykorzystuje obiekt o nazwie SEQUENCE, który również pełni rolę generatora wartości |
Warstwa JPA musi generować różne polecenia SQL w zależności od SGBD, aby utworzyć generator wartości. Poprzez konfigurację określa się jej typ obiektu SGBD, którym ma zarządzać. Dzięki temu może ona rozpoznać, jaka jest standardowa strategia generowania wartości klucza głównego dla tego obiektu SGBD. Argument strategy = GenerationType.AUTO wskazuje warstwie JPA, że ma zastosować tę standardową strategię. Technika ta sprawdziła się we wszystkich przykładach zawartych w niniejszym dokumencie dla siedmiu wykorzystanych SGBD.
- wiersz 14: adnotacja @Version określa pole służące do zarządzania współbieżnym dostępem do tego samego wiersza tabeli.
Aby zrozumieć problem równoczesnego dostępu do tego samego wiersza tabeli [personne], załóżmy, że aplikacja internetowa umożliwia aktualizację danych osoby, i rozważmy następujący przypadek:
W momencie T1 użytkownik U1 rozpoczyna edycję danych osoby P. W tym momencie liczba dzieci wynosi 0. Zmienia tę liczbę na 1, ale zanim zatwierdzi swoją zmianę, użytkownik o identyfikatorze U2 rozpoczyna edycję tej samej osoby P. Ponieważ U1 nie zatwierdził jeszcze swojej zmiany, U2 widzi na ekranie, że liczba dzieci wynosi 0. U2 zmienia imię osoby P na wielkie litery. Następnie U1 i U2 zatwierdzają swoje zmiany w tej kolejności. Zmiana wprowadzona przez U2 zostanie zachowana: w bazie imię zostanie zapisane wielkimi literami, a liczba dzieci pozostanie równa zero, mimo że U1 sądzi, że zmienił ją na 1.
Pojęcie wersji osoby pomaga nam rozwiązać ten problem. Rozważmy ten sam przypadek użycia:
W momencie T1 użytkownik U1 rozpoczyna edycję osoby P. W tym momencie liczba dzieci wynosi 0, a wersja to V1. Zmienia liczbę dzieci na 1, ale zanim zatwierdzi swoją zmianę, użytkownik U2 rozpoczyna edycję tej samej osoby P. Ponieważ U1 nie zatwierdził jeszcze swojej zmiany, U2 widzi liczbę dzieci równą 0, a wersję V1. U2 zmienia imię osoby P na wielkie litery. Następnie U1 i U2 zatwierdzają swoje zmiany w tej kolejności. Przed zatwierdzeniem zmiany sprawdza się, czy osoba wprowadzająca zmiany w danym użytkowniku P posiada tę samą wersję, co aktualnie zarejestrowany użytkownik P. Tak będzie w przypadku użytkownika U1. Jego zmiana zostaje zatem zaakceptowana, a wersja edytowanej osoby zostaje zmieniona z V1 na V2, aby odnotować fakt, że osoba ta została zmieniona. Podczas zatwierdzania zmiany wprowadzonej przez U2 zauważymy, że U2 posiada wersję V1 osoby P, podczas gdy obecnie jej wersja to V2. Będziemy wtedy mogli poinformować użytkownika U2, że ktoś go wyprzedził i że musi zacząć od nowej wersji osoby P. Użytkownik ten to zrobi, pobierze osobę P o wersji V2, która ma teraz dziecko, zamieni imię na wielkie litery i zatwierdzi zmiany. Jego zmiana zostanie zaakceptowana, jeśli zarejestrowana osoba P nadal ma wersję V2. Ostatecznie zmiany wprowadzone przez U1 i U2 zostaną uwzględnione, podczas gdy w przypadku scenariusza bez wersji jedna ze zmian zostałaby utracona.
Warstwa [dao] aplikacji klienckiej może samodzielnie zarządzać wersją klasy [Personne]. Za każdym razem, gdy nastąpi zmiana obiektu P, wersja tego obiektu zostanie zwiększona o 1 w tabeli. Adnotacja @Version pozwala przenieść to zarządzanie na warstwę JPA. Odpowiednie pole wcale nie musi nazywać się version, jak w przykładzie. Może nosić dowolną nazwę.
Pola odpowiadające adnotacjom @Id i @Version istnieją ze względu na trwałość danych. Nie byłyby one potrzebne, gdyby klasa [Personne] nie musiała być trwale zapisywana. Widać zatem, że obiekt ma inną reprezentację w zależności od tego, czy musi być trwale zapisany, czy nie.
- wiersz 17: ponownie adnotacja @Column, dostarczająca informacji o kolumnie tabeli [personne] powiązanej z polem nom klasy Personne. Znajdują się tu dwa nowe argumenty:
- unique=true oznacza, że imię i nazwisko osoby musi być unikalne. W bazie danych przełoży się to na dodanie ograniczenia unikalności dla kolumny NOM w tabeli [personne].
- length=30 ustala liczbę znaków w kolumnie NOM na 30. Oznacza to, że typ tej kolumny będzie wynosił VARCHAR(30).
- wiersz 24: adnotacja @Temporal służy do wskazania, jaki typ SQL należy przypisać kolumnie/pólowi typu data/czas. Typ TemporalType.DATE oznacza samą datę bez powiązanej godziny. Inne możliwe typy to TemporalType.TIME do kodowania godziny oraz TemporalType.TIMESTAMP do kodowania daty wraz z godziną.
Omówmy teraz pozostałą część kodu klasy [Personne]:
- wiersz 6: klasa implementuje interfejs Serializable. Operacja sérialisation na obiekcie polega na przekształceniu go w ciąg bitów. Operacja désérialisation jest operacją odwrotną. Serializacja i deserializacja są wykorzystywane zwłaszcza w aplikacjach typu klient-serwer, w których obiekty są wymieniane przez sieć. Aplikacje klienckie lub serwerowe nie są świadome tej operacji, która jest wykonywana w sposób przezroczysty przez JVM. Aby jednak była ona możliwa, klasy wymienianych obiektów muszą być „oznaczone” słowem kluczowym Serializable.
- wiersz 37: konstruktor klasy. Należy zauważyć, że pola id i version nie są częścią parametrów. W rzeczywistości te dwa pola są zarządzane przez warstwę JPA, a nie przez aplikację.
- wiersze 51 i kolejne: metody get i set dla każdego z pól klasy. Należy zauważyć, że adnotacje JPA można umieścić na metodach get pól zamiast na samych polach. Miejsce umieszczenia adnotacji określa tryb, w jakim JPA ma uzyskać dostęp do pól:
- jeśli adnotacje są umieszczone na poziomie pól, JPA uzyska bezpośredni dostęp do pól w celu ich odczytu lub zapisu
- jeśli adnotacje są umieszczone na poziomie metod get, JPA będzie uzyskiwać dostęp do pól za pośrednictwem metod get / set w celu ich odczytu lub zapisu
To właśnie pozycja adnotacji @Id określa pozycję adnotacji JPA w danej klasie. Umieszczona na poziomie pola oznacza bezpośredni dostęp do pól, a umieszczona na poziomie get – dostęp do pól za pośrednictwem metod get i set. Pozostałe adnotacje należy wówczas umieścić w taki sam sposób jak adnotację @Id.
2.1.3. Projekt Eclipse do testów
Nasze pierwsze eksperymenty przeprowadzimy na poprzedniej encji [Personne]. Wykorzystamy do tego następującą architekturę:
![]() |
- w [7]: baza danych, która zostanie wygenerowana na podstawie adnotacji encji [Personne] oraz dodatkowych konfiguracji wprowadzonych w pliku o nazwie [persistence.xml]
- w [5, 6]: warstwa JPA zaimplementowana przez Hibernate
- w pliku [4]: encja [Personne]
- w [3]: program testowy typu konsolowego
Przeprowadzimy różne eksperymenty:
- wygenerujemy schemat BD na podstawie skryptu Ant i narzędzia Hibernate Tools
- wygenerujemy BD i zainicjujemy go kilkoma danymi
- wykorzystanie schematu BD i wykonanie czterech podstawowych operacji na tabeli [personne] (wstawianie, aktualizacja, usuwanie, zapytanie)
Potrzebne narzędzia to:
- Eclipse wraz z wtyczkami opisanymi w punkcie 5.2.
- projekt [hibernate-personnes-entites], który znajduje się w folderze <przykłady>/hibernate/direct/osoby-entities
- różne pliki SGBD opisane w załącznikach (punkt 5 i kolejne).
Projekt Eclipse wygląda następująco:
![]() |
- w pliku [1]: folder projektu Eclipse
- w pliku [2]: projekt zaimportowany do Eclipse (Plik / Importuj)
- w pliku [3]: jednostka [Personne] będąca przedmiotem testów
- w [4]: programy testowe
- w [5]: [persistence.xml] to plik konfiguracyjny warstwy JPA
- w pliku [6]: wykorzystywane biblioteki. Zostały one opisane w punkcie 1.5.
- w [8]: skrypt ant, który zostanie wykorzystany do wygenerowania tabeli powiązanej z encją [Personne]
- w [9]: pliki [persistence.xml] dla każdego z użytych SGBD
- w [10]: schematy wygenerowanej bazy danych dla każdego z wykorzystanych plików SGBD
Opiszemy te elementy po kolei.
2.1.4. Entyteta [Personne] (2)
Wprowadzamy niewielką zmianę do poprzedniego opisu encji [Personne] oraz uzupełniamy informacje:
package entites;
...
@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// konstruktory
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
....
}
// toString
public String toString() {
return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
.format(getDatenaissance()), isMarie(), getNbenfants());
}
// metody pobierające i ustawiające
...
}
- wiersz 7: nadajemy nazwę [jpa01_personne] tabeli powiązanej z encją [Personne]. W niniejszym dokumencie zostaną utworzone różne tabele w schemacie o nazwie jpa. Pod koniec tego samouczka schemat jpa będzie zawierał wiele tabel. Aby ułatwić czytelnikowi orientację, tabele powiązane ze sobą będą miały ten sam prefiks jpaxx_.
- wiersz 45: metoda [toString] służąca do wyświetlenia obiektu [Personne] na konsoli.
2.1.5. Konfiguracja warstwy dostępu do danych
W powyższym projekcie Eclipse konfiguracja warstwy JPA jest zapewniona przez plik [META-INF/persistence.xml]:
![]() |
Podczas uruchamiania program szuka pliku [META-INF/persistence.xml] w folderze classpath aplikacji. W naszym projekcie Eclipse cała zawartość folderów [/src] i [1] jest kopiowana do folderów [/bin] i [2]. Ten folder jest częścią katalogu classpath projektu. Z tego powodu katalog [META-INF/persistence.xml] zostanie znaleziony podczas konfiguracji warstwy JPA.
Domyślnie Eclipse nie umieszcza kodów źródłowych w folderze [/src] projektu, lecz bezpośrednio w samym folderze. Wszystkie nasze projekty Eclipse zostaną skonfigurowane tak, aby kody źródłowe znajdowały się w folderze [/src], a skompilowane klasy w folderze [/bin], jak pokazano w punkcie 5.2.1.
Przyjrzyjmy się konfiguracji warstwy JPA zawartej w pliku [persistence.xml] naszego projektu:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- klasy trwałe -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
<property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- automatyczne tworzenie schematu -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- właściwości DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
Aby zrozumieć tę konfigurację, musimy przyjrzeć się architekturze dostępu do danych w naszej aplikacji:
![]() |
- plik [persistence.xml] skonfiguruje warstwy [4, 5, 6]
- [4]: implementacja JPA w Hibernate
- [5]: Hibernate uzyskuje dostęp do bazy danych za pośrednictwem puli połączeń. Pula połączeń to rezerwa otwartych połączeń z SGBD. Dostęp do SGBD mają wielu użytkowników, podczas gdy ze względu na wydajność nie może on przekroczyć limitu N jednocześnie otwartych połączeń. Dobrze napisany kod otwiera połączenie z serwerem SGBD na jak najkrótszy czas: wysyła polecenia do serwera SQL, a następnie zamyka połączenie. Będzie to powtarzał za każdym razem, gdy zajdzie potrzeba pracy z bazą danych. Koszt otwarcia i zamknięcia połączenia nie jest bez znaczenia i właśnie tu do akcji wkracza pula połączeń. Po uruchomieniu aplikacji pula ta otworzy N1 połączeń z serwerem SGBD. To właśnie do puli aplikacja zwróci się o otwarte połączenie, gdy będzie go potrzebować. Połączenie to zostanie zwrócone do puli, gdy tylko aplikacja przestanie go potrzebować, najlepiej jak najszybciej. Połączenie nie jest zamykane i pozostaje dostępne dla kolejnego użytkownika. Pula połączeń jest zatem systemem współdzielenia otwartych połączeń.
- [6]: sterownik JDBC używany przez SGBD
Zobaczmy teraz, w jaki sposób plik [persistence.xml] konfiguruje powyższe warstwy [4, 5, 6]:
- wiersz 2: tagiem głównym pliku XML jest <persistence>.
- wiersz 3: element <persistence-unit> służy do zdefiniowania jednostki trwałości. Może istnieć wiele jednostek trwałości. Każda z nich ma nazwę (atrybut name) oraz typ transakcji (atrybut transaction-type). Aplikacja będzie miała dostęp do jednostki trwałości poprzez jej nazwę, w tym przypadku jpa. Typ transakcji RESOURCE_LOCAL oznacza, że aplikacja samodzielnie zarządza transakcjami przy użyciu SGBD. Tak będzie w tym przypadku. Gdy aplikacja działa w kontenerze EJB3, może korzystać z jego usługi transakcyjnej. W takim przypadku należy ustawić transaction-type=JTA (Java Transaction API). JTA jest wartością domyślną, gdy atrybut transaction-type nie występuje.
- wiersz 5: tag <provider> służy do zdefiniowania klasy implementującej interfejs [javax.persistence.spi.PersistenceProvider], który umożliwia aplikacji zainicjowanie warstwy trwałości. Ponieważ używamy implementacji JPA / Hibernate, klasa użyta w tym przypadku jest klasą Hibernate.
- wiersz 6: tag <properties> wprowadza właściwości specyficzne dla konkretnego, wybranego interfejsu provider. W zależności od tego, czy wybrano Hibernate, Toplink, Kodo itp., właściwości będą się różnić. Poniższe właściwości są specyficzne dla Hibernate.
- wiersz 8: nakazuje Hibernate przeszukanie pliku classpath projektu w celu znalezienia klas opatrzonych adnotacją @Entity, aby je zarządzać. Klasy @Entity można również zadeklarować za pomocą tagów <class>nom_de_la_classe</class>, bezpośrednio pod tagiem <persistence-unit>. Tak właśnie postąpimy w przypadku provider JPA / Toplink.
- Wiersze 10–12, tutaj zakomentowane, konfigurują logi konsoli Hibernate:
- wiersz 10: określa, czy mają być wyświetlane polecenia SQL generowane przez Hibernate na SGBD. Jest to bardzo przydatne na etapie nauki. Ze względu na most relacyjno-obiektowy aplikacja operuje na obiektach trwałych, na których wykonuje operacje typu [persist, merge, remove]. Bardzo interesujące jest ustalenie, jakie polecenia SQL są faktycznie generowane podczas tych operacji. Analizując je, stopniowo zaczynamy odgadnąć polecenia typu SQL, które Hibernate wygeneruje podczas wykonywania danej operacji na obiektach trwałych, a most relacyjno-obiektowy zaczyna nabierać konkretnych kształtów w naszej głowie.
- wiersz 11: polecenia SQL wyświetlane na konsoli można ładnie sformatować, aby ułatwić ich odczyt
- wiersz 12: wyświetlane polecenia SQL będą ponadto opatrzone komentarzami
- wiersze 15–19 definiują warstwę JDBC (warstwa [6] w architekturze):
- wiersz 15: klasa sterownika JDBC dla SGBD, w tym przypadku MySQL5
- wiersz 16: adres URL wykorzystywanej bazy danych
- wiersze 17, 18: nazwa użytkownika i hasło do połączenia
- W tym miejscu wykorzystujemy elementy wyjaśnione w załącznikach w paragrafie 5.5. Zachęcamy czytelnika do zapoznania się z tą sekcją dotyczącą MySQL5.
- wiersz 22: Hibernate musi znać SGBD, z którym ma do czynienia. Wszystkie SGBD mają bowiem własne rozszerzenia SQL, co stanowi specyficzny sposób zarządzania automatycznym generowaniem wartości klucza głównego, ... co sprawia, że Hibernate musi znać obiekt SGBD, z którym współpracuje, aby móc wysyłać do niego polecenia SQL, które ten obiekt zrozumie. [MySQL5InnoDBDialect] określa SGBD MySQL5 z tabelami typu InnoDB, które obsługują transakcje.
- wiersze 24–28 konfigurują pulę połączeń c3p0 (warstwa [5] w architekturze):
- wiersze 24, 25: minimalna (domyślnie 3) i maksymalna liczba połączeń (domyślnie 15) w puli. Domyślna początkowa liczba połączeń wynosi 3.
- wiersz 26: maksymalny czas oczekiwania w milisekundach na żądanie połączenia ze strony klienta. Po upływie tego czasu c3p0 zwróci mu wyjątek.
- wiersz 27: aby uzyskać dostęp do BD, Hibernate wykorzystuje przygotowane polecenia SQL (PreparedStatement), które c3p0 może zapisać w pamięci podręcznej. Oznacza to, że jeśli aplikacja po raz drugi zażąda przygotowanego polecenia SQL, które znajduje się już w pamięci podręcznej, nie będzie konieczne jego ponowne przygotowywanie (przygotowanie polecenia SQL wiąże się z kosztem), a wykorzystane zostanie to, które znajduje się w pamięci podręcznej. W tym miejscu określa się maksymalną liczbę przygotowanych zleceń SQL, które może pomieścić pamięć podręczna, uwzględniając wszystkie połączenia (przygotowane zlecenie SQL należy do jednego połączenia).
- wiersz 28: częstotliwość sprawdzania poprawności połączeń w milisekundach. Połączenie z puli może stać się nieprawidłowe z różnych powodów (sterownik JDBC unieważnia połączenie z powodu zbyt długiego czasu trwania, sterownik JDBC wykazuje „błędy” itp.).
- wiersz 20: w tym miejscu żądamy, aby podczas inicjalizacji jednostki trwałości wygenerowano bazę danych obrazów obiektów @Entity. Hibernate dysponuje teraz wszystkimi narzędziami do wysyłania poleceń SQL służących do generowania tabel bazy danych:
- konfiguracja obiektów @Entity pozwala mu określić, które tabele należy wygenerować
- wiersze 15–18 i 24–28 umożliwiają nawiązanie połączenia z SGBD
- wiersz 22 pozwala określić, którego dialektu SQL należy użyć do generowania tabel
W ten sposób plik [persistence.xml] używany w tym przypadku tworzy nową bazę danych przy każdym uruchomieniu aplikacji. Tabele są odtwarzane (create table) po ich usunięciu (drop table), jeśli wcześniej istniały. Należy zauważyć, że oczywiście nie należy tego robić w przypadku bazy produkcyjnej...
Testy wykazały, że faza usuwania i tworzenia tabel mogła zakończyć się niepowodzeniem. Miało to miejsce zwłaszcza wtedy, gdy w ramach tego samego testu przechodzono z warstwy JPA/Hibernate do warstwy JPA/Toplink lub odwrotnie. Na podstawie tych samych obiektów @Entity obie implementacje nie generują dokładnie tych samych tabel, generatorów, sekwencji itp., a czasami zdarzało się, że faza usuwania i tworzenia kończyła się niepowodzeniem i konieczne było ręczne usunięcie tabel. W części „Załączniki”, w paragrafie 5 i kolejnych, opisano aplikacje, których można użyć do ręcznego wykonania tej pracy. Należy zauważyć, że implementacja JPA/Hibernate okazała się najbardziej wydajna na tym etapie początkowego tworzenia zawartości bazy danych: awarie zdarzały się rzadko.
Narzędzia wykorzystywane przez warstwę JPA / Hibernate znajdują się w bibliotece [jpa-hibernate], przedstawionej w paragrafie 1.5 na stronie 8. Sterowniki JDBC niezbędne do uzyskania dostępu do SGBD znajdują się w bibliotece [jpa-divers]. Obie te biblioteki zostały umieszczone w classpath w ramach analizowanego tutaj projektu. Poniżej przypominamy ich zawartość:
![]() |
2.1.6. Generowanie bazy danych za pomocą skryptu Ant
Jak właśnie widzieliśmy, Hibernate udostępnia narzędzia do generowania bazy danych obrazującej obiekty @Entity aplikacji. Hibernate może:
- wygenerować plik tekstowy z poleceniami SQL służący do utworzenia bazy. Wykorzystywany jest wówczas wyłącznie dialekt określony w pliku [persistence.xml].
- utworzyć tabele odzwierciedlające obiekty @Entity w docelowej bazie danych zdefiniowanej w pliku [persistence.xml]. W tym przypadku wykorzystywany jest cały plik [persistence.xml].
Przedstawimy skrypt Ant, który może wygenerować schemat bazy danych oraz tabele obiektów @Entity. Ten skrypt nie jest mojego autorstwa: opiera się on na podobnym skrypcie z pliku [ref1]. Ant (Another Neat Tool) to narzędzie do wykonywania zadań w języku Java w trybie wsadowym. Skrypty Ant nie są łatwe do zrozumienia dla początkujących. Wykorzystamy tylko jeden z nich, ten, który teraz omawiamy:
![]() |
- w [1]: struktura katalogów przykładów z tego samouczka.
- w [2]: folder [personnes-entites] projektu Eclipse, który obecnie analizujemy
- w [3]: folder <lib> zawierający pięć bibliotek jar zdefiniowanych w paragrafie 1.5.
- w [4]: archiwum [hibernate-tools.jar] niezbędne do wykonania jednego z zadań skryptu [ant-hibernate.xml], który będziemy omawiać.
![]() |
- w [5]: projekt Eclipse oraz skrypt [ant-hibernate.xml]
- w [6]: folder projektu [src]
Skrypt [ant-hibernate.xml] [5] będzie korzystał z archiwów JAR znajdujących się w folderze <lib> [3], w szczególności archiwum [hibernate-tools.jar] [4] z folderu [lib/hibernate]. Odtworzyliśmy strukturę katalogów, aby czytelnik mógł zobaczyć, że aby znaleźć katalog [lib], zaczynając od katalogu [personnes-entites] [2] skryptu [ant-hibernate.xml], należy podążać ścieżką: ../../../lib.
Przyjrzyjmy się skryptowi [ant-hibernate.xml]:
<project name="jpa-hibernate" default="compile" basedir=".">
<!-- nazwa projektu i wersja -->
<property name="proj.name" value="jpa-hibernate" />
<property name="proj.shortname" value="jpa-hibernate" />
<property name="version" value="1.0" />
<!-- Właściwości globalne -->
<property name="src.java.dir" value="src" />
<property name="lib.dir" value="../../../lib" />
<property name="build.dir" value="bin" />
<!-- ścieżka klasy projektu -->
<path id="project.classpath">
<fileset dir="${lib.dir}">
<include name="**/*.jar" />
</fileset>
</path>
<!-- Pliki konfiguracyjne, które powinny znajdować się w ścieżce klasy-->
<patternset id="conf">
<include name="**/*.xml" />
<include name="**/*.properties" />
</patternset>
<!-- Czyszczenie projektu -->
<target name="clean" description="Nettoyer le projet">
<delete dir="${build.dir}" />
<mkdir dir="${build.dir}" />
</target>
<!-- Kompilacja projektu -->
<target name="compile" depends="clean">
<javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
</target>
<!-- Skopiowanie plików konfiguracyjnych do ścieżki klasy -->
<target name="copyconf">
<mkdir dir="${build.dir}" />
<copy todir="${build.dir}">
<fileset dir="${src.java.dir}">
<patternset refid="conf" />
</fileset>
</copy>
</target>
<!-- Narzędzia Hibernate -->
<taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />
<!-- Generowanie pliku DDL bazy danych -->
<target name="DDL" depends="compile, copyconf" description="Génération DDL base">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- Użyj META-INF/persistence.xml -->
<jpaconfiguration />
<!-- eksport -->
<hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
<!-- Wygeneruj bazę -->
<target name="BD" depends="compile, copyconf" description="Génération BD">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- Użyj META-INF/persistence.xml -->
<jpaconfiguration />
<!-- eksport -->
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
</project>
- wiersz 1: projekt [ant] nosi nazwę „jpa-hibernate”. Zawiera on zbiór zadań, z których jedno jest zadaniem domyślnym: w tym przypadku jest to zadanie o nazwie „compile”. Skrypt ant jest wywoływany w celu wykonania zadania T. Jeśli zadanie to nie jest określone, wykonywane jest zadanie domyślne. parametr basedir="." oznacza, że dla wszystkich ścieżek względnych występujących w skrypcie punktem wyjścia jest folder, w którym znajduje się skrypt ant, w tym przypadku folder <exemples>/hibernate/direct/personnes-entites.
- wiersze 3–11: definiują zmienne skryptowe za pomocą tagu <property name="nomVariable" value="valeurVariable"/>. Zmienna ta może być następnie wykorzystana w skrypcie w notacji ${nomVariable}. Nazwy mogą być dowolne. Przyjrzyjmy się bliżej zmiennym zdefiniowanym w wierszach 9–11:
- wiersz 9: definiuje zmienną o nazwie „src.java.dir” (nazwa jest dowolna), która w dalszej części skryptu będzie odnosić się do folderu zawierającego kod źródłowy Java. Jej wartością jest „src” – ścieżka względna do folderu wskazanego przez atrybut basedir (wiersz 1). Jest to zatem ścieżka „./src”, gdzie znak „.” oznacza tutaj folder <exemples>/hibernate/direct/personnes-entites. Kod źródłowy Java znajduje się właśnie w folderze <personnes-entites>/src (por. [6] powyżej).
- wiersz 10: definiuje zmienną o nazwie „lib.dir”, która w dalszej części skryptu będzie wskazywać katalog zawierający archiwa JAR potrzebne do wykonania zadań Java w skrypcie. Jej wartość ../../../lib wskazuje na folder <exemples>/lib (patrz [3] powyżej).
- wiersz 11: definiuje zmienną o nazwie „build.dir”, która w dalszej części skryptu będzie wskazywać folder, w którym mają być generowane pliki .class powstałe w wyniku kompilacji plików źródłowych .java. Jej wartość „bin” wskazuje na folder <personnes-entites>/bin. Wyjaśniliśmy już, że w analizowanym projekcie Eclipse folder <bin> był tym, w którym generowano pliki .class. Ant postąpi tak samo.
- wiersze 14–18: tag <path> służy do definiowania elementów classpath, z których będą korzystać zadania ant. W tym przypadku ścieżka „project.classpath” (nazwa jest dowolna) obejmuje wszystkie archiwa .jar znajdujące się w drzewie katalogów <exemples>/lib.
- wiersze 21–24: tag <patternset> służy do wskazania zbioru plików za pomocą wzorców nazw. W tym przypadku patternset o nazwie „conf” oznacza wszystkie pliki z rozszerzeniami .xml lub .properties. Ten patternset posłuży do wskazania plików .xml i .properties z katalogu <src> (persistence.xml, log4j.properties) (por. [6]), które są plikami konfiguracyjnymi aplikacji. Podczas wykonywania niektórych zadań pliki te muszą zostać skopiowane do folderu <bin>, aby znalazły się w katalogu classpath projektu. Wówczas do ich oznaczenia użyjemy pliku patternset conf.
- wiersze 27–30: tag <target> określa zadanie skryptu. Jest to pierwsze takie zadanie, z jakim mamy do czynienia. Wszystko, co poprzedzało ten fragment, dotyczyło konfiguracji środowiska uruchomieniowego skryptu ant. Zadanie nosi nazwę „clean”. Jest ono wykonywane w dwóch etapach: folder <bin> jest usuwany (wiersz 28), a następnie odtwarzany (wiersz 29).
- wiersze 33–35: zadanie „compile”, które jest zadaniem domyślnym skryptu (wiersz 1). Zależy ono (atrybut „depends”) od zadania „clean”. Oznacza to, że przed uruchomieniem zadania „compile” skrypt ant musi najpierw wykonać zadanie „clean”, czyli skrypt c.a.d, w celu wyczyszczenia folderu <bin>. Celem zadania „compile” jest w tym przypadku kompilacja kodu źródłowego Java z folderu <src>.
- wiersz 34: wywołanie kompilatora Java z trzema parametrami:
- srcdir: folder zawierający kod źródłowy Java, w tym przypadku folder <src>
- destdir: folder, w którym mają zostać zapisane wygenerowane pliki .class, w tym przypadku folder <bin>
- classpathref: ścieżka classpath, która ma być użyta do kompilacji, w tym przypadku wszystkie archiwa jar z drzewa katalogów <lib>
- (ciąg dalszy)
- wiersze 38–45: zadanie copyconf, którego celem jest skopiowanie do folderu <bin> wszystkich plików .xml i .properties z folderu <src>.
- wiersz 48: definicja zadania za pomocą tagu <taskdef>. Takie zadanie ma służyć do ponownego wykorzystania w innych miejscach skryptu. Jest to ułatwienie w kodowaniu. Ponieważ zadanie jest używane w różnych miejscach skryptu, definiuje się je raz za pomocą tagu <taskdef>, a następnie ponownie wykorzystuje się je poprzez jego nazwę, gdy jest to potrzebne.
- Zadanie nosi nazwę hibernatetool (atrybut name).
- Jego klasa jest określona przez atrybut classname. W tym przypadku wskazana klasa znajduje się w archiwum [hibernate-tools.jar], o którym już wspominaliśmy.
- Atrybut classpathref wskazuje zadaniu ant, gdzie ma szukać poprzedniej klasy
- (ciąg dalszy)
- wiersze 51–60 dotyczą zadania, które nas tutaj interesuje, a mianowicie generowania schematu bazy danych obrazów obiektów @Entity naszego projektu Eclipse.
- wiersz 51: zadanie nosi nazwę DDL (podobnie jak Data Definition Language, zadanie SQL związane z tworzeniem obiektów bazy danych). Zależy ono od zadań „compile” i „copyconf” w tej kolejności. Zadanie DDL spowoduje zatem, w podanej kolejności, wykonanie zadań „clean”, „compile” i „copyconf”. Gdy uruchamia się zadanie DDL, folder <bin> zawiera pliki .class odpowiadające plikom źródłowym .java, w szczególności obiekty @Entity, a także plik [META-INF/persistence.xml], który konfiguruje warstwę JPA / Hibernate.
- wiersze 53–59: wywoływane jest zadanie [hibernatetool] zdefiniowane w wierszu 48. Przekazywanych jest mu wiele parametrów, oprócz tych już zdefiniowanych w wierszu 48:
- wiersz 53: folder wyjściowy dla wyników wygenerowanych przez zadanie będzie folderem bieżącym.
- wiersz 54: katalogiem <bin> zadania classpath będzie katalog <bin>
- wiersz 56: wskazuje zadaniu [hibernatetool], w jaki sposób może ono rozpoznać swoje środowisko wykonania: tag <jpaconfiguration/> informuje ją, że znajduje się w środowisku JPA i że w związku z tym musi użyć pliku [META-INF/persistence.xml], który znajdzie tutaj, w swoim classpath.
- Wiersz 58 określa warunki generowania bazy danych: drop=true oznacza, że przed utworzeniem tabel muszą zostać wydane polecenia SQL typu „drop table”, create=true oznacza, że należy utworzyć plik tekstowy z poleceniami SQL służącymi do utworzenia bazy danych, a outputfilename określa nazwę tego pliku SQL – w tym przypadku schema.sql w folderze <ddl> projektu Eclipse, a export=false oznacza, że wygenerowane polecenia SQL nie powinny być wykonywane w połączeniu z SGBD. Jest to ważna kwestia: oznacza to, że do wykonania zadania nie ma potrzeby uruchamiania docelowego pliku SGBD. Parametr delimiter określa znak oddzielający dwa polecenia SQL w wygenerowanym schemacie, a format=true powoduje, że wygenerowany tekst zostanie poddany podstawowemu formatowaniu.
- wiersze 51–60 dotyczą zadania, które nas tutaj interesuje, a mianowicie generowania schematu bazy danych obrazów obiektów @Entity naszego projektu Eclipse.
- (ciąg dalszy)
- wiersze 63–72 definiują zadanie o nazwie BD. Jest ono identyczne z poprzednim zadaniem DDL, z tą różnicą, że tym razem generuje bazę danych (export="true" w wierszu 70). Zadanie nawiązuje połączenie z zadaniem SGBD, korzystając z informacji znalezionych w zadaniu [persistence.xml], aby uruchomić w nim schemat SQL i wygenerować bazę danych. Aby wykonać zadanie BD, konieczne jest zatem uruchomienie zadania SGBD.
2.1.7. Wykonanie zadania przed zadaniem DDL
Aby uruchomić skrypt [ant-hibernate.xml], musimy najpierw dokonać kilku ustawień w środowisku Eclipse.
![]() |
- w [1]: wybierz [External Tools]
- w [2]: utworzyć nową konfigurację ant
![]() |
- w [3]: nadać nazwę konfiguracji ant
- w [5]: wyznaczyć skrypt ant za pomocą przycisku [4]
- w [6]: zastosuj zmiany
- w [7]: utworzono konfigurację ant DDL
![]() |
![]() |
- w [8]: w zakładce JRE należy określić, który plik JRE ma być używany. Pole [10] jest zazwyczaj wstępnie wypełnione wartością JRE używaną przez Eclipse. Zatem zazwyczaj nie ma potrzeby wprowadzania żadnych zmian w tym panelu. Niemniej jednak spotkałem się z przypadkiem, w którym skrypt ant nie mógł znaleźć kompilatora <javac>. Nie znajduje się on w JRE (Java Runtime Environment), ale w JDK (Java Development Kit). Narzędzie ant w Eclipse odnajduje ten kompilator poprzez zmienną środowiskową JAVA_HOME (Start / Panel sterowania / Wydajność i konserwacja / System / zakładka Zaawansowane / przycisk Zmienne środowiskowe) o nazwie [A]. Jeśli ta zmienna nie została zdefiniowana, można umożliwić ant znalezienie kompilatora <javac>, wpisując w [10] nie JRE, ale JDK. Znajduje się on w tym samym folderze co pliki JRE i [B]. Użyjemy przycisku [9], aby zgłosić plik JDK wśród dostępnych plików JRE i [C], aby następnie móc go wybrać w pliku [10].
- w [12]: w zakładce [Targets] wybiera się zadanie DDL. W ten sposób konfiguracja ant, którą nazwaliśmy DDL [7], będzie odpowiadać wykonaniu zadania o nazwie DDL [12], które, jak wiemy, generuje schemat DDL bazy danych obrazów obiektów @Entity aplikacji.
![]() |
- w [13]: zatwierdzamy konfigurację
- w [14]: uruchamia się ją
W widoku [console] wyświetlane są logi wykonania zadania ant DDL:
Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
[delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
[mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
[javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
[copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
- przypomnijmy, że zadanie DDL nosi nazwę [hibernatetool] (wiersz 10) i jest zależne od zadań clean (wiersz 2), compile (wiersz 5) oraz copyconf (wiersz 7).
- wiersz 10: zadanie [hibernatetool] przetwarza plik [persistence.xml] z konfiguracji JPA
- wiersz 11: zadanie [hbm2ddl] wygeneruje schemat DDL bazy danych
- wiersze 12–22: schemat bazy danych DDL
Pamiętamy, że zadanie [hbm2ddl] miało wygenerować schemat DDL w konkretnym miejscu:
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
- wiersz 74: schemat ma zostać wygenerowany w pliku ddl/schema.sql. Sprawdźmy:
![]() |
- w [1]: plik ddl/schema.sql rzeczywiście istnieje (należy wykonać F5, aby odświeżyć drzewo katalogów)
- na [2]: jego zawartość. Jest to schemat bazy danych MySQL5. Plik konfiguracyjny [persistence.xml] dla warstwy JPA określał bowiem pliki SGBD i MySQL5 (wiersz 8 poniżej):
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
<!-- automatyczne tworzenie schematu -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- właściwości DataSource c3p0 -->
...
Przyjrzyjmy się powiązaniu między obiektem a relacją, które zostało tutaj utworzone, analizując konfigurację obiektu @Entity Personne oraz wygenerowany schemat DDL:
![]() |
![]() |
Należy zwrócić uwagę na kilka kwestii:
- A1-B1: nazwa tabeli podana w pliku A1 jest zgodna z nazwą używaną w pliku B1. Warto zwrócić uwagę na wpis drop, który w pliku B1 poprzedza wpis create.
- A2-B2: przedstawia sposób generowania klucza głównego. Tryb AUTO określony w A2 przełożył się na atrybut autoincrement właściwy dla MySQL5. Tryb generowania klucza głównego jest najczęściej specyficzny dla SGBD.
- A3-B3: przedstawia typ SQL z bitem specyficznym dla MySQL5 w celu reprezentacji typu Java boolean.
Powtórzmy ten test z innym SGBD:
![]() |
- Folder [conf] [1] zawiera pliki [persistence.xml] dla różnych plików SGBD. Weźmy na przykład plik Oracle [2] i umieśćmy go w folderze [META-INF] [3] w miejsce poprzedniego. Jego zawartość jest następująca:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- Klasy trwałe -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- automatyczne tworzenie schematu -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
<!-- właściwości DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
Zachęcamy czytelnika do zapoznania się z sekcją dotyczącą Oracle (punkt 5.7) w załącznikach, zwłaszcza w celu zrozumienia konfiguracji pliku JDBC.
W tym przypadku istotny jest właściwie tylko wiersz 25: informuje on Hibernate, że od tej pory SGBD jest SGBD Oracle. Wykonanie zadania ant DDL daje powyższy wynik [4]. Warto zauważyć, że schemat Oracle różni się od schematu MySQL5. Jest to zaleta JPA: programista nie musi martwić się o te szczegóły, co znacznie zwiększa przenośność jego rozwiązań.
2.1.8. Wykonanie zadania ant BD
Być może pamiętamy, że zadanie ant o nazwie BD wykonuje to samo, co zadanie ant DDL, ale dodatkowo generuje bazę danych. Należy zatem uruchomić zadanie SGBD. Rozważymy przypadek zadań SGBD i MySQL5 i zachęcamy czytelnika do skopiowania pliku [conf/mysql5/persistence.xml] do folderu [src/META-INF]. Aby sprawdzić działanie zadania, wykorzystamy wtyczkę SQL Explorer (patrz punkt 5.2.6) w celu sprawdzenia stanu pliku jpa BD przed i po wykonaniu zadania ant BD.
Najpierw musimy utworzyć nową konfigurację ant w celu wykonania zadania BD. Zaleca się postępowanie zgodnie z instrukcjami opisanymi dla konfiguracji DDL w punkcie 2.1.7. Nowa konfiguracja ant będzie nosiła nazwę BD:
![]() |
- w [1]: powielamy poprzednią konfigurację o nazwie DDL
- na [2]: nową konfigurację nazywamy BD. Konfiguracja ta uruchamia zadania o nazwach ant, BD oraz [3], które fizycznie tworzą bazę danych.
- Po wykonaniu tej czynności uruchom SGBD oraz MySQL5 (punkt 5.5).
Teraz korzystamy z wtyczki SQL Explorer do przeglądania baz danych zarządzanych przez SGBD. W razie potrzeby czytelnik powinien wcześniej zapoznać się z obsługą tej wtyczki (patrz punkt 5.2.6).
![]() |
- [1]: otwieramy perspektywę SQL Explorer [Window / Open Perspective / Other]
- [2]: w razie potrzeby należy utworzyć połączenie [mysql5-jpa] (patrz punkt 5.5.5, strona 252) i je otworzyć
- [3]: logujemy się jako jpa / jpa
- [4]: nawiązano połączenie z plikiem MySQL5.
![]() |
- w [5]: plik BD zawiera tylko jedną tabelę: [articles]
- w [6]: uruchamia się wykonanie zadania ant BD. Ponieważ znajdujemy się w perspektywie [SQL Explorer], nie widzimy widoku [Console], który pokazuje nam logi zadania. Można wyświetlić ten widok [Window / Show View / ...] lub powrócić do perspektywy Java [Window / Open Perspective / ...].
- w widoku [7]: po zakończeniu zadania Ant BD można ewentualnie powrócić do perspektywy [SQL Explorer] i odświeżyć drzewo BD jpa.
- w [8]: widoczna jest utworzona tabela [jpa01_personne].
Zachęcamy czytelnika do ponownego wygenerowania pliku BD przy użyciu innych plików SGBD. Należy postępować w następujący sposób:
- skopiować plik [conf/<sgbd>/persistence.xml] do folderu [src/META-INF], gdzie <sgbd> to przetestowany plik SGBD
- uruchomić <sgbd>, postępując zgodnie z instrukcjami zawartymi w załącznikach dotyczących tego pliku
- w oknie SQL Explorer utworzyć połączenie z <sgbd>. Jest to również wyjaśnione w załącznikach dla każdego z plików SGBD
- powtórzyć poprzednie testy
W tym momencie mamy już pewną wiedzę:
- lepiej rozumiemy pojęcie mostu obiektowo-relacyjnego. W tym przypadku został on zrealizowany przez Hibernate. Później będziemy korzystać z Toplink.
- wiemy, że ten most obiektowo-relacyjny jest konfigurowany w dwóch miejscach:
- w obiektach @Entity, gdzie określa się powiązania między polami obiektów a kolumnami tabel w pliku BD
- w pliku [META-INF/persistence.xml], gdzie przekazujemy implementacji JPA informacje o dwóch elementach mostu obiektowo-relacyjnego: obiektach @Entity (obiekt) oraz bazie danych (relacja).
- utworzyliśmy dwa zadania Ant o nazwach DDL i BD, które pozwalają nam utworzyć bazę danych na podstawie powyższej konfiguracji, jeszcze przed napisaniem jakiegokolwiek kodu Java.
Teraz, gdy warstwa JPA naszej aplikacji jest poprawnie skonfigurowana, możemy zacząć zgłębiać API i JPA za pomocą kodu Java.
2.1.9. : kontekst trwałości aplikacji
Wyjaśnijmy nieco środowisko uruchomieniowe klienta JPA:
![]() |
Wiemy, że warstwa JPA [2] tworzy most obiektowo-relacyjny [3] / [4]. Zbiór obiektów zarządzanych przez warstwę JPA w ramach tego mostu obiektowo-relacyjnego nazywamy „kontekstem trwałości”. Aby uzyskać dostęp do danych z kontekstu trwałości, klient JPA [1] musi przejść przez warstwę JPA [2]:
- może utworzyć obiekt i poprosić warstwę JPA o zapewnienie jego trwałości. Obiekt staje się wówczas częścią kontekstu trwałości.
- może zwrócić się do warstwy [JPA] o odwołanie do istniejącego obiektu trwałego.
- może zmodyfikować obiekt trwały uzyskany z warstwy JPA.
- może zwrócić się do warstwy JPA o usunięcie obiektu z kontekstu trwałości.
Warstwa JPA udostępnia klientowi interfejs o nazwie [EntityManager], który – jak sama nazwa wskazuje – umożliwia zarządzanie obiektami @Entity w kontekście trwałości. Poniżej przedstawiamy główne metody tego interfejsu:
umieszcza entity w kontekście trwałości | |
usuwa entity z kontekstu trwałości | |
łączy obiekt entity z klienta, który nie jest zarządzany przez kontekst trwałości z obiektem entity z kontekstu trwałości o tym samym kluczu głównym. Wynikiem jest obiekt entity z kontekstu trwałości. | |
umieszcza w kontekście trwałości obiekt wyszukany w bazie za pomocą jego klucza głównego. Typ T obiektu pozwala warstwie JPA rozpoznać, do której tabeli należy skierować zapytanie. Tak utworzony obiekt trwały jest zwracany do klienta. | |
tworzy obiekt Query na podstawie zapytania JPQL (Java Persistence Query Language). Zapytanie JPQL jest analogiczne do zapytania SQL, z tą różnicą, że wysyłane są zapytania dotyczące obiektów, a nie tabel. | |
metoda analogiczna do poprzedniej, z tą różnicą, że queryText jest poleceniem SQL, a nie JPQL. | |
metoda identyczna jak w przypadku createQuery, z tą różnicą, że polecenie JPQL queryText zostało została przeniesiona do pliku konfiguracyjnego i powiązana z nazwą. To właśnie ta nazwa stanowi parametr metody. |
Obiekt EntityManager ma cykl życia, który niekoniecznie pokrywa się z cyklem życia aplikacji. Ma on swój początek i koniec. W związku z tym klient JPA może kolejno współpracować z różnymi obiektami EntityManager. Kontekst trwałości powiązany z obiektem EntityManager ma taki sam cykl życia jak on sam. Są one ze sobą nierozerwalnie powiązane. Gdy obiekt EntityManager zostanie zamknięty, jego kontekst trwałości jest w razie potrzeby synchronizowany z bazą danych, a następnie przestaje istnieć. Aby ponownie uzyskać kontekst trwałości, należy utworzyć nowy obiekt EntityManager.
Klient JPA może utworzyć obiekt EntityManager, a tym samym kontekst trwałości, za pomocą następującej instrukcji:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
- javax.persistence.Persistence to klasa statyczna umożliwiająca uzyskanie fabryki obiektów EntityManager. Fabryka ta jest powiązana z konkretną jednostką trwałości. Przypomnijmy, że plik konfiguracyjny [META-INF/persistence.xml] pozwala definiować jednostki trwałości, a każda z nich ma swoją nazwę:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
W powyższym przykładzie jednostka trwałości nosi nazwę jpa. Towarzyszy jej cała specyficzna dla niej konfiguracja, w szczególności obiekt SGBD, z którym współpracuje. Instrukcja [Persistence.createEntityManagerFactory("jpa")] tworzy fabrykę obiektów typu EntityManagerFactory, zdolną do dostarczania obiektów EntityManager przeznaczonych do zarządzania kontekstami trwałości powiązanymi z jednostką trwałości o nazwie jpa. Pobranie obiektu EntityManager, a tym samym kontekstu trwałości, odbywa się na podstawie obiektu EntityManagerFactory w następujący sposób:
Następujące metody interfejsu [EntityManager] umożliwiają zarządzanie cyklem życia kontekstu trwałości:
kontekst trwałości zostaje zamknięty. Wymusza synchronizację kontekstu trwałości z bazą danych:
| |
kontekst trwałości został opróżniony ze wszystkich obiektów, ale nie został zamknięty. | |
kontekst trwałości jest synchronizowany z bazą danych w sposób opisany dla close() |
Klient JPA może wymusić synchronizację kontekstu trwałości z bazą danych za pomocą metody [EntityManager]. Poprzednia metoda to flush. Synchronizacja może być jawna lub niejawna. W pierwszym przypadku to klient musi wykonać operacje flush, gdy chce przeprowadzić synchronizację; w przeciwnym razie synchronizacja odbywa się w określonych momentach, które zostaną poniżej sprecyzowane. Tryb synchronizacji jest zarządzany przez następujące metody interfejsu [EntityManager]:
Dla metody flushmode istnieją dwie możliwe wartości: FlushModeType.AUTO (domyślnie): synchronizacja odbywa się przed każdym zapytaniem SELECT kierowanym do bazy danych. FlushModeType.COMMIT: synchronizacja odbywa się dopiero po zakończeniu transakcji w bazie danych. | |
zwraca aktualny tryb synchronizacji |
Podsumujmy. W trybie FlushModeType.AUTO, który jest trybem domyślnym, kontekst trwałości będzie synchronizowany z bazą danych w następujących momentach:
- przed każdą operacją SELECT w bazie danych
- po zakończeniu transakcji w bazie
- po operacji flush lub close na kontekście trwałości
W trybie FlushModeType.COMMIT sytuacja wygląda tak samo, z wyjątkiem operacji 1, która nie ma miejsca. Normalnym trybem interakcji z warstwą JPA jest tryb transakcyjny. Klient wykonuje różne operacje na kontekście trwałości w ramach jednej transakcji. W tym przypadku momenty synchronizacji kontekstu trwałości z bazą danych to przypadki 1 i 2 opisane powyżej w trybie AUTO oraz wyłącznie przypadek 2 w trybie COMMIT.
Na koniec omówmy interfejs Query o numerze API, który umożliwia wysyłanie poleceń JPQL do kontekstu trwałości lub poleceń SQL bezpośrednio do bazy danych w celu pobrania danych. Interfejs Query wygląda następująco:
![]() |
Będziemy korzystać z metod od 1 do 4 wymienionych powyżej:
- 1 – metoda getResultList wywołuje metodę SELECT, która zwraca kilka obiektów. Obiekty te zostaną pobrane do obiektu List. Obiekt ten jest interfejsem. Interfejs ten udostępnia obiekt Iterator, który umożliwia przeglądanie elementów listy L w następującej postaci:
Iterator iterator = L.iterator();
while (iterator.hasNext()) {
// wykorzystanie obiektu iterator.next(), który reprezentuje bieżący element listy
...
}
Listę L można również wykorzystać za pomocą obiektu for:
for (Object o : L) {
// wykorzystać obiekt o
}
- 2 – metoda getSingleResult wykonuje polecenie JPQL / SQL / SELECT, które zwraca pojedynczy obiekt.
- 3 – metoda executeUpdate wykonuje polecenie SQL typu „update” lub „delete” i zwraca liczbę wierszy, których dotyczy operacja.
- 4 – metoda setParameter(String, Object) pozwala przypisać wartość do parametru o nazwie w skonfigurowanym poleceniu JPQL
- 5 – metoda setParameter(int, Object), jednak parametr nie jest określany przez nazwę, lecz przez pozycję w poleceniu JPQL.
2.1.10. Pierwszy klient JPA
Wróćmy do projektu z perspektywy języka Java:
![]() |
Wiemy już prawie wszystko o tym projekcie, z wyjątkiem zawartości folderu [src/tests], który właśnie analizujemy. Folder zawiera dwa programy testujące warstwę JPA:
- [InitDB.java] to program, który wstawia kilka wierszy do tabeli [jpa01_personne] w bazie danych. Jego kod dostarczy nam pierwszych elementów warstwy JPA.
- [Main.java] to program, który wykonuje operacje CRUD na tabeli [jpa01_personne]. Analiza jego kodu pozwoli nam zapoznać się z podstawowymi pojęciami dotyczącymi kontekstu trwałości oraz cyklu życia obiektów w tym kontekście.
2.1.10.1. Kod
Kod programu [InitDB.java] wygląda następująco:
package tests;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import entites.Personne;
public class InitDB {
// stałe
private final static String TABLE_NAME = "jpa01_personne";
public static void main(String[] args) throws ParseException {
// Jednostka trwałości
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
// pobierz EntityManagerFactory z jednostki trwałości
EntityManager em = emf.createEntityManager();
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usunięcie elementów z tabeli osób
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// utworzenie dwóch osób
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// trwałość osób
em.persist(p1);
em.persist(p2);
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// koniec transakcji
tx.commit();
// koniec EntityManager
em.close();
// koniec EntityManagerFactory
emf.close();
// log
System.out.println("terminé ...");
}
}
Kod ten należy interpretować w świetle wyjaśnień zawartych w paragrafie 2.1.9.
- wiersz 19: żądany jest obiekt EntityManagerFactory typu emf dla jednostki trwałości jpa (zdefiniowanej w persistence.xml). Operacja ta jest zazwyczaj wykonywana tylko raz w całym cyklu życia aplikacji.
- wiersz 21: żądany jest obiekt EntityManager typu em w celu zarządzania kontekstem trwałości.
- wiersz 23: żądany jest obiekt Transaction do zarządzania transakcją. Należy tutaj przypomnieć, że operacje na kontekście trwałości są wykonywane w ramach transakcji. Zobaczymy, że nie jest to obowiązkowe, ale w takim przypadku mogą pojawić się problemy. Jeśli aplikacja działa w kontenerze EJB3, wówczas operacje na kontekście trwałości zawsze odbywają się w ramach transakcji.
- wiersz 24: rozpoczęcie transakcji
- wiersz 26: wykonuje polecenie SQL delete na tabeli „jpa01_personne” (nativeQuery). Robimy to, aby opróżnić tabelę z całej zawartości i w ten sposób lepiej zobaczyć wynik działania aplikacji [InitDB]
- wiersze 28–29: tworzone są dwa obiekty Personne o nazwach p1 i p2. Są to zwykłe obiekty i na razie nie mają one nic wspólnego z kontekstem trwałości. W odniesieniu do kontekstu trwałości Hibernate określa te obiekty jako przejściowe (transient), aby odróżnić je od obiektów trwałych (persistent), które są zarządzane przez kontekst trwałości. Będziemy raczej mówić o obiektach niepersystentnych (termin niepochodzący z języka francuskiego), aby zaznaczyć, że nie są one jeszcze zarządzane przez kontekst trwałości, oraz o obiektach trwałych w odniesieniu do tych, które są przez niego zarządzane. Znajdziemy trzecią kategorię obiektów – obiekty odłączone (detached), które wcześniej były obiektami trwałymi, ale których kontekst trwałości został zamknięty. Klient może posiadać odwołania do takich obiektów, co wyjaśnia, dlaczego nie są one koniecznie niszczone po zamknięciu kontekstu trwałości. Mówi się wówczas, że znajdują się one w stanie odłączonym. Operacja [EntityManager].merge pozwala na ponowne przyłączenie ich do nowo utworzonego kontekstu trwałości.
- wiersze 31–32: osoby p1 i p2 są włączane do kontekstu trwałości za pomocą operacji [EntityManager].persist. Stają się wówczas obiektami trwałymi.
- wiersze 35–37: wykonywane jest polecenie JPQL „select p from Personne p order by p.nom asc”. Personne nie jest tabelą (nazywa się ona jpa01_personne), lecz obiektem @Entity powiązanym z tabelą. Mamy tu do czynienia z zapytaniem JPQL (Java Persistence Query Language) w kontekście trwałości, a nie z poleceniem SQL w bazie danych. Niemniej jednak, poza obiektem Personne, który zastąpił tabelę jpa01_personne, składnia jest identyczna. Pętla for przegląda listę (osób) wynikową zapytania select, aby wyświetlić każdy jej element na konsoli. Chcemy tutaj sprawdzić, czy elementy umieszczone w kontekście trwałości w wierszach 31–32 rzeczywiście znajdują się w tabeli. W sposób przezroczysty nastąpi synchronizacja kontekstu trwałości z bazą danych. W rzeczywistości zostanie wysłane zapytanie select, a wspomniano, że jest to jeden z przypadków, w których odbywa się synchronizacja. To właśnie w tym momencie, w tle, JPA / Hibernate wygeneruje dwa polecenia SQL i insert, które wstawią te dwie osoby do tabeli jpa01_personne. Operacja persist tego nie zrobiła. Operacja ta dodaje obiekty do kontekstu trwałości bez wywierania wpływu na bazę danych. Rzeczywiste zmiany zachodzą podczas synchronizacji, w tym przypadku tuż przed operacją select w bazie danych.
- wiersz 39: kończymy transakcję rozpoczętą w wierszu 24. Ponownie nastąpi synchronizacja. Nic się tutaj nie wydarzy, ponieważ kontekst trwałości nie uległ zmianie od ostatniej synchronizacji.
- wiersz 41: zamykamy kontekst trwałości.
- wiersz 43: zamykamy fabrykę EntityManager.
2.1.10.2. ne wykonanie kodu
- uruchom SGBD MySQL5
- w razie potrzeby umieścić plik conf/mysql5/persistence.xml w katalogu META-INF/persistence.xml
- uruchom aplikację [InitDB]
Otrzymujemy następujące wyniki:
![]() |
- w [1]: wyświetlanie konsoli w perspektywie Java. Otrzymujemy oczekiwany wynik.
- w [2]: sprawdzamy zawartość tabeli [jpa01_personne] za pomocą perspektywy SQL Explorer, zgodnie z wyjaśnieniem w paragrafie 2.1.8. Można zauważyć dwie rzeczy:
- klucz podstawowy ID został wygenerowany automatycznie
- to samo dotyczy numeru wersji. Widzimy, że pierwsza wersja ma numer 0..
Mamy tu pierwsze elementy struktury JPA. Udało nam się wstawić dane do tabeli. Będziemy opierać się na tych osiągnięciach, aby napisać drugi test, ale najpierw omówmy logi.
2.1.11. Wdrażanie logów Hibernate
Można sprawdzić, jakie polecenia SQL zostały wysłane do bazy danych przez warstwę JPA / Hibernate. Warto je poznać, aby sprawdzić, czy warstwa JPA jest równie wydajna, jak programista, który sam napisałby polecenia SQL.
Dzięki JPA / Hibernate logi SQL można sprawdzić w pliku [persistence.xml]:
<!-- Klasy trwałe -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- logowanie JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
- wiersze 4–6: logi SQL nie były dotychczas włączone. Włączamy je teraz, usuwając znaczniki komentarzy z wierszy 3 i 7.
Ponownie uruchamiamy aplikację [InitDB]. Wyświetlane informacje w konsoli wyglądają wówczas następująco:
- wiersze 2–4: polecenie SQL delete wynikające z instrukcji:
// usuwanie elementów z tabeli osób
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
- wiersze 5–18: polecenia SQL insert wynikające z instrukcji:
// trwałość osób
em.persist(p1);
em.persist(p2);
- wiersze 21–32: polecenie SQL select wynikające z instrukcji:
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList())
Jeśli wyświetlimy pośrednie komunikaty konsoli, zauważymy, że zapis logów SQL dla instrukcji I w kodzie Java następuje w momencie wykonania tej instrukcji. Nie oznacza to jednak, że wyświetlone polecenie SQL jest w tym momencie wykonywane w bazie danych. W rzeczywistości jest ona buforowana do wykonania podczas następnej synchronizacji kontekstu trwałości z bazą danych.
Inne logi można uzyskać za pośrednictwem pliku [src/log4j.properties]:
![]() |
- w pliku [1] plik [log4j.properties] jest przetwarzany przez archiwum [log4j-1.2.13.jar] [2] za pomocą narzędzia o nazwie LOG4j (Logs for Java), dostępnego pod adresem [http://logging.apache.org/log4j/docs/index.html]. Umieszczony w folderze [src] projektu Eclipse, wiemy, że plik [log4j.properties] zostanie automatycznie skopiowany do folderu [bin] projektu [3]. Po wykonaniu tej czynności plik znajduje się teraz w folderze classpath projektu i właśnie stamtąd archiwum [2] go pobierze.
Plik [log4j.properties] pozwala nam monitorować niektóre logi Hibernate. Podczas poprzednich uruchomień jego zawartość wyglądała następująco:
# Kierowanie komunikatów dziennika do stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
# Opcja rejestratora głównego
log4j.rootLogger=ERROR, stdout
# Opcje logowania Hibernate (INFO wyświetla wyłącznie komunikaty startowe)
#log4j.logger.org.hibernate=INFO
# Rejestruj argumenty wykonawcze parametrów wiązania JDBC
#log4j.logger.org.hibernate.type=DEBUG
Nie będę się zbytnio rozpisywał na temat tej konfiguracji, ponieważ nigdy nie poświęciłem czasu na dokładne zapoznanie się z plikiem LOG4j.
- Wiersze 1–8 występują we wszystkich plikach log4j.properties, z którymi miałem do czynienia
- wiersze 10–14 występują w plikach log4j.properties z przykładów Hibernate.
- wiersz 11: kontroluje ogólne logi Hibernate. Ponieważ wiersz ten jest zakomentowany, logi te są tutaj wyłączone. Dostępnych jest kilka poziomów logowania: INFO (ogólne informacje o działaniu Hibernate), WARN (Hibernate ostrzega o potencjalnym problemie), DEBUG (szczegółowe logi). Poziom INFO jest najmniej szczegółowy, a tryb DEBUG najbardziej szczegółowy. Włączenie wiersza 11 pozwala dowiedzieć się, co robi Hibernate, zwłaszcza podczas uruchamiania aplikacji. Często jest to interesujące.
- Wiersz 12, jeśli jest aktywny, pozwala poznać argumenty faktycznie używane podczas wykonywania skonfigurowanych zapytań SQL.
Zacznijmy od usunięcia komentarza z linii 14
# Rejestrowanie argumentów środowiskowych parametrów wiązania JDBC
log4j.logger.org.hibernate.type=DEBUG
i ponownie uruchommy [InitDB]. Nowe logi wynikające z tej zmiany są następujące (fragment):
- wiersze 8–10 to nowe wpisy w dzienniku wynikające z aktywacji wiersza 14 w [log4j.properties]. Wskazują one 5 wartości przypisanych do parametrów formalnych ? w zapytaniu parametrycznym z wierszy 2–7. Widać zatem, że kolumna VERSION otrzyma wartość 0 (wiersz 8).
Teraz aktywujmy wiersz 11 pliku [log4j.properties]:
# Opcje logowania Hibernate (INFO wyświetla tylko komunikaty startowe)
log4j.logger.org.hibernate=INFO
i ponownie uruchommy [InitDB]:
Analiza tych logów dostarcza wielu interesujących informacji:
- wiersz 7: Hibernate podaje nazwę klasy @Entity, którą znalazł
- wiersz 8: wskazuje, że klasa [Personne] zostanie powiązana z tabelą [jpa01_personne]
- wiersz 9: wskazuje pulę połączeń C3P0, która zostanie wykorzystana, nazwę sterownika JDBC oraz adres URL bazy danych, którą należy zarządzać
- wiersz 10: podaje inne cechy połączenia JDBC: właściciela, typ zatwierdzenia itp.
- wiersz 14: dialekt używany do komunikacji z SGBD
- wiersz 15: typ używanej transakcji. JDBCTransactionFactory oznacza, że aplikacja sama zarządza swoimi transakcjami. Nie działa ona w kontenerze EJB3, który zapewniałby własną obsługę transakcji.
- Kolejne wiersze dotyczą opcji konfiguracyjnych Hibernate, z którymi się nie spotkaliśmy. Zainteresowanych czytelników zachęcamy do zapoznania się z dokumentacją Hibernate.
- wiersz 37: polecenia SQL będą wyświetlane na konsoli. Zostało to zdefiniowane w pliku [persistence.xml]:
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="use_sql_comments" value="true" />
- wiersze 43–45: schemat bazy danych jest eksportowany do plików SGBD i c.a.d. Baza danych jest opróżniana, a następnie odtwarzana. Mechanizm ten wynika z konfiguracji wprowadzonej w pliku [persistence.xml] (wiersz 4 poniżej):
...
<property name="hibernate.connection.password" value="jpa" />
<!-- automatyczne tworzenie schematu -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Dialekt -->
...
Gdy aplikacja „zawiesza się” z powodu niezrozumiałego wyjątku Hibernate, należy najpierw włączyć logi Hibernate w trybie DEBUG w pliku [log4j.properties], aby uzyskać jaśniejszy obraz sytuacji:
# Opcja rejestratora głównego
log4j.rootLogger=ERROR, stdout
# Opcje logowania Hibernate (INFO wyświetla tylko komunikaty startowe)
log4j.logger.org.hibernate=DEBUG
W dalszej części tego dokumentu logi są domyślnie wyłączone, aby zapewnić bardziej czytelny wygląd konsoli.
2.1.12. Poznaj język JPQL / HQL za pomocą konsoli Hibernate
Uwaga: Ta sekcja wymaga wtyczki Hibernate Tools (punkt 5.2.5).
W kodzie aplikacji [InitDB] wykorzystaliśmy zapytanie JPQL. JPQL (Java Persistence Query Language) to język służący do wysyłania zapytań do kontekstu trwałości. Napotkane zapytanie miało następującą postać:
Wybierała ona wszystkie elementy tabeli powiązanej z @Entity [Personne] i zwracała je w porządku rosnącym według nazwy. W powyższym zapytaniu p.nom jest polem „nazwa” instancji p klasy [Personne]. Zapytanie JPQL działa zatem na obiektach @Entity w kontekście trwałości, a nie bezpośrednio na tabelach bazy danych. Warstwa JPA przetłumaczy z kolei to zapytanie JPQL na zapytanie SQL odpowiednie dla klasy SGBD, z którą współpracuje. Tak więc w przypadku implementacji JPA / Hibernate połączonej z SGBD i MySQL5, poprzednie zapytanie JPQL jest przekształcane w następujące zapytanie SQL:
select
personne0_.ID as ID0_,
personne0_.VERSION as VERSION0_,
personne0_.NOM as NOM0_,
personne0_.PRENOM as PRENOM0_,
personne0_.DATENAISSANCE as DATENAIS5_0_,
personne0_.MARIE as MARIE0_,
personne0_.NBENFANTS as NBENFANTS0_
from
jpa01_personne personne0_
order by
personne0_.NOM asc
Warstwa JPA wykorzystała konfigurację obiektu @Entity [Personne] do wygenerowania prawidłowego zamówienia SQL. W tym przypadku zastosowano most obiektowo-relacyjny.
Wtyczka [Hibernate Tools] (punkt 5.2.5) udostępnia narzędzie o nazwie „Hibernate Console”, które umożliwia
- wygenerować polecenia JPQL lub ich nadzbioru HQL (Hibernate Query Language) w kontekście trwałości
- uzyskania wyników
- sprawdzenia, jaki odpowiednik SQL został wykonany w bazie
Konsola Hibernate jest niezwykle cennym narzędziem do nauki języka JPQL oraz zapoznania się z mostem JPQL / SQL. Wiadomo, że JPA czerpał w dużym stopniu inspirację z narzędzi takich jak Hibernate czy Toplink. JPQL jest bardzo zbliżony do języka HQL stosowanego w Hibernate, ale nie obejmuje wszystkich jego funkcji. W konsoli Hibernate można wydawać polecenia HQL, które będą normalnie wykonywane w konsoli, ale nie stanowią części języka JPQL i dlatego nie można ich używać w kliencie JPA. W takich przypadkach będziemy o tym informować.
Utwórzmy konsolę Hibernate dla naszego bieżącego projektu Eclipse:
![]() |
- [1]: przechodzimy do perspektywy [Hibernate Console] (Window / Open Perspective / Other)
- [2]: tworzymy nową konfigurację w oknie [Hibernate Configuration]
- za pomocą przycisku [4] wybieramy projekt Java, dla którego tworzona jest konfiguracja Hibernate. Jego nazwa wyświetla się w [3].
- W polu [5] nadajemy tej konfiguracji wybraną przez nas nazwę. W tym przypadku wykorzystaliśmy nazwę [3].
- w [6] wskazujemy, że korzystamy z konfiguracji JPA, aby narzędzie wiedziało, że ma przetworzyć plik [META-INF/persistence.xml]
- w pliku [7]: wskazujemy, że w tym pliku [META-INF/persistence.xml] należy użyć jednostki trwałości o nazwie jpa.
- W pliku [8] zatwierdzamy konfigurację.
Następnie należy uruchomić plik SGBD. W tym przypadku chodzi o plik MySQL5.
![]() |
- w [1]: utworzona konfiguracja ma drzewo z trzema gałęziami
- w [2]: gałąź [Configuration] zawiera listę obiektów, których konsola użyła do skonfigurowania się: w tym przypadku @Entity Personne.
- w [3]: Session Factory to pojęcie z Hibernate zbliżone do EntityManager z JPA. Realizuje ona pomost między obiektami a relacjami dzięki obiektom z gałęzi [Configuration]. W [3] przedstawiono obiekty kontekstu trwałości, w tym przypadku ponownie @Entity Personne.
- W [4]: baza danych, do której dostęp uzyskuje się za pomocą konfiguracji znajdującej się w [persistence.xml]. Znajduje się tam tabela [jpa01_personne].
![]() |
- w pliku [1] tworzy się edytor HQL
- w edytorze HQL,
- w [2] wybieramy konfigurację Hibernate, której chcemy użyć, jeśli jest ich kilka
- w [3] wpisujemy polecenie JPQL, które chcemy wykonać
- w oknie [4] uruchamia się je
- w [5] otrzymujemy wyniki zapytania w oknie [Hibernate Query Result]. Mogą tu wystąpić dwie trudności:
- nie otrzymujemy żadnych wyników (żadnego wiersza). Konsola Hibernate wykorzystała zawartość pliku [persistence.xml] do nawiązania połączenia z plikiem SGBD. Jednak ta konfiguracja zawiera właściwość nakazującą opróżnienie bazy danych:
<property name="hibernate.hbm2ddl.auto" value="create" />
Należy zatem ponownie uruchomić aplikację [InitDB] przed ponownym wykonaniem powyższego polecenia JPQL.
- (ciąg dalszy)
- Nie wyświetla się okno [Hibernate Query Result]. Wywołujemy je za pomocą polecenia [Window / Show View / ...]
Okno [Hibernate Dynamic SQL preview] ([1] poniżej) pozwala wyświetlić zapytanie SQL, które zostanie uruchomione w celu wykonania polecenia JPQL, które właśnie wpisujemy. Gdy tylko składnia polecenia JPQL jest poprawna, w tym oknie pojawia się odpowiadające mu polecenie SQL:
![]() |
- w [2] usuwamy poprzednie polecenie HQL
- w [3] wykonuje się nowe polecenie
- w [4], wynik
- w [5], polecenie SQL, które zostało wykonane na podstawie
Edytor HQL oferuje pomoc w pisaniu poleceń HQL:
![]() |
- w [1]: gdy edytor rozpozna, że p jest obiektem Personne, może podczas wpisywania proponować pola z p.
- w [2]: nieprawidłowe polecenie HQL. Należy wpisać where p.marie=true.
- w [3]: błąd jest sygnalizowany w oknie [SQL Preview]
Zachęcamy czytelnika do wydania kolejnych poleceń HQL / JPQL w bazie.
2.1.13. Drugi klient JPA
Wróćmy do projektu z perspektywy języka Java:
![]() |
- [InitDB.java] to program, który wstawiał kilka wierszy do tabeli [jpa01_personne] w bazie danych. Analiza jego kodu pozwoliła nam zdobyć pierwsze elementy API i JPA.
- [Main.java] to program, który wykonuje operacje CRUD na tabeli [jpa01_personne]. Analiza jego kodu pozwoli nam powrócić do podstawowych pojęć związanych z kontekstem trwałości oraz cyklem życia obiektów w tym kontekście.
2.1.13.1. Struktura kodu
[Main.java] przeprowadza serię testów, z których każdy ma na celu pokazanie konkretnego aspektu programu JPA:
![]() |
Metoda [main]
- wywołuje kolejno metody od test1 do test11. Kod każdej z tych metod przedstawimy osobno.
- Korzysta ona ponadto z prywatnych metod pomocniczych: clean, dump, log, getEntityManager, getNewEntityManager.
Przedstawiamy metodę main oraz tak zwane metody pomocnicze:
package tests;
...
import entites.Personne;
@SuppressWarnings("unchecked")
public class Main {
// stałe
private final static String TABLE_NAME = "jpa01_personne";
// Kontekst trwałości
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// obiekty współdzielone
private static Personne p1, p2, newp1;
public static void main(String[] args) throws Exception {
// czyszczenie bazy danych
log("clean");clean();
// zrzut tabeli
dump();
// test1
log("test1");test1();
...
// test11
log("test11");test11();
// koniec kontekstu trwałości
if (em.isOpen())
em.close();
// zamknięcie EntityManagerFactory
emf.close();
}
// pobierz bieżący EntityManager
private static EntityManager getEntityManager() {
if (em == null || !em.isOpen()) {
em = emf.createEntityManager();
}
return em;
}
// pobierz nowy EntityManager
private static EntityManager getNewEntityManager() {
if (em != null && em.isOpen()) {
em.close();
}
em = emf.createEntityManager();
return em;
}
// wyświetlenie zawartości tabeli
private static void dump() {
// bieżący kontekst trwałości
EntityManager em = getEntityManager();
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// koniec transakcji
tx.commit();
}
// reset BD
private static void clean() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuń elementy z tabeli PERSONNES
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// koniec transakcji
tx.commit();
}
// logi
private static void log(String message) {
System.out.println("main : ----------- " + message);
}
// tworzenie obiektów
public static void test1() throws ParseException {
...
}
// modyfikacja obiektu kontekstowego
public static void test2() {
...
}
// pobieranie obiektów
public static void test3() {
...
}
// usunięcie obiektu należącego do kontekstu trwałości
public static void test4() {
....
}
// odłączanie, ponowne dołączanie i modyfikowanie
public static void test5() {
...
}
// usunięcie obiektu niebędącego częścią kontekstu trwałości
public static void test6() {
...
}
// modyfikowanie obiektu niebędącego częścią kontekstu trwałości
public static void test7() {
...
}
// ponowne przypisanie obiektu do kontekstu trwałości
public static void test8() {
...
}
// zapytanie SELECT powoduje synchronizację
// bazy danych z kontekstem trwałości
public static void test9() {
....
}
// kontrola wersji (blokowanie optymistyczne)
public static void test10() {
...
}
// wycofanie transakcji
public static void test11() throws ParseException {
...
}
}
- wiersz 13: obiekt EntityManagerFactory typu emf utworzony na podstawie jednostki trwałości jpa zdefiniowanej w [persistence.xml]. Pozwoli nam on na tworzenie w trakcie działania aplikacji różnych kontekstów trwałości.
- wiersz 14: kontekst trwałości EntityManager nie został jeszcze zainicjowany
- wiersz 17: trzy obiekty [Personne] współdzielone przez testy
- wiersz 21: tabela jpa01_personne jest opróżniana, a następnie wyświetlana w wierszu 24, aby upewnić się, że zaczynamy od pustej tabeli.
- wiersze 27–31: sekwencja testów
- wiersze 34–35: zamknięcie kontekstu trwałości, jeśli był otwarty.
- wiersz 38: zamknięcie obiektu EntityManagerFactory emf.
- wiersze 42–47: metoda [getEntityManager] ustawia obiekt EntityManager (czyli kontekst trwałości) jako bieżący lub tworzy nowy, jeśli nie istnieje (wiersze 43–44).
- wiersze 50–56: metoda [getNewEntityManager] tworzy nowy kontekst trwałości. Jeśli wcześniej istniał, zostaje zamknięty (wiersze 51–52)
- wiersze 59–72: metoda [dump] wyświetla zawartość tabeli [jpa01_personne]. Ten kod pojawił się już wcześniej w metodzie [InitDB].
- wiersze 75–85: metoda [clean] opróżnia tabelę [jpa01_personne]. Kod ten pojawił się już wcześniej w metodzie [InitDB].
- wiersze 88–90: metoda [log] wyświetla na konsoli komunikat przekazany jej jako parametr, aby został zauważony.
Możemy teraz przejść do analizy testów.
2.1.13.2. Test 1
Kod testu 1 wygląda następująco:
// tworzenie obiektów
public static void test1() throws ParseException {
// kontekst trwałości
EntityManager em = getEntityManager();
// tworzenie osób
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// trwałość osób
em.persist(p1);
em.persist(p2);
// zakończenie transakcji
tx.commit();
// wyświetlanie tabeli
dump();
}
Kod ten pojawił się już wcześniej w [InitDB]: tworzy on dwie osoby i umieszcza je w kontekście trwałości.
- wiersz 4: wywoływany jest bieżący kontekst trwałości
- wiersze 6–7: tworzy się dwie osoby
- wiersze 9–15: obie osoby są umieszczane w kontekście trwałości w ramach transakcji.
- wiersz 15: w wyniku zatwierdzenia transakcji następuje synchronizacja kontekstu trwałości z bazą danych. Obie osoby zostaną dodane do tabeli [jpa01_personne].
- wiersz 17: wyświetla się tabela
Wynik wyświetlony w konsoli dla tego pierwszego testu wygląda następująco:
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
2.1.13.3. Test 2
Kod testu 2 wygląda następująco:
// modyfikacja obiektu kontekstu
public static void test2() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększanie liczby dzieci p1
p1.setNbenfants(p1.getNbenfants() + 1);
// zmiana stanu cywilnego
p1.setMarie(false);
// obiekt p1 jest automatycznie zapisywany (sprawdzanie zmian)
// podczas następnej synchronizacji (commit lub select)
// koniec transakcji
tx.commit();
// wyświetla się nowa tabela
dump();
}
- Celem testu 2 jest zmiana obiektu w kontekście trwałości, a następnie wyświetlenie zawartości tabeli w celu sprawdzenia, czy zmiana została wprowadzona
- wiersz 4: pobieramy bieżący kontekst trwałości
- wiersze 6–7: operacje będą wykonywane w ramach transakcji
- wiersze 9, 11: zmieniona zostaje liczba dzieci osoby p1 oraz jej stan cywilny
- wiersz 15: zakończenie transakcji, a więc synchronizacja kontekstu trwałości z bazą danych
- wiersz 17: wyświetlenie tabeli
Wynik wyświetlany w konsoli dla testu 2 wygląda następująco:
- wiersz 4: osoba p1 przed modyfikacją
- wiersz 8: osoba p1 po modyfikacji. Należy zauważyć, że jej numer wersji zmienił się na 1. Numer ten zwiększa się o 1 przy każdej aktualizacji wiersza.
2.1.13.4. Test 3
Kod testu 3 wygląda następująco:
// zapytanie o obiekty
public static void test3() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// żądanie osoby p1
Personne p1b = em.find(Personne.class, p1.getId());
// ponieważ p1 znajduje się już w kontekście trwałości, nie nastąpił dostęp do bazy danych
// p1b i p1 to te same odwołania
System.out.format("p1==p1b ? %s%n", p1 == p1b);
// żądanie obiektu, który nie istnieje, powoduje, że wskaźnik 1 przyjmuje wartość null
Personne px = em.find(Personne.class, -4);
System.out.format("px==null ? %s%n", px == null);
// koniec transakcji
tx.commit();
}
- Test 3 dotyczy metody [EntityManager.find], która pozwala pobrać obiekt z bazy danych i umieścić go w kontekście trwałości. Nie będziemy już wyjaśniać transakcji, która ma miejsce we wszystkich testach, chyba że jest ona wykorzystywana w nietypowy sposób.
- wiersz 9: zwracamy się do kontekstu trwałości z zapytaniem o osobę, która ma ten sam klucz pierwotny co osoba p1. Istnieją dwa przypadki:
- p1 znajduje się już w kontekście trwałości. Tak jest w tym przypadku. Wówczas nie następuje żaden dostęp do bazy danych. Metoda find zwraca jedynie odwołanie do obiektu trwałego.
- p1 nie znajduje się w kontekście trwałości. Wówczas następuje dostęp do bazy danych za pomocą podanego klucza głównego. Pobierany wiersz jest umieszczany w kontekście trwałości, a metoda find zwraca odwołanie do tego nowego obiektu trwałego.
- wiersz 12: sprawdzane jest, czy find zwróciło już odwołanie do obiektu p1 znajdującego się w kontekście
- wiersz 14: żądany jest obiekt, który nie istnieje ani w kontekście trwałości, ani w bazie danych. Metoda find zwraca wówczas wskaźnik null. Punkt ten jest sprawdzany w wierszu 15.
Wynik wyświetlany w konsoli dla testu 3 jest następujący:
2.1.13.5. Test 4
Kod testu 4 wygląda następująco:
// usunięcie obiektu należącego do kontekstu trwałości
public static void test4() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuwamy obiekt trwały p2
em.remove(p2);
// koniec transakcji
tx.commit();
// wyświetlanie nowej tabeli
dump();
}
- Test 4 dotyczy metody [EntityManager.remove], która pozwala usunąć element z kontekstu trwałości, a tym samym z bazy danych.
- wiersz 9: osoba p2 zostaje usunięta z kontekstu trwałości
- wiersz 11: synchronizacja kontekstu z bazą danych
- wiersz 13: wyświetlenie tabeli. Zazwyczaj osoba p2 nie powinna już tam być.
Wynik testu 4 wyświetlany na konsoli wygląda następująco:
- wiersz 3: osoba p2 w test1
- wiersze 12–14: osoba ta już nie istnieje po zakończeniu operacji test4.
2.1.13.6. Test 5
Kod testu 5 wygląda następująco:
// odłączanie, ponowne dołączanie i modyfikacja
public static void test5() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// p1 odłączony
Personne oldp1=p1;
// ponowne przypisanie p1 do nowego kontekstu
p1 = em.find(Personne.class, p1.getId());
// weryfikacja
System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);
// koniec transakcji
tx.commit();
// zwiększono liczbę elementów potomnych p1
p1.setNbenfants(p1.getNbenfants() + 1);
// wyświetlanie nowej tabeli
dump();
}
- Test 5 dotyczy cyklu życia obiektów trwałych w kilku kolejnych kontekstach trwałości. Do tej pory w różnych testach zawsze korzystaliśmy z tego samego kontekstu trwałości.
- Wiersz 4: żądany jest nowy kontekst trwałości. Metoda [getNewEntityManager] zamyka poprzedni i otwiera nowy. W rezultacie obiekty p1 i p2 przechowywane przez aplikację nie znajdują się już w stanie trwałym. Należały one do kontekstu, który został zamknięty. Mówi się, że znajdują się one w stanie odłączonym. Nie należą one do nowego kontekstu trwałości.
- wiersze 6–7: początek transakcji. Zostanie ona tutaj wykorzystana w nietypowy sposób.
- wiersz 9: zapisujemy adres obiektu p1, który jest teraz odłączony.
- wiersz 11: wysyłamy zapytanie do kontekstu trwałości o osobę p1 (podając klucz pierwotny p1). Ponieważ kontekst jest nowy, osoba p1 nie znajduje się w nim. Nastąpi zatem dostęp do bazy danych. Zwrócony obiekt zostanie umieszczony w nowym kontekście.
- wiersz 13: sprawdzamy, czy obiekt trwały p1 z kontekstu różni się od obiektu oldp1, który był poprzednim odłączonym obiektem p1.
- wiersz 15: transakcja została zakończona
- wiersz 17: modyfikujemy, poza transakcją, nowy obiekt trwały p1. Co się dzieje w tym przypadku? Chcemy to wiedzieć.
- wiersz 19: żądamy wyświetlenia tabeli. Przypominamy, że z powodu obiektu select wygenerowanego przez metodę dump automatycznie przeprowadzana jest synchronizacja kontekstu trwałości z bazą danych.
Wynik wyświetlony w konsoli dla testu 5 wygląda następująco:
- wiersz 5: metoda find rzeczywiście uzyskała dostęp do bazy danych, w przeciwnym razie oba wskaźniki byłyby równe
- wiersze 7 i 3: liczba elementów potomnych p1 rzeczywiście wzrosła o 1. Zmiana, dokonana poza transakcją, została zatem uwzględniona. Zależy to w rzeczywistości od użytej metody SGBD. W przypadku SGBD polecenie SQL jest zawsze wykonywane w ramach transakcji. Jeśli klient JPA sam nie rozpocznie jawnej transakcji, wówczas SGBD rozpocznie transakcję niejawną. Występują dwa typowe przypadki:
- 1 – każde pojedyncze polecenie SQL jest przedmiotem transakcji, otwieranej przed poleceniem i zamykanej po nim. Mówi się wówczas o trybie autocommit. Wszystko przebiega więc tak, jakby klient JPA przeprowadzał transakcje dla każdego polecenia SQL.
- 2 – zlecenie SGBD nie działa w trybie autocommit i rozpoczyna domyślną transakcję przy pierwszym zleceniu SQL, które klient JPA wysyła poza transakcją, a następnie pozwala klientowi ją zamknąć. Wszystkie polecenia SQL wysyłane przez klienta JPA stają się wówczas częścią tej domyślnej transakcji. Transakcja ta może zakończyć się w wyniku różnych zdarzeń: klient zamyka połączenie, rozpoczyna nową transakcję itp.
Mamy tu do czynienia z sytuacją zależną od konfiguracji SGBD. Mamy zatem do czynienia z kodem nieprzenośnym. Nieco dalej pokażemy kod bez transakcji i zobaczymy, że nie wszystkie SGBD zachowują się tak samo w stosunku do tego kodu. Uznamy zatem, że praca poza transakcjami jest błędem programistycznym.
- wiersz 7: należy zauważyć, że numer wersji zmienił się na 2.
2.1.13.7. Test 6
Kod testu 6 wygląda następująco:
// usunięcie obiektu niebędącego częścią kontekstu trwałości
public static void test6() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuwamy p1, który nie należy do nowego kontekstu
try {
em.remove(p1);
// koniec transakcji
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
// wycofuje się transakcję
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// wyświetlanie nowej tabeli
dump();
}
- Test 6 ma na celu usunięcie obiektu, który nie należy do kontekstu trwałości.
- wiersz 4: żądany jest nowy kontekst trwałości. Poprzedni zostaje zatem zamknięty, a obiekty, które zawierał, stają się odłączone. Dotyczy to obiektu p1 z poprzedniego testu 5.
- wiersze 6–7: rozpoczęcie transakcji.
- wiersz 10: usuwamy obiekt p1, który stał się obiektem niezwiązanym. Wiemy, że spowoduje to wyjątek, dlatego operację tę otoczyliśmy blokiem try/catch.
- wiersz 12: zatwierdzenie nie nastąpi.
- wiersze 16–21: transakcja musi zakończyć się kodem commit (wszystkie operacje w transakcji zostały zatwierdzone) lub rollback (wszystkie operacje w transakcji zostały cofnięte). Wystąpił wyjątek, więc wykonujemy rollback dla transakcji. Nie ma nic do cofnięcia, ponieważ jedyna operacja w transakcji zakończyła się niepowodzeniem, ale rollback kończy transakcję. Po raz pierwszy używamy operacji [EntityTransaction].rollback. Powinniśmy byli to zrobić już w pierwszych przykładach. Nie zrobiliśmy tego, aby zachować prostotę kodu. Czytelnik powinien jednak pamiętać, że w kodzie zawsze należy przewidzieć przypadek cofnięcia transakcji za pomocą rollback.
- wiersz 24: wyświetlamy tabelę. Zazwyczaj nie powinna ulec zmianie.
Wynik wyświetlany na konsoli w teście 6 wygląda następująco:
- wiersz 6: usunięcie p1 nie powiodło się. Komunikat o wyjątku wyjaśnia, że próbowano usunąć obiekt odłączony, a więc niebędący częścią kontekstu. Nie jest to możliwe.
- wiersz 8: osoba p1 nadal tam jest.
2.1.13.8. Test 7
Kod testowy 7 wygląda następująco:
// modyfikacja obiektu niebędącego częścią kontekstu trwałości
public static void test7() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększana jest liczba elementów potomnych p1, które nie należą do nowego kontekstu
p1.setNbenfants(p1.getNbenfants() + 1);
// koniec transakcji
tx.commit();
// wyświetlanie nowej tabeli – nie powinna ulec zmianie
dump();
}
- Test 7 ma na celu modyfikację obiektu, który nie należy do kontekstu trwałości, oraz sprawdzenie, jaki wpływ ma to na bazę danych. Można przypuszczać, że nie ma żadnego wpływu. Tak właśnie pokazują wyniki testu.
- wiersz 4: żądany jest nowy kontekst trwałości. Mamy zatem nowy kontekst, w którym nie ma żadnych obiektów trwałych.
- wiersze 6–7: rozpoczęcie transakcji.
- wiersz 9: modyfikujemy odłączony obiekt p1. Jest to operacja, która nie angażuje kontekstu trwałości em. Nie należy więc spodziewać się wyjątku ani niczego podobnego. Jest to podstawowa operacja na obiekcie POJO.
- wiersz 11: zatwierdzenie powoduje synchronizację kontekstu z bazą danych. Kontekst ten jest pusty. Baza danych nie ulega zatem zmianie.
- wiersz 24: wyświetlana jest tabela. Zazwyczaj nie powinna ulec zmianie.
Wynik wyświetlany w konsoli dla testu 7 wygląda następująco:
- wiersz 7: osoba p1 nie uległa zmianie w bazie danych. W kolejnym teście należy jednak pamiętać, że w pamięci liczba jej dzieci wynosi teraz 5.
2.1.13.9. Test 8
Kod testu 8 wygląda następująco:
// ponowne przypisanie obiektu do kontekstu trwałości
public static void test8() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// odłączony obiekt p1 jest ponownie przypisywany do nowego kontekstu
newp1 = em.merge(p1);
// teraz to newp1 stanowi część kontekstu, a nie p1
// koniec transakcji
tx.commit();
// wyświetlana jest nowa tabela – liczba elementów potomnych p1 musiała ulec zmianie
dump();
}
- Test 8 ponownie przypisuje do kontekstu trwałości odłączony obiekt.
- wiersz 4: żądany jest nowy kontekst trwałości. Mamy więc nowy kontekst bez obiektów trwałych w środku.
- wiersze 6–7: rozpoczęcie transakcji.
- wiersz 9: do kontekstu trwałości ponownie dołączamy odłączony obiekt p1. Operacja merge może obejmować kilka operacji:
- przypadek 1: w kontekście trwałości istnieje obiekt trwały ps1 o tym samym kluczu głównym co odłączony obiekt p1. Zawartość obiektu p1 jest kopiowana do obiektu ps1, a obiekt merge staje się odniesieniem do obiektu ps1.
- Przypadek 2: w kontekście trwałości nie istnieje obiekt trwały ps1 o tym samym kluczu głównym co odłączony obiekt p1. Następnie wysyłane jest zapytanie do bazy danych, aby sprawdzić, czy poszukiwany obiekt istnieje w bazie. Jeśli tak, obiekt ten jest przenoszony do kontekstu trwałości, staje się obiektem trwałym o numerze ps1 i powracamy do poprzedniego przypadku 1.
- przypadek 3: ani w kontekście trwałości, ani w bazie nie istnieje obiekt o tym samym kluczu podstawowym co obiekt odłączony p1. Tworzony jest wówczas nowy obiekt [Personne] (new), a następnie umieszczany w kontekście trwałości. Następnie powracamy do przypadku 1.
- W rezultacie: obiekt odłączony p1 pozostaje odłączony. Operacja merge zwraca odwołanie (w tym przypadku newp1) do obiektu trwałego ps1 pochodzącego z obiektu merge. Aplikacja kliencka musi teraz pracować z obiektem trwałym ps1, a nie z obiektem odłączonym p1.
- Należy zwrócić uwagę na różnicę między przypadkami 1 i 3 w odniesieniu do zlecenia SQL zaplanowanego dla obiektu merge: w przypadkach 1 i 2 jest to polecenie UPDATE, podczas gdy w przypadku 3 jest to polecenie INSERT.
- wiersz 12: operacja commit powoduje synchronizację kontekstu z bazą danych. Kontekst ten nie jest już pusty. Zawiera obiekt newp1. Obiekt ten zostanie zapisany w bazie danych.
- wiersz 24: wyświetlamy tabelę, aby to sprawdzić.
Wynik wyświetlany w konsoli dla testu 8 wygląda następująco:
- liczba elementów potomnych p1 wynosiła 4 w teście 6 (wiersz 4), następnie wzrosła do 5 w teście 7, ale nie została zapisana w bazie danych (wiersz 7). Po merge w bazie zapisano newp1: w wierszu 10 widoczne jest 5 elementów potomnych.
- wiersz 10: numer wersji newp1 wzrósł do 3.
2.1.13.10. Test 9
Kod testu 9 wygląda następująco:
// zapytanie SELECT powoduje synchronizację
// bazy danych z kontekstem trwałości
public static void test9() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększono liczbę dzieci newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// wyświetlanie osób – liczba dzieci newp1 musiała ulec zmianie
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// koniec transakcji
tx.commit();
}
- Test 9 ma na celu pokazanie mechanizmu synchronizacji kontekstu, który następuje automatycznie przed uruchomieniem select.
- wiersz 5: nie zmienia się kontekstu trwałości. newp1 znajduje się zatem w tym kontekście.
- wiersze 7–8: początek transakcji.
- wiersz 10: liczba elementów potomnych obiektu trwałego newp1 zostaje zwiększona o 1 (5 → 6).
- wiersze 12–15: wyświetlanie tabeli za pomocą polecenia SELECT. Kontekst zostanie zsynchronizowany z bazą danych przed wykonaniem select.
- wiersz 17: koniec transakcji
Aby sprawdzić synchronizację, uruchamiamy wyświetlanie logów Hibernate w trybie DEBUG (log4j.properties):
# Opcja rejestratora głównego
log4j.rootLogger=ERROR, stdout
# Opcje rejestrowania Hibernate (INFO wyświetla tylko komunikaty startowe)
log4j.logger.org.hibernate=DEBUG
Wyświetlanie konsoli dla testu 9 wygląda następująco:
- wiersz 1: rozpoczyna się test 9
- wiersze 2–6: rozpoczyna się transakcja JDBC. Tryb autocommit w SGBD jest wyłączony (wiersz 5)
- wiersz 7: wyświetlenie spowodowane przez wiersz 12 kodu Java. Kolejne wiersze kodu Java spowodują wygenerowanie select, a tym samym synchronizację kontekstu trwałości z bazą danych.
- wiersz 8: polecenie JPQL, które chcemy wysłać, zostało już wysłane. Hibernate odnajduje je w swojej pamięci podręcznej „przygotowanych zapytań”.
- wiersz 9: Hibernate informuje, że przeprowadzi operację flush kontekstu trwałości
- wiersze 11–12: Hibernate (Hb) wykrywa, że encja Personne#1 (o kluczu podstawowym 1) została zmieniona (dirty).
- wiersze 12–13: Hb informuje, że aktualizuje ten element i zmienia jego numer wersji z 3 na 4.
- wiersz 15: synchronizacja kontekstu spowoduje 0 wstawień, 1 aktualizację (update) i 0 usunięć (delete)
- wiersze 17–34: synchronizacja kontekstu (flush). Warto zwrócić uwagę na: zwiększenie numeru wersji (wiersz 19), przygotowane polecenie SQL update (wiersz 21), wartości parametrów polecenia update (wiersze 24–31).
- wiersz 35: rozpoczyna się zlecenie select
- wiersz 38: zlecenie SQL, które zostanie wykonane
- wiersz 40: zlecenie select zwraca tylko jeden wiersz
- wiersz 42: Hb wykrywa, że w swoim kontekście trwałości ma już encję Personne#1, którą zapytanie select pobrało z bazy. Nie kopiuje więc wiersza uzyskanego z bazy do kontekstu – operację tę nazywa „hydratacją”.
- wiersz 43: sprawdza, czy obiekty zwrócone przez select mają zależności (zazwyczaj klucze obce), które również należałoby załadować (kolekcje niebędące leniwymi). W tym przypadku nie ma żadnych.
- wiersz 44: wyświetlenie wywołane przez kod Java
- wiersz 45: zakończenie transakcji JDBC wywołane przez kod Java
- wiersz 46: rozpoczyna się automatyczna synchronizacja kontekstu, która ma miejsce podczas operacji commit.
- wiersz 48: Hb stwierdza, że kontekst nie uległ zmianie od poprzedniej synchronizacji.
- wiersz 50: koniec operacji commit.
Ponownie logi Hibernate w trybie DEBUG okazują się bardzo przydatne, aby dokładnie zrozumieć, co robi Hibernate.
2.1.13.11. Test 10
Kod testu 10 wygląda następująco:
// kontrola wersji (blokowanie optymistyczne)
public static void test10() {
// kontekst trwałości
EntityManager em = getEntityManager();
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększenie wersji newp1 bezpośrednio w bazie (zapytanie natywne)
em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
// zakończenie transakcji
tx.commit();
// początek nowej transakcji
tx = em.getTransaction();
tx.begin();
// zwiększamy liczbę potomków newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// zakończenie transakcji – musi się ona zakończyć niepowodzeniem, ponieważ newp1 nie ma już prawidłowej wersji
try {
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
// wycofujemy transakcję
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// zamyka się kontekst, który nie jest już aktualny
em.close();
// zrzut tabeli – wersja p1 musiała ulec zmianie
dump();
}
- Test 10 ma na celu zademonstrowanie mechanizmu wprowadzonego przez pole version w @Entity Personne, które posiada atrybut @Version o wartości JPA. Wyjaśniliśmy, że ta adnotacja powoduje, iż w bazie danych wartość kolumny powiązanej z adnotacją @Version jest zwiększana przy każdym update wykonanym na wierszu, do którego ona należy. Mechanizm ten, zwany również blokowaniem optymistycznym (optimistic locking), wymaga, aby klient, który chce zmodyfikować obiekt O w bazie danych, posiadał jego najnowszą wersję. Jeśli jej nie posiada, oznacza to, że obiekt został zmodyfikowany od momentu jego pobrania i należy go o tym powiadomić.
- wiersz 4: nie zmienia się kontekstu trwałości. newp1 znajduje się zatem w tym kontekście.
- wiersze 6–7: początek transakcji.
- wiersz 9: wersja obiektu newp1 zostaje zwiększona o 1 (4 -> 5) bezpośrednio w bazie danych. Zapytania typu nativeQuery omijają kontekst trwałości i odwołują się bezpośrednio do bazy danych. W rezultacie obiekt trwały newp1 i jego odpowiednik w bazie danych nie mają już tej samej wersji.
- wiersz 10: koniec pierwszej transakcji
- wiersze 13–14: początek drugiej transakcji
- wiersz 16: liczba elementów potomnych obiektu trwałego newp1 zostaje zwiększona o 1 (6 → 7).
- wiersz 19: koniec transakcji. Następuje zatem synchronizacja. Spowoduje ona aktualizację liczby elementów podrzędnych obiektu newp1 w bazie danych. Aktualizacja ta zakończy się niepowodzeniem, ponieważ obiekt trwały newp1 ma wersję 4, podczas gdy w bazie danych obiekt, który ma zostać zaktualizowany, ma wersję 5. Zostanie zgłoszony wyjątek, co uzasadnia zastosowanie bloku try/catch w kodzie.
- wiersz 21: wyświetlany jest wyjątek wraz z jego przyczyną.
- wiersz 25: cofnięcie transakcji
- wiersz 33: wyświetlenie tabeli: powinno być widoczne, że wersja obiektu newp1 w bazie danych wynosi 5.
Wynik testu 10 wyświetlany w konsoli wygląda następująco:
- wiersz 5: commit rzeczywiście generuje wyjątek. Jest to wyjątek typu [javax.persistence.RollbackException]. Komunikat związany z tym wyjątkiem jest niejasny. Jeśli przyjrzymy się przyczynie tego wyjątku (Exception.getCause), widzimy, że mamy do czynienia z wyjątkiem Hibernate spowodowanym próbą modyfikacji wiersza w bazie danych bez posiadania odpowiedniej wersji.
- wiersz 7: widać, że wersja newp1 w bazie została rzeczywiście zmieniona na 5 przez nativeQuery.
2.1.13.12. Test 11
Kod testu 11 wygląda następująco:
// wycofanie transakcji
public static void test11() throws ParseException {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = null;
try {
tx = em.getTransaction();
tx.begin();
// ponowne przypisanie p1 do kontekstu poprzez pobranie go z bazy
p1 = em.find(Personne.class, p1.getId());
// zwiększamy liczbę dzieci p1
p1.setNbenfants(p1.getNbenfants() + 1);
// wyświetlanie osób – liczba dzieci p1 musiała ulec zmianie
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// utworzenie dwóch osób o tym samym imieniu, co jest zabronione przez DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// trwałość osób
em.persist(p3);
em.persist(p4);
// zakończenie transakcji
tx.commit();
} catch (RuntimeException e1) {
// wystąpił problem
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// rezygnujemy z bieżącego kontekstu
em.clear();
}
// zrzut – tabela nie powinna ulec zmianie z powodu cofnięcia transakcji
dump();
}
- Test 11 dotyczy mechanizmu rollback w ramach transakcji. Transakcja działa na zasadzie „wszystko albo nic”: operacje SQL, które zawiera, są albo wszystkie pomyślnie wykonane (commit), albo wszystkie cofnięte w przypadku niepowodzenia jednej z nich (rollback).
- wiersz 4: kontynuujemy z tym samym kontekstem trwałości. Czytelnik być może pamięta, że kontekst został zamknięty po awarii poprzedniego testu. W tym przypadku [getEntityManager] zwraca zupełnie nowy, a więc pusty kontekst.
- wiersze 7–27: pojedynczy blok try/catch do obsługi potencjalnych problemów
- wiersze 8–9: początek transakcji, która będzie zawierała kilka operacji SQL
- wiersz 11: p1 jest wyszukiwane w bazie danych i umieszczane w kontekście
- wiersz 13: zwiększamy liczbę potomków p1 (z 6 na 7)
- wiersze 15–18: wyświetlana jest zawartość bazy danych, co wymusi synchronizację kontekstu. W bazie liczba dzieci p1 wzrośnie do 7, co powinno potwierdzić wyświetlenie w konsoli.
- wiersze 20–21: utworzenie dwóch osób o tej samej nazwie: p3 i p4. Jednak pole „nazwa” w @Entity Personne ma atrybut unique=true, co spowodowało nałożenie ograniczenia unikalności na kolumnę NOM w tabeli [jpa01_personne].
- wiersze 23–24: osoby o identyfikatorach p3 i p4 są umieszczane w kontekście trwałości.
- wiersz 26: transakcja zostaje zatwierdzona. Następnie następuje druga synchronizacja kontekstu, przy czym pierwsza miała miejsce w przypadku select. JPA wygeneruje dwa polecenia SQL i insert dla osób o numerach p3 i p4. p3 zostanie wstawiony. W przypadku p4, SGBD wygeneruje wyjątek, ponieważ p4 ma taką samą nazwę jak p3. p4 nie zostanie zatem wstawiony, a sterownik JDBC zgłosi wyjątek do klienta.
- wiersz 27: obsługa wyjątku
- wiersze 29–31: wyświetlamy wyjątek oraz dwie poprzednie przyczyny w łańcuchu wyjątków, które doprowadziły nas do tego momentu.
- wiersz 34: cofamy aktualnie aktywną transakcję. Rozpoczęła się ona w wierszu 9 kodu Java. Wcześniej przeprowadzono operację update w celu zmiany liczby dzieci w rekordzie p1, a następnie operację insert dotyczącą osoby p3. Wszystkie te zmiany zostaną cofnięte w wyniku wycofania transakcji.
- wiersz 39: kontekst trwałości zostaje wyczyszczony
- wiersz 42: wyświetlana jest tabela [jpa01_personne]. Należy sprawdzić, czy p1 nadal ma 6 dzieci oraz czy w tabeli nie ma ani p3, ani p4.
Wynik wyświetlony w konsoli dla testu 11 jest następujący:
main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
- wiersz 3: liczba elementów potomnych p1 wzrosła w bazie z 6 do 7, wersja p1 zmieniła się na 6.
- wiersz 4: wyjątek przechwycony podczas zatwierdzania transakcji. Jeśli dobrze się przyjrzeć, widać, że przyczyną jest zduplikowany klucz X (nazwa). To właśnie wstawienie rekordu p4 powoduje ten błąd, podczas gdy już wstawiony rekord p3 również ma nazwę X.
- wiersz 7: tabela po cofnięciu transakcji. p1 powrócił do wersji 5 i liczby potomków 6, natomiast p3 i p4 nie zostały wstawione.
2.1.13.13. Test 12
Kod testu 12 wygląda następująco:
// powtarzamy tę samą czynność, ale bez transakcji
// uzyskujemy ten sam wynik, co poprzednio z SGBD: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
// z SQLSERVER powoduje, że tabela jest pusta. Połączenie pozostaje w stanie uniemożliwiającym ponowne wykonanie
// programu. Należy wówczas ponownie uruchomić serwer.
// to samo dotyczy SGBD Derby
// HSQL wstawia pierwszą osobę – nie ma cofnięcia zmian
public static void test12() throws ParseException {
// ponownie przypisujemy p1
p1 = em.find(Personne.class, p1.getId());
// zwiększa się liczbę dzieci p1
p1.setNbenfants(p1.getNbenfants() + 1);
// wyświetlanie osób – liczba dzieci p1 musiała ulec zmianie
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// utworzenie 2 osób o tym samym imieniu, co jest zabronione przez DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// trwałość osób
em.persist(p3);
em.persist(p4);
// zrzut danych, który spowoduje synchronizację kontekstu em z BD
try {
dump();
} catch (RuntimeException e3) {
System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
.getCause().getMessage());
}
// zamykamy bieżący kontekst
em.close();
// zrzut
dump();
}
- Test 12 powtarza to samo, co test 11, ale poza transakcją. Chcemy sprawdzić, co się dzieje w tym przypadku.
- wiersze 1–6: przedstawiają wyniki testów z różnymi kodami SGBD:
- przy pewnej liczbie wartości SGBD (Firebird, Oracle, MySQL5, Postgres) otrzymujemy ten sam wynik, co w przypadku testu 11. To sugeruje, że te SGBD samodzielnie zainicjowały transakcję obejmującą wszystkie polecenia SQL otrzymane aż do tego, które spowodowało błąd, oraz że same zainicjowały rollback.
- W połączeniu z innymi instancjami SGBD (serwer SQL, Apache Derby) dochodzi do awarii aplikacji i/lub instancji SGBD.
- W przypadku SGBD i HSQLDB wydaje się, że transakcja otwarta przez SGBD znajduje się w trybie autocommit: zmiana liczby elementów potomnych w p1 oraz wstawienie p3 zostały utrwalone. Nie powiodło się jedynie wstawienie p4.
W rezultacie otrzymujemy wynik zależny od SGBD, co sprawia, że aplikacja nie jest przenośna. Należy pamiętać, że operacje na kontekście trwałości muszą zawsze odbywać się w ramach transakcji.
2.1.14. Zmiana na SGBD
Wróćmy do architektury testowej naszego obecnego projektu:
![]() |
Aplikacja kliencka [3] widzi jedynie interfejs JPA [5]. Nie widzi ani jego rzeczywistej implementacji, ani docelowego SGBD. Należy zatem zapewnić możliwość zmiany tych dwóch elementów łańcucha bez wprowadzania zmian w kliencie [3]. Właśnie to próbujemy teraz sprawdzić, zaczynając od zmiany SGBD. Do tej pory korzystaliśmy z MySQL5. Przedstawiamy sześć innych, opisanych w załącznikach (punkt 5), mając nadzieję, że wśród nich znajdzie się ulubiony przez czytelnika SGBD.
W każdym razie zmiana, jaką należy wprowadzić w projekcie Eclipse, jest prosta (patrz poniżej): należy zastąpić plik konfiguracyjny warstwy persistence.xml [1] warstwy JPA jednym z plików znajdujących się w folderze conf [2] projektu. Sterowniki JDBC i SGBD znajdują się już w bibliotece [jpa-divers], [3] oraz [4].
![]() |
2.1.14.1. Oracle 10g Express
Oracle 10g Express został przedstawiony w załącznikach w punkcie 5.7. Plik persistence.xml firmy Oracle ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- Klasy trwałe -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- automatyczne tworzenie schematu -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
<!-- właściwości DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
Ta konfiguracja jest identyczna z tą dla plików SGBD i MySQL5, z wyjątkiem następujących szczegółów:
- wiersze 15–18, które konfigurują połączenie JDBC z bazą danych
- wiersz 22: który określa dialekt SQL, który ma być używany
W kolejnych przykładach będziemy wskazywać tylko te wiersze, które ulegają zmianie. Wyjaśnienie konfiguracji można znaleźć w załączniku poświęconym używanemu SGBD. Za każdym razem podano tam przykład wykorzystania połączenia JDBC w kontekście wtyczki [SQL Explorer]. Korzystając z informacji zawartych w załączniku, czytelnik będzie mógł powtórzyć operację weryfikacji wyniku działania aplikacji [InitDB], przeprowadzoną w punkcie 2.1.10.2.
Postępujemy zgodnie z instrukcjami zawartymi w wyżej wymienionym paragrafie:
- uruchamiamy w Oracle wtyczkę SGBD
- umieścić plik conf/oracle/persistence.xml w katalogu META-INF/persistence.xml
- uruchom aplikację [InitDB]
Na konsoli pojawiają się następujące wyniki:
![]() |
W dalszej części nie będziemy już przedstawiać tego zrzutu ekranu, który jest zawsze taki sam. Bardziej interesująca jest perspektywa SQL Explorer dotycząca powiązania JDBC z SGBD. Postępujemy zgodnie z procedurą opisaną w paragrafie 2.1.8.
![]() |
- w [1]: połączenie z Oracle
- w [2]: struktura drzewa połączeń po uruchomieniu [InitDB]
- w [3]: struktura tabeli [jpa01_personne]
- w [4]: jej zawartość.
Po wykonaniu tych czynności czytelnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD.
2.1.14.2. PostgreSQL 8.2
PostgreSQL 8.2 został przedstawiony w załącznikach w paragrafie 5.6. Jego plik persistence.xml ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
</persistence-unit>
</persistence>
Aby uruchomić plik [InitDB]:
- uruchom plik SGBD PostgreSQL
- umieścić plik conf/postgres/persistence.xml w katalogu META-INF/persistence.xml
- uruchom aplikację [InitDB]
Widok SQL Explorer połączenia JDBC z SGBD przedstawia się następująco:
![]() |
- w [1]: połączenie z PostgreSQL
- w [2]: drzewo połączeń po wykonaniu [InitDB]
- w [3]: struktura tabeli [jpa01_personne]
- w [4]: jej zawartość.
Po wykonaniu tych czynności czytelnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD
2.1.14.3. SQL Server Express 2005
SQL Server Express 2005 został przedstawiony w załącznikach w paragrafie 5.8, na stronie 270. Jego plik persistence.xml ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
<property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
</persistence-unit>
</persistence>
Aby uruchomić plik [InitDB]:
- uruchom plik SGBD SQL Server
- umieścić plik conf/sqlserver/persistence.xml w katalogu META-INF/persistence.xml
- uruchomić aplikację [InitDB]
Perspektywa SQL Explorer dotycząca powiązania JDBC z SGBD przedstawia się następująco:
![]() |
- w [1]: połączenie z serwerem SQL
- w [2]: drzewo połączeń po uruchomieniu [InitDB]
- w [3]: struktura tabeli [jpa01_personne]
- w [4]: jej zawartość.
Po wykonaniu tych czynności czytelnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD
2.1.14.4. Firebird 2.0
Firebird 2.0 został przedstawiony w załącznikach w punkcie 5.4. Jego plik persistence.xml ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
<property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
<property name="hibernate.connection.username" value="sysdba" />
<property name="hibernate.connection.password" value="masterkey" />
...
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
</persistence-unit>
</persistence>
Aby uruchomić plik [InitDB]:
- uruchom plik SGBD Firebird
- umieścić plik conf/firebird/persistence.xml w katalogu META-INF/persistence.xml
- uruchom aplikację [InitDB]
Widok SQL Explorer połączenia JDBC z SGBD przedstawia się następująco:
![]() |
- w [1]: połączenie z Firebird
- w [2]: drzewo połączeń po uruchomieniu [InitDB]
- w pliku [3]: struktura tabeli [jpa01_personne]
- w [4]: jej zawartość.
Po wykonaniu tych czynności czytelnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD.
2.1.14.5. Apache Derby
Apache Derby został omówiony w załącznikach w punkcie 5.10. Jego plik persistence.xml ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
<property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Dialekt -->
...
</persistence-unit>
</persistence>
Aby uruchomić [InitDB]:
- uruchom plik SGBD Apache Derby
- umieścić plik conf/derby/persistence.xml w katalogu META-INF/persistence.xml
- uruchomić aplikację [InitDB]
Perspektywa SQL Explorer dotycząca połączenia JDBC z SGBD przedstawia się następująco:
![]() |
- w [1]: połączenie z Apache Derby
- w [2]: struktura drzewa połączeń po uruchomieniu [InitDB]. Warto zwrócić uwagę na tabelę [HIBERNATE_UNIQUE_KEY] utworzoną przez JPA / Hibernate w celu automatycznego generowania kolejnych wartości klucza głównego ID. Wspomnieliśmy już, że mechanizm ten jest często zastrzeżony. Widać to wyraźnie w tym przypadku. Dzięki JPA programista nie musi zagłębiać się w szczegóły dotyczące SGBD.
- w [3]: struktura tabeli [jpa01_personne]
- w pliku [4]: jego zawartość.
Po wykonaniu tych czynności czytelnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD.
2.1.14.6. HSQLDB
Plik HSQLDB znajduje się w załącznikach w punkcie 5.9. Jego zawartość to:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- połączenie JDBC -->
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
<property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
<property name="hibernate.connection.username" value="sa" />
<!--
<property name="hibernate.connection.password" value="" />
-->
...
<!-- Dialekt -->
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
</properties>
</persistence-unit>
</persistence>
Aby uruchomić plik [InitDB]:
- uruchom plik SGBD HSQL
- umieścić plik conf/hsql/persistence.xml w katalogu META-INF/persistence.xml
- uruchom aplikację [InitDB]
Perspektywa SQL Explorer dotycząca powiązania JDBC z SGBD przedstawia się następująco:
![]() |
- w [1]: połączenie z HSQL
- w [2]: drzewo połączeń po wykonaniu [InitDB].
- w [3]: struktura tabeli [jpa01_personne]
- w [4]: jej zawartość.
Po wykonaniu tych czynności czytnik powinien uruchomić aplikację [Main], a następnie zatrzymać SGBD.
2.1.15. Zmiana implementacji JPA
Wróćmy do architektury testowej naszego obecnego projektu:
![]() |
Poprzednia analiza wykazała, że udało nam się zamienić SGBD na [7] bez wprowadzania żadnych zmian w kodzie klienta [3]. Teraz zmieniamy implementację JPA [6] i ponownie pokazujemy, że odbywa się to w sposób przezroczysty dla kodu klienckiego [3]. Bierzemy implementację TopLink oraz [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:
![]() |
2.1.15.1. Projekt Eclipse
W związku ze zmianą implementacji JPA tworzymy nowy projekt Eclipse, aby nie zakłócać pracy istniejącego projektu. Nowy projekt wykorzystuje bowiem biblioteki trwałości danych, które mogą kolidować z bibliotekami Hibernate:
![]() |
- w [1]: folder [<exemples>/toplink/direct/personnes-entites] zawiera projekt Eclipse. Należy go zaimportować.
- w [2]: zaimportowany projekt [toplink-personnes-entites]. Jest on identyczny (został utworzony przez skopiowanie) z projektem [hibernate-personne-entites], z wyjątkiem dwóch szczegółów:
- plik [META-INF/persistence.xml] [3] konfiguruje teraz warstwę JPA / Toplink
- Biblioteka [jpa-hibernate] została zastąpiona bibliotekami [jpa-toplink], [4] oraz [5] (por. punkt 1.5).
- w [6]: folder [conf] zawiera wersję pliku [persistence.xml] dla każdego pliku SGBD.
- w [7]: folder [ddl], który będzie zawierał skrypty SQL służące do generowania schematu bazy danych.
2.1.15.2. Konfiguracja warstwy JPA / Toplink
Wiemy, że warstwa JPA jest konfigurowana przez plik [META-INF/persistence.xml]. Ten z kolei konfiguruje obecnie implementację JPA / Toplink. Jego zawartość dla warstwy JPA połączonej z SGBD i MySQL5 jest następująca:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- klasy trwałe -->
<class>entites.Personne</class>
<!-- właściwości jednostki trwałości -->
<properties>
<!-- połączenie JDBC -->
<property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="MySQL4" />
<!-- serwer aplikacji -->
<property name="toplink.target-server" value="None" />
<!-- generowanie schematu -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- logi -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
- wiersz 3: bez zmian
- wiersz 5: dostawcą jest teraz Toplink. Klasa wymieniona w tym miejscu znajduje się w bibliotece [jpa-toplink] ([1] poniżej):
![]() |
- wiersz 7: tag <class> służy do nazwania wszystkich klas @Entity w projekcie, tutaj tylko klasy Personne. Hibernate posiadał opcję konfiguracyjną, która pozwalała uniknąć nazywania tych klas. Przeglądał on plik classpath projektu w celu znalezienia w nim klas @Entity.
- wiersz 9: tag <properties>, który wprowadza właściwości specyficzne dla używanej implementacji JPA, w tym przypadku Toplink.
- wiersze 11–14: konfiguracja połączenia JDBC z SGBD MySQL5
- wiersze 15–18: konfiguracja puli połączeń JDBC zarządzanej natywnie przez Toplink:
- wiersze 15, 16: maksymalna i minimalna liczba połączeń w puli połączeń do odczytu. Wartość domyślna (2,2)
- wiersze 17, 18: maksymalna i minimalna liczba połączeń w puli połączeń do zapisu. Wartość domyślna (10,2)
- wiersz 20: docelowy plik SGBD. Lista dostępnych plików SGBD znajduje się w pakiecie [oracle.toplink.essentials.platform.database] (patrz [2] powyżej). SGBD MySQL5 nie występuje na liście [2], dlatego wybrano MySQL4. Toplink obsługuje nieco mniej pakietów niż Hibernate. Spośród siedmiu pakietów użytych w naszych przykładach Firebird nie jest obsługiwany. Na liście nie ma również pakietu Oracle. W rzeczywistości znajduje się on w innym pakiecie ([3] powyżej). Jeśli w obu tych pakietach docelowy plik SGBD jest oznaczony klasą <Sgbd>Platform.class, tag będzie wyglądał następująco:
<property name="toplink.target-database" value="<Sgbd>" />
- wiersz 22: określa serwer aplikacji, jeśli aplikacja działa na takim serwerze. Obecne możliwe wartości (None, OC4J_10_1_3, SunAS9). Wartość domyślna (None).
- wiersze 24–28: podczas inicjalizacji warstwy JPA zostanie ona poproszona o wyczyszczenie bazy danych zdefiniowanej przez połączenie JDBC z wierszy 11–14. W ten sposób zaczniemy od pustej bazy danych.
- wiersz 24: zlecamy Toplinkowi wykonanie operacji drop, a następnie create na tabelach schematu bazy danych
- wiersz 25: zlecamy Toplinkowi wygenerowanie skryptów SQL dla operacji drop i create. application-location określa folder, w którym zostaną wygenerowane te skrypty. Domyślnie: (bieżący katalog).
- wiersz 26: nazwa skryptu SQL dla operacji create.. Wartość domyślna: createDDL.jdbc.
- wiersz 27: nazwa skryptu SQL dla operacji drop.. Wartość domyślna: dropDDL.jdbc.
- wiersz 28: tryb generowania schematu (domyślnie: both):
- both: skrypty i baza danych
- database: tylko baza danych
- sql-script: tylko skrypty
- wiersz 30: wyłączamy (OFF) logi Toplink. Dostępne są następujące poziomy logowania: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. Domyślny: INFO.
Pełną definicję znaczników <property>, które można stosować w Toplink, można znaleźć pod adresem URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html].
2.1.15.3. Test [InitDB]
Nie trzeba robić nic więcej. Jesteśmy gotowi do przeprowadzenia pierwszego testu [InitDB]:
- uruchom SGBD, tutaj MySQL5
- uruchom [InitDB]
![]() |
- w [1]: wyświetlanie konsoli. Widzimy wyniki uzyskane wcześniej przy użyciu JPA / Hibernate.
- w [3]: otwieramy perspektywę [SQL Explorer], a następnie otwieramy połączenie [mysql5-jpa]
- w [4]: drzewo struktury bazy danych jpa. Okazuje się, że wykonanie [InitDB] utworzyło dwie tabele: [jpa01_personne], której się spodziewano, oraz tabelę [sequence], której nie oczekiwano.
![]() |
- w [5]: struktura tabeli [jpa01_personne], a w [6] – jej zawartość
- w pliku [7]: struktura tabeli [sequence], a w pliku [8] – jej zawartość.
Plik konfiguracyjny [persistence.xml] wymagał wygenerowania skryptów z pliku DDL:
<!-- generowanie schematu -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
Zobaczmy, co zostało wygenerowane w folderze [ddl/mysql5]:
![]() |
create.sql
CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- wiersz 1: rekord DDL z tabeli [jpa01_personne]. Widać, że Toplink nie zastosował atrybutu autoincrement dla klucza głównego ID. W związku z tym nie następuje automatyczne zwiększanie jego wartości podczas wstawiania wierszy.
- wiersz 2: DDL z tabeli [sequence]. Jej nazwa wydaje się wskazywać, że Toplink wykorzystuje tę tabelę do generowania wartości klucza głównego ID.
- wiersz 3: wstawienie pojedynczego wiersza do tabeli [SEQUENCE]
drop.sql
DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
- wiersz 1: usunięcie tabeli [jpa01_personne]
- wiersz 2: usunięcie konkretnego wiersza z tabeli [SEQUENCE]. Sama tabela nie jest usuwana, podobnie jak ewentualne pozostałe wiersze, które może zawierać.
Aby dowiedzieć się więcej o roli tabeli [SEQUENCE], należy w tabeli [persistence.xml] włączyć logi Toplink na poziomie FINE, który rejestruje polecenia SQL wysyłane przez Toplink:
<!-- logi -->
<property name="toplink.logging.level" value="FINE" />
Ponownie uruchamia się InitDB. Poniżej zachowano jedynie fragment wyświetlacza konsoli:
...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Połączenie(30708295)--Wątek(Thread[main,5,main])--Połączono: jdbc:mysql://localhost:3306/jpa
User: jpa@localhost
Database: MySQL Version: 5.0.37-community-nt
Driver: MySQL-AB JDBC Driver Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Połączenie(19255406)--Wątek(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Połączenie(30708295)--Wątek(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) domyślnie 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Połączenie(19255406)--Wątek(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Wątek(Thread[main,5,main])--Wyjątek [TOPLINK-4002] (Oracle TopLink Essentials – 2.0 (kompilacja b41-beta2 (30.03.2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Połączenie(30708295)--Wątek(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Połączenie(19255406)--Wątek(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Połączenie(30708295)--Wątek(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) wartości („SEQ_GEN”, 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--usunięto z jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Połączenie(14069849)--Wątek(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Połączenie(30708295)--Wątek(Thread[main,5,main])--rozłączenie
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Wątek(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa wylogowanie zakończone sukcesem
...
terminé ...
- wiersze 2–5: połączenie z SGBD wraz z jego parametrami. W rzeczywistości logi pokazują, że Toplink tworzy 3 połączenia z SGBD. Należy sprawdzić, czy liczba ta jest powiązana z jedną z wartości konfiguracyjnych używanych dla puli połączeń JDBC:
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
- wiersz 7: usunięcie tabeli [jpa01_personne]. Jest to normalne, ponieważ plik [persistence.xml] wymaga wyczyszczenia bazy jpa.
- wiersz 8: utworzenie tabeli [jpa01_personne]. Zauważamy, że klucz główny ID nie posiada atrybutu autoincrement.
- wiersz 9: utworzenie tabeli [SEQUENCE], która już istnieje, utworzona podczas poprzedniego uruchomienia.
- wiersze 10–13: Toplink zgłasza błąd podczas tworzenia tabeli [SEQUENCE].
- wiersze 15–18: Toplink czyści tabelę [SEQUENCE]. Po zakończeniu tego czyszczenia tabela [SEQUENCE] zawiera jeden wiersz (SEQ_NAME, SEQ_COUNT) o wartościach („SEQ_GEN”, 1).
- wiersz 18: tabela [jpa01_personne] zostaje opróżniona.
- wiersze 19–20: Toplink pomija jedyny wiersz, w którym SEQ_NAME = 'SEQ_GEN' z tabeli [SEQUENCE], o wartości ('SEQ_GEN', 1) na wartość („SEQ_GEN”, 51)
- wiersz 21: Toplink pobiera wartość 51 z wiersza („SEQ_GEN”, 51) z tabeli [SEQUENCE].
- wiersze 24–27: Toplink wstawia do tabeli [jpa01_personne] dwie osoby: „Martin” i „Durant”. Pojawia się tu pewna zagadka: klucze główne tych dwóch wierszy otrzymują wartości 2 i 3, nie wiadomo jednak, w jaki sposób wartości te zostały uzyskane. Nie wiadomo, czy wartość SEQ_COUNT (51) uzyskana w wierszu 21 miała jakieś zastosowanie. Należy zauważyć, że wartość wersji wierszy wynosi 1, podczas gdy Hibernate zaczynał od 0.
- wiersz 28: Toplink generuje SELECT, aby uzyskać wszystkie wiersze z tabeli [jpa01_personne]
- wiersze 29–30: wiersze wyświetlane przez klienta Java
- wiersze 31–32: Toplink zamyka połączenie. Operację tę powtórzy dla każdego z początkowo otwartych połączeń.
Ostatecznie nie znamy dokładnie roli tabeli [SEQUENCE], ale wydaje się jednak, że odgrywa ona pewną rolę w generowaniu wartości klucza głównego ID. Przechodząc na najszczegółniejszy poziom logów, FINEST, dowiadujemy się nieco więcej o roli tabeli [SEQUENCE].
<!-- logi -->
<property name="toplink.logging.level" value="FINEST" />
Poniżej zamieściliśmy wyłącznie logi dotyczące dodania tych dwóch osób do tabeli. To właśnie tutaj widać mechanizm generowania wartości klucza głównego:
- wiersz 4: widać, że liczba 51 pobrana z tabeli [SEQUENCE] w wierszu 2 służy do wyznaczenia przedziału wartości dla klucza głównego: [2,51]
- wiersz 5: pierwsza osoba otrzymuje wartość 2 jako klucz główny
- wiersz 8: druga osoba otrzymuje wartość 3 jako klucz główny
- wiersz 12: przedstawia zarządzanie wersjami dla pierwszej osoby
- wiersz 17: to samo dotyczy drugiej osoby
Poziom logów [FINEST] pokazuje również granice transakcji generowanych przez Toplink. Analiza tych logów pozwala zobaczyć, jak działa Toplink, i stanowi doskonały sposób na zrozumienie mostu między modelem obiektowym a relacyjnym.
Z powyższego wynika, że:
- różne implementacje JPA generują różne schematy baz danych. W tym przykładzie Hibernate i Toplink nie wygenerowały tych samych schematów.
- że poziomy logów FINE, FINER, FINEST w Toplinku należy wykorzystać, gdy tylko zajdzie potrzeba uzyskania wyjaśnień dotyczących dokładnego działania Toplinku.
2.1.15.4. Test [Main]
Teraz uruchamiamy test [Main]:
![]() |
- w [1]: wszystkie testy zakończyły się powodzeniem z wyjątkiem testu nr 11 [2]
- w [3]: wiersz 376, wiersz kodu, w którym wystąpił wyjątek
Kod powodujący wyjątek wygląda następująco:
} catch (RuntimeException e1) {
// wystąpił problem
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
...
- wiersz [3]: wiersz, w którym wystąpił wyjątek. Mamy NullPointerException, co sugeruje, że jedna z metod getCause z wierszy 4 i 5 zwróciła wskaźnik null. Wyrażenie takie jak [e1.getCause().getCause()] zakłada, że łańcuch wyjątków składa się z trzech elementów [e1.getCause().getCause(), e1.getCause(), e1]. Jeśli zawiera on tylko dwa, pierwsze wyrażenie spowoduje wyjątek.
Modyfikujemy poprzedni kod tak, aby wyświetlał tylko dwa ostatnie wyjątki z łańcucha wyjątków:
} catch (RuntimeException e1) {
// wystąpił problem
System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage());
try {
...
Po uruchomieniu otrzymujemy następujący wynik:
...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
Tym razem test 11 zakończył się powodzeniem. Wyświetlenia dotyczące wyjątku (wiersze 6–10) zostały wywołane przez kod Java (wiersz 3 powyższego kodu). Przypominamy, że test 11 łączył w ramach tej samej transakcji kilka operacji SQL, z których jedna zakończyła się niepowodzeniem i miała spowodować cofnięcie transakcji. Stany tabeli [jpa01_personne] przed (wiersz 3) i po teście (wiersz 12) są identyczne, co wskazuje, że cofnięcie transakcji miało miejsce.
Należy tutaj zwrócić uwagę na ważną kwestię: implementacje JPA / Hibernate i JPA / Toplink nie są w 100% zamienne. W tym przykładzie musimy zmienić kod klienta JPA, aby uniknąć błędu NullPointerException. Problem ten pojawi się ponownie w późniejszym czasie, tym razem w kontekście wyjątku.
2.1.16. Zmiana kodu z SGBD na JPA w implementacji / Toplink
Wróćmy do architektury testowej naszego obecnego projektu:
![]() |
Wcześniej SGBD używany w [7] to MySQL5. Pokazujemy wraz z Oracle, jak zmienić SGBD. W każdym razie zmiana, którą należy wprowadzić w projekcie Eclipse, jest prosta (patrz poniżej): należy zastąpić plik konfiguracyjny warstwy JPA o nazwie persistence.xml [1] jednym z plików znajdujących się w folderze conf ([2] i [3]) w projekcie.
![]() |
2.1.16.1. Oracle 10g Express
Oracle 10g Express został przedstawiony w załącznikach w punkcie 5.7. Plik persistence.xml firmy Oracle dla Toplink ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- klasy trwałe -->
<class>entites.Personne</class>
<!-- właściwości jednostki trwałości -->
<properties>
<!-- połączenie JDBC -->
<property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
<property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="Oracle" />
<!-- serwer aplikacji -->
<property name="toplink.target-server" value="None" />
<!-- generowanie schematu -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/oracle" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- logi -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
Ta konfiguracja jest identyczna z tą dla plików SGBD i MySQL5, z wyjątkiem następujących szczegółów:
- wiersze 11–14, które konfigurują połączenie JDBC z bazą danych
- wiersz 20: który określa docelowy plik SGBD
- wiersz 25: który określa folder generowania skryptów SQL dla DDL
Aby uruchomić test [InitDB]:
- uruchom SGBD Oracle
- umieścić plik conf/oracle/persistence.xml w katalogu META-INF/persistence.xml
- uruchomić aplikację [InitDB]
W konsoli oraz w perspektywie [SQL Explorer] otrzymujemy następujące wyniki:
![]() |
- [1]: wyświetlanie w konsoli
- [2]: połączenie [oracle-jpa] w SQL Explorer
- [3]: baza danych jpa
- [4]: InitDB utworzyło dwie tabele: JPA01_PERSONNE i SEQUENCE, podobnie jak w przypadku MySQL5. Czasami w [4] pojawiają się tabele [BIN*]. Odpowiadają one usuniętym tabelom. Aby zaobserwować to zjawisko, wystarczy ponownie uruchomić [InitDB]. Faza inicjalizacji warstwy JPA obejmuje czyszczenie bazy danych jpa, podczas którego tabela [JPA01_PERSONNE] jest usuwana:
![]() |
W [A] pojawia się tabela [BIN]. Oracle nie usuwa trwale tabeli, która przeszła operację drop, lecz umieszcza ją w koszu [Recycle Bin]. Kosz ten jest widoczny w [B] za pomocą narzędzia SQL Developer opisanego w paragrafie 5.7.4. W [B] można usunąć tabelę [JPA01_PERSONNE] znajdującą się w koszu. Spowoduje to opróżnienie kosza [C]. Jeśli w programie SQL Explorer odświeży się (kliknięcie prawym przyciskiem myszy / Refresh) tabele, widać, że tabela BIN już nie istnieje ([D]).
- [5, 6]: struktura i zawartość tabeli [JPA01_PERSONNE]
- [7, 8]: struktura i zawartość tabeli [SEQUENCE]
To wszystko! Czytelnik jest teraz proszony o uruchomienie aplikacji [Main] w środowisku Oracle.
2.1.16.2. Pozostałe tabele SGBD
Nie będziemy się zbytnio rozwodzić nad pozostałymi plikami SGBD. Wystarczy po prostu powtórzyć procedurę zastosowaną w przypadku Oracle. Należy zwrócić uwagę na następujące kwestie:
- niezależnie od tego, jaki jest plik SGBD, Toplink zawsze stosuje tę samą technikę do generowania wartości klucza głównego ID w tabeli [JPA01_PERSONNE]: wykorzystuje tabelę [SEQUENCE] opisaną powyżej.
- Toplink nie rozpoznaje klucza SGBD z Firebirda. Na takie przypadki istnieje ogólna baza danych:
W przypadku tej generycznej bazy o nazwie [Auto] testy z Firebird kończą się niepowodzeniem z powodu błędów składniowych SQL. Toplink wykorzystuje dla klucza głównego ID typ SQL Number(10), którego Firebird nie rozpoznaje. Należy zatem wybrać SGBD o typach SQL zgodnych z Firebirdem (w tym przykładzie). Tak jest w przypadku Apache Derby:
<!-- połączenie JDBC -->
<property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
<!-- SGBD -->
<!--
TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
-->
<property name="toplink.target-database" value="Derby" />
...
- Toplink nie potrafi wygenerować oryginalnego schematu bazy danych dla plików SGBD i HSQLDB. Oznacza to, że dyrektywa:
<!-- generowanie schematu -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
nie działa dla HSQLDB. Przyczyną jest błąd składniowy podczas tworzenia tabeli [jpa01_personne]:
[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Połączenie(29775659)--Wątek(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Połączenie(29775659)--Wątek(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Wątek(Thread[main,5,main])--Wyjątek [TOPLINK-4002] (Oracle TopLink Essentials – 2.0 (kompilacja b41-beta2 (30.03.2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]
Wiersz 4, składnia NOM VARCHAR(30) UNIQUE NOT NULL nie jest akceptowana przez HSQL. Hibernate użył następującej składni: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).
Ogólnie rzecz biorąc, Hibernate wykazał się większą skutecznością niż Toplink w rozpoznawaniu ciągów znaków SGBD, na których przeprowadzono testy opisane w niniejszym dokumencie.
2.1.17. Wnioski
Analiza @Entity [Personne] kończy się w tym miejscu. Z koncepcyjnego punktu widzenia zrobiono stosunkowo niewiele: przeanalizowaliśmy most obiektowo-relacyjny w najprostszym przypadku: obiekt @Entity <--> tabela. Analiza ta pozwoliła nam jednak przedstawić narzędzia, z których będziemy korzystać w całym dokumencie. Dzięki temu będziemy mogli nieco szybciej przejść do analizy innych przypadków mostu obiektowo-relacyjnego, które będziemy badać:
- Do poprzedniej klasy @Entity [Personne] dodamy pole adresse, modelowane przez klasę [Adresse]. W zakresie bazy danych omówimy dwie możliwe implementacje. Obiekty [Personne] i [Adresse] tworzą
- jedną tabelę o nazwie [personne], zawierającą adres
- dwie tabele [personne] i [adresse] połączone relacją klucza obcego typu jeden do jednego.
- przykład relacji „jeden do wielu”, w której tabela [article] jest powiązana z tabelą [categorie] za pomocą klucza obcego
- przykład relacji wiele-do-wielu, w której dwie tabele [personne] i [activite] są połączone za pomocą tabeli łączącej [personne_activite].
2.2. Przykład 2: relacja jeden do jednego poprzez inkluzję
2.2.1. Schemat bazy danych
1 ![]() | 2 |
- w [1]: baza danych (wtyczka Azurri Clay)
- w [2]: DDL wygenerowana przez Hibernate dla MySQL5
Tabela [jpa02_personne] to omówiona wcześniej tabela [jpa01_personne], do której dodano adres (wiersze 12–18 w pliku DDL).
2.2.2. Obiekty @Entity reprezentujące bazę danych
Adres osoby będzie reprezentowany przez następującą klasę [Adresse]:
package entites;
...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {
// pola
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
// konstruktory
public Adresse() {
}
public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
}
}
- Główną nowością jest adnotacja @Embeddable w wierszu 5. Klasa [Adresse] nie jest przeznaczona do utworzenia tabeli, dlatego nie posiada adnotacji @Entity. Adnotacja @Embeddable wskazuje, że klasa ta ma zostać wbudowana w obiekt @Entity, a tym samym w tabelę z nim powiązaną. Dlatego w schemacie bazy danych klasa [Adresse] nie pojawia się jako oddzielna tabela, lecz jako część tabeli powiązanej z @Entity [Personne].
Klasa @Entity [Personne] nie różni się znacząco od poprzedniej wersji: dodano do niej jedynie pole adresse:
package entites;
...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@Embedded
private Adresse adresse;
// konstruktory
public Personne() {
}
...
}
- zmiana dotyczy wierszy 33–34. Obiekt [Personne] posiada teraz pole adresse typu Adresse. To dotyczy obiektu POJO. Adnotacja @Embedded służy do mostkowania obiektów i relacji. Wskazuje ona, że pole [Adresse adresse] powinno zostać zamknięte w tej samej tabeli co obiekt [Personne].
2.2.3. Środowisko testowe
Przeprowadzimy testy bardzo podobne do tych, które omówiliśmy wcześniej. Zostaną one przeprowadzone w następującym kontekście:
![]() |
Wykorzystywana implementacja to JPA / Hibernate [6]. Projekt Eclipse dla testów wygląda następująco:
![]() |
Projekt Eclipse [1] różni się od poprzedniego jedynie kodami Java [2]. Środowisko (biblioteki – persistence.xml – system zarządzania bazami danych – foldery konfiguracyjne, DDL – skrypt Ant) jest tym samym, które zostało już omówione wcześniej, w szczególności w punkcie 2.1.5. Będzie to miało zastosowanie również do przyszłych projektów Hibernate i, z nielicznymi wyjątkami, nie będziemy już wracać do tego środowiska. W szczególności pliki persistence.xml, które konfigurują warstwę JPA/Hibernate dla różnych SGBD, to te, które zostały już omówione i które znajdują się w folderze <conf>.
W razie wątpliwości co do procedur, które należy zastosować, zachęcamy czytelnika do powrotu do procedur opisanych w poprzednim opracowaniu.
Projekt Eclipse o nazwie [3] znajduje się w folderze przykładów [4]. Należy go zaimportować.
2.2.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami zawartymi w punkcie 2.1.7, plik DDL uzyskany dla pliku SGBD MySQL5 ma następującą postać:
drop table if exists jpa02_hb_personne;
create table jpa02_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
Hibernate poprawnie rozpoznał, że adres osoby powinien zostać dodany do tabeli powiązanej z @Entity Personne (wiersze 11–17).
2.2.5. InitDB
Kod dla [InitDB] jest następujący:
package tests;
...
public class InitDB {
// stałe
private final static String TABLE_NAME = "jpa02_hb_personne";
public static void main(String[] args) throws ParseException {
// Kontekst trwałości
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// pobieramy EntityManager z poprzedniego EntityManagerFactory
em = emf.createEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zapytanie
Query sql1;
// usunięcie elementów z tabeli PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
sql1.executeUpdate();
// tworzenie osób
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// tworzenie adresów
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// powiązania osoba <--> adres
p1.setAdresse(a1);
p2.setAdresse(a2);
// trwałość osób
em.persist(p1);
em.persist(p2);
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// zakończenie transakcji
tx.commit();
// koniec EntityManager
em.close();
// koniec EntityManagerFactory
emf.close();
// log
System.out.println("terminé...");
}
}
W tym kodzie nie ma nic nowego. Wszystko to już się pojawiało. Wykonanie [InitDB] wraz z MySQL5 daje następujące wyniki:
![]() |
![]() |
- [1]: wyświetlanie w konsoli
- [2]: tabela [jpa02_hb_personne] w widoku SQL Explorer
- [3] i [4]: jej struktura i zawartość.
2.2.6. Strona główna
Klasa [Main] ma następujący wygląd:
package tests;
...
import entites.Adresse;
import entites.Personne;
@SuppressWarnings( { "unused", "unchecked" })
public class Main {
// stałe
private final static String TABLE_NAME = "jpa02_hb_personne";
// Kontekst trwałości
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// obiekty współdzielone
private static Personne p1, p2, newp1;
private static Adresse a1, a2, a3, a4, newa1, newa4;
public static void main(String[] args) throws Exception {
// pobieramy obiekt EntityManager z obiektu EntityManagerFactory
em = emf.createEntityManager();
// czyszczenie bazy
log("clean");clean();
// zrzut tabeli
dumpPersonne();
// test1
log("test1"); test1();
// test2
log("test2"); test2();
// test3
log("test3"); test3();
// test4
log("test4"); test4();
// test5
log("test5");test5();
// koniec kontekstu trwałości
if (em != null && em.isOpen())
em.close();
// zamknięcie EntityManagerFactory
emf.close();
}
// pobierz bieżący EntityManager
private static EntityManager getEntityManager() {
...
}
// pobierz nowy EntityManager
private static EntityManager getNewEntityManager() {
...
}
// wyświetlenie zawartości tabeli „Osoba”
private static void dumpPersonne() {
...
}
// wyczyść BD
private static void clean() {
...
}
// logi
private static void log(String message) {
...
}
// tworzenie obiektów
public static void test1() throws ParseException {
// kontekst trwałości
EntityManager em = getEntityManager();
// tworzenie osób
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// tworzenie adresów
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// powiązania osoba <--> adres
p1.setAdresse(a1);
p2.setAdresse(a2);
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// trwałość osób
em.persist(p1);
em.persist(p2);
// koniec transakcji
tx.commit();
// zrzut
dumpPersonne();
}
// modyfikacja obiektu kontekstu
public static void test2() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększamy liczbę dzieci obiektu p1
p1.setNbenfants(p1.getNbenfants() + 1);
// zmiana stanu cywilnego
p1.setMarie(false);
// obiekt p1 jest automatycznie zapisywany (sprawdzanie zmian)
// podczas następnej synchronizacji (commit lub select)
// koniec transakcji
tx.commit();
// wyświetla się nowa tabela
dumpPersonne();
}
// usunięcie obiektu należącego do kontekstu trwałości
public static void test4() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuwanie obiektu powiązanego p2
em.remove(p2);
// koniec transakcji
tx.commit();
// wyświetlanie nowej tabeli
dumpPersonne();
}
// odłączanie, ponowne dołączanie i modyfikacja
public static void test5() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// ponowne przypisanie p1 do nowego kontekstu
p1 = em.find(Personne.class, p1.getId());
// koniec transakcji
tx.commit();
// zmiana adresu p1
p1.getAdresse().setVille("Paris");
// wyświetlanie nowej tabeli
dumpPersonne();
}
}
Ponownie nie ma tu niczego, czego byśmy już nie widzieli. Wyświetlanie w konsoli wygląda następująco:
Czytelnik powinien spróbować powiązać wyniki z kodem.
2.2.7. Implementacja JPA / Toplink
Obecnie korzystamy z implementacji JPA / Toplink:
![]() |
Nowy projekt Eclipse dotyczący testów wygląda następująco:
![]() |
Kod Java jest identyczny jak w poprzednim projekcie Hibernate. Środowisko (biblioteki – persistence.xml – system zarządzania bazami danych – foldery konfiguracyjne, DDL – skrypt Ant) jest takie samo, jak omówiono w punkcie 2.1.15.2. Będzie to miało zastosowanie również do przyszłych projektów Toplink i, z nielicznymi wyjątkami, nie będziemy już wracać do tego środowiska. W szczególności pliki persistence.xml, które konfigurują warstwę JPA/Toplink dla różnych SGBD, to te, które zostały już omówione i które znajdują się w folderze <conf>.
W razie wątpliwości co do procedur, które należy zastosować, zachęcamy czytelnika do zapoznania się z procedurami opisanymi w poprzednim opracowaniu.
Projekt Eclipse o nazwie [3] znajduje się w folderze przykładów [4]. Należy go zaimportować.
Uruchomienie pliku [InitDB] wraz z plikami SGBD i MySQL5 daje następujące wyniki:
![]() |
![]() |
- [1]: wyświetlanie w konsoli
- [2]: tabele [jpa02_tl_personne] i [SEQENCE] w widoku SQL Explorer
- [3] i [4]: struktura i zawartość pliku [jpa02_tl_personne].
Skrypty SQL wygenerowane w katalogu ddl/mysql5 [5] są następujące:
create.sql
CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.3. Przykład 3: relacja jeden do jednego za pomocą klucza obcego
2.3.1. ny schemat bazy danych
1 ![]() | 2 |
- w [1]: baza danych. Tym razem adres osoby jest umieszczony w osobnej tabeli [adresse]. Tabela [personne] jest powiązana z tą tabelą za pomocą klucza obcego.
- w [2]: tabela DDL wygenerowana przez Hibernate dla tabeli MySQL5:
- wiersze 9–20: tabela [adresse], która zostanie powiązana z klasą [Adresse], która stała się obiektem @Entity.
- wiersz 10: klucz główny tabeli [adresse]
- wiersz 30: zamiast pełnego adresu w tabeli [personne] znajduje się teraz identyfikator [adresse_id] tego adresu.
- wiersze 34–38: osoba (adresse_id) jest kluczem obcym dla adresu (id).
2.3.2. Obiekty @Entity reprezentujące bazę danych
Osoba posiadająca adres jest obecnie reprezentowana przez następującą klasę [Personne]:
package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
...
}
- wiersze 32–34: adres osoby
- wiersz 32: adnotacja @OneToOne określa relację jeden do jednego: osoba ma co najmniej jeden i co najwyżej jeden adres. Atrybut cascade = CascadeType.ALL oznacza, że każda operacja (persist, merge, remove) na @Entity [Personne] musi być kaskadowo zastosowana na @Entity [Adresse]. Z punktu widzenia kontekstu trwałości em oznacza to, co następuje. Jeśli p jest osobą i ma swój adres:
- jawną operację em.persist(p) spowoduje ona niejawną operację em.persist(a)
- jawną operację em.merge(p) spowoduje niejawną operację em.merge(a)
- jawna operacja em.remove(p) spowoduje wykonanie niejawnej operacji em.remove(a)
- wiersz 32: adnotacja @OneToOne określa relację jeden do jednego: osoba ma co najmniej jeden i co najwyżej jeden adres. Atrybut cascade = CascadeType.ALL oznacza, że każda operacja (persist, merge, remove) na @Entity [Personne] musi być kaskadowo zastosowana na @Entity [Adresse]. Z punktu widzenia kontekstu trwałości em oznacza to, co następuje. Jeśli p jest osobą i ma swój adres:
Doświadczenie pokazuje, że te domyślne kaskady nie są panaceum. Programista w końcu zapomina, do czego one służą. W kodzie można preferować operacje jawne. Istnieją różne rodzaje kaskad. Adnotacja @OneToOne mogłaby zostać zapisana w następujący sposób:
//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)
Atrybut cascade przyjmuje tutaj jako wartość tablicę stałych określających pożądane typy kaskad.
Atrybut fetch=FetchType.LAZY nakazuje Hibernate załadowanie zależności w ostatniej chwili. Umieszczając listę osób w kontekście trwałości, niekoniecznie chcemy umieszczać w nim ich adresy. Na przykład adres ten może być potrzebny tylko dla konkretnej osoby wybranej przez użytkownika za pośrednictwem interfejsu internetowego. Natomiast atrybut fetch=FetchType.EAGER powoduje natychmiastowe załadowanie zależności.
- (ciąg dalszy)
- wiersz 33: adnotacja @JoinColumn definiuje klucz obcy, który tabela @Entity [Personne] posiada w tabeli @Entity [Adresse]. Atrybut name określa nazwę kolumny, która pełni rolę klucza obcego. Atrybut unique=true wymusza relację jeden do jednego: w kolumnie [adresse_id] nie może wystąpić dwukrotnie ta sama wartość. Atrybut nullable=false wymusza, aby każda osoba posiadała adres.
Adres osoby jest teraz reprezentowany przez następującą instancję @Entity o nazwie [Adresse]:
package entites;
...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {
// pola
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
// konstruktory
public Adresse() {
}
...
}
- wiersz 4: klasa [Adresse] staje się obiektem @Entity. Będzie ona zatem stanowić podstawę tabeli w bazie danych.
- wiersze 9–12: podobnie jak każdy obiekt @Entity, [Adresse] posiada klucz główny. Został on nazwany Id i posiada te same (standardowe) adnotacje, co klucz główny Id obiektu @Entity [Personne].
- wiersze 39–40: relacja jeden do jednego z @Entity [Personne]. Jest tu kilka niuansów:
- po pierwsze, pole personne nie jest obowiązkowe. Pozwala nam ono, na podstawie adresu, dotrzeć do jedynej osoby posiadającej ten adres. Gdybyśmy nie chcieli tej wygody, pole personne nie istniałoby, a wszystko i tak by działało.
- Relacja „jeden do jednego”, łącząca dwie encje [Personne] i [Adresse], została już skonfigurowana w @Entity [Personne]:
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
Aby te dwie konfiguracje relacji „jeden do jednego” nie kolidowały ze sobą, jedna z nich jest traktowana jako principale, a druga jako inverse. To właśnie relacja o nazwie principale jest zarządzana przez most obiektowo-relacyjny. Druga relacja, o nazwie inverse, nie jest zarządzana bezpośrednio: jest zarządzana pośrednio poprzez relację principale. W @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
to atrybut mappedBy, który tworzy powyższą relację jeden do jednego, relacja inverse z relacją principale typu „jeden do jednego”, zdefiniowaną przez pole adresse w @Entity [Personne].
2.3.3. Projekt Eclipse / Hibernate 1
Wykorzystana tutaj implementacja JPA pochodzi z biblioteki Hibernate. Projekt Eclipse zawierający testy wygląda następująco:
![]() |
Projekt ten znajduje się pod nazwą [3] w folderze przykładów [4]. Zaimportujemy go.
2.3.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami zawartymi w punkcie 2.1.7, plik DDL uzyskany dla plików SGBD i MySQL5 jest tym samym plikiem, który pokazano na początku tego punktu.
2.3.5. InitDB
Kod dla [InitDB] jest następujący:
package tests;
...
import entites.Adresse;
import entites.Personne;
public class InitDB {
// stałe
private final static String TABLE_PERSONNE = "jpa03_hb_personne";
private final static String TABLE_ADRESSE = "jpa03_hb_adresse";
public static void main(String[] args) throws ParseException {
// Kontekst trwałości
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// – na podstawie poprzedniego kodu EntityManagerFactory uzyskujemy kod EntityManager
em = emf.createEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zapytanie
Query sql1;
// usunięcie elementów z tabeli PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// usunięcie elementów z tabeli ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// tworzenie osób
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// tworzenie adresów
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// powiązania osoba <--> adres
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// zachowanie osób i, w konsekwencji, ich adresów
em.persist(p1);
em.persist(p2);
// oraz adresów a3 i a4 niepowiązanych z osobami
em.persist(a3);
em.persist(a4);
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// wyświetlanie adresów
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
// koniec transakcji
tx.commit();
// koniec EntityManager
em.close();
// koniec EntityManagerFactory
emf.close();
// log
System.out.println("terminé...");
}
}
Komentujemy tylko te elementy, które stanowią nowość w stosunku do tego, co zostało już omówione:
- wiersze 31–32: tworzymy dwie osoby
- wiersze 34–37: tworzy się cztery adresy
- wiersze 39–42: przypisujemy osoby (p1, p2) do adresów (a1, a2). Adresy (a3, a4) są osierocone. Żadna osoba nie odwołuje się do nich. Kod DDL na to pozwala. Chociaż każda osoba musi mieć adres, to odwrotna sytuacja nie ma miejsca.
- wiersze 44–45: zapisujemy osoby (p1, p2). Ponieważ na relacji „jeden do jednego”, łączącej osobę z jej adresem, ustawiono atrybut cascade = CascadeType.ALL, adresy (a1, a2) tych dwóch osób powinny również zostać poddane operacji persist. To właśnie chcemy sprawdzić. W przypadku adresów osieroconych (a3, a4) musimy wykonać te czynności jawnie (wiersze 47–48).
- wiersze 51–53: wyświetlenie tabeli osób
- wiersze 56–57: wyświetlenie tabeli adresów
Uruchomienie [InitDB] wraz z MySQL5 daje następujące wyniki:
![]() |
![]() |
- [1]: wyświetlanie w konsoli
- [2]: tabele [jpa03_hb_*] w widoku SQL Explorer
- [3]: tabela osób
- [4]: tabela adresów. Wszystkie są na swoim miejscu. Warto również zwrócić uwagę na powiązanie między kolumną [adresse_id] w tabeli [3] a kolumną [id] w tabeli [4] (klucz obcy).
2.3.6. Strona główna
Klasa [Main] zawiera sześć testów, które teraz przeanalizujemy.
2.3.6.1. Test1
Test ten wygląda następująco:
// tworzenie obiektów
public static void test1() throws ParseException {
// kontekst trwałości
EntityManager em = getEntityManager();
// tworzenie osób
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// tworzenie adresów
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// powiązania osoba <--> adres
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// trwałość osób
em.persist(p1);
em.persist(p2);
// oraz adresów a3 i a4 niepowiązanych z osobami
em.persist(a3);
em.persist(a4);
// koniec transakcji
tx.commit();
// wyświetlanie tabel
dumpPersonne();
dumpAdresse();
}
Ten kod pochodzi z [InitDB]. Jego wynik jest następujący:
Obie tabele zostały wypełnione.
2.3.6.2. Test2
Test ten wygląda następująco:
// modyfikacja obiektu kontekstu
public static void test2() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zwiększanie liczby dzieci p1
p1.setNbenfants(p1.getNbenfants() + 1);
// zmiana stanu cywilnego
p1.setMarie(false);
// obiekt p1 jest automatycznie zapisywany (sprawdzanie zmian)
// podczas następnej synchronizacji (commit lub select)
// koniec transakcji
tx.commit();
// wyświetlono nową tabelę
dumpPersonne();
}
Jego wynik jest następujący:
- wiersz 4: liczba dzieci osoby p1 wzrosła o 1, a jej wersja zmieniła się z 0 na 1
2.3.6.3. Test4
Test ten wygląda następująco:
// usunięcie obiektu należącego do kontekstu trwałości
public static void test4() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuwany jest obiekt powiązany p2
em.remove(p2);
// koniec transakcji
tx.commit();
// wyświetlanie nowych tabel
dumpPersonne();
dumpAdresse();
}
- wiersz 9: usuwamy osobę p2. Osoba ta ma relację kaskadową z adresem a2. Zatem adres a2 również powinien zostać usunięty.
Wynik testu 4 jest następujący:
- osoba p2, występująca w wierszu 3 testu 1, nie występuje już w teście 4
- to samo dotyczy jej adresu a2, który w wierszu 7 testu 1 jest obecny, a w teście 4 go nie ma.
2.3.6.4. Test 5
Ten test wygląda następująco:
// odłączanie, ponowne dołączanie i modyfikowanie
public static void test5() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// ponowne przypisanie p1 do nowego kontekstu
p1 = em.find(Personne.class, p1.getId());
// zmiana adresu p1
p1.getAdresse().setVille("Paris");
// koniec transakcji
tx.commit();
// wyświetlanie nowych tabel
dumpPersonne();
dumpAdresse();
}
- wiersz 4: mamy nowy, a więc pusty kontekst trwałości.
- wiersz 9: umieszczamy w nim osobę p1. p1 jest wyszukiwany w bazie, ponieważ nie ma go w kontekście. Elementy zależne od p1 (jego adres) nie są pobierane z bazy, ponieważ wpisano:
@OneToOne(..., fetch=FetchType.LAZY)
Jest to koncepcja „lazy loading” lub „ładowania na żądanie”: zależności obiektu trwałego są pobierane do pamięci dopiero wtedy, gdy są potrzebne.
- wiersz 11: modyfikujemy pole „miasto” w adresie p1. Ze względu na getAdresse oraz na to, że adres p1 nie znajdował się jeszcze w kontekście trwałości, zostanie on do niego pobrany poprzez odczyt z bazy danych.
- wiersz 13: zatwierdzamy transakcję, co spowoduje synchronizację kontekstu trwałości z bazą danych. Kontekst ten wykryje, że adres osoby p1 został zmodyfikowany i zapisze go.
Wykonanie test5 daje następujące wyniki:
- miasto osoby p1 (wiersz 3 test4, wiersz 10 test5) rzeczywiście zmieniło się z Angers (wiersz 5 test4) na Paryż (wiersz 12 test5).
2.3.6.5. Test6
Ten test wygląda następująco:
// usuwanie obiektu „Adres”
public static void test6() {
EntityTransaction tx = null;
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
tx = em.getTransaction();
tx.begin();
// ponowne przypisanie adresu a3 do nowego kontekstu
a3 = em.find(Adresse.class, a3.getId());
System.out.println(a3);
// usuwamy go
em.remove(a3);
// koniec transakcji
tx.commit();
// zrzut tabeli Adres
dumpAdresse();
}
- wiersz 5: znajdujemy się w nowym kontekście trwałości, a więc pustym.
- wiersz 10: umieszczamy adres a3 w kontekście trwałości
- wiersz 13: usuwamy go. Był to adres osierocony (niepowiązany z żadną osobą). Usunięcie jest zatem możliwe.
Wynik wykonania jest następujący:
- adres a3 z testu 5 (wiersz 6) zniknął z listy adresów w teście 6 (wiersze 11–12)
2.3.6.6. Test 7
Ten test wygląda następująco:
// cofnięcie
public static void test7() {
EntityTransaction tx = null;
try {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
tx = em.getTransaction();
tx.begin();
// ponowne przypisanie adresu a1 do nowego kontekstu
newa1 = em.find(Adresse.class, a1.getId());
// ponowne przypisanie adresu a4 do nowego kontekstu
newa4 = em.find(Adresse.class, a4.getId());
// próbuje się je usunąć – powinno to wywołać wyjątek, ponieważ nie można usunąć adresu powiązanego z osobą, co ma miejsce w przypadku newa1
em.remove(newa4);
em.remove(newa1);
// koniec transakcji
tx.commit();
} catch (RuntimeException e1) {
// wystąpił błąd
System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
.getCause());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// rezygnujemy z bieżącego kontekstu
em.clear();
}
// zrzut – tabela „Adres” nie powinna ulec zmianie z powodu cofnięcia transakcji
dumpAdresse();
}
- test7: testujemy wycofanie transakcji
- wiersz 6: znajdujemy się w nowym, a więc pustym kontekście trwałości
- wiersz 11: umieszczamy adres a1 w kontekście trwałości pod numerem referencyjnym newa1
- wiersz 13: umieszczamy adres a4 w kontekście trwałości, pod odniesieniem newa4
- wiersze 15–16: usuwa się dwa adresy: newa1 i newa4. newa1 jest adresem osoby p1, a zatem w bazie danych p1 odwołuje się do newa1 za pomocą klucza obcego. Usunięcie newa1 zakończy się zatem niepowodzeniem i spowoduje wygenerowanie wyjątku podczas synchronizacji kontekstu trwałości przy zatwierdzeniu transakcji (wiersz 18). Transakcja ta zostanie poddana operacji rollback (wiersz 25), w związku z czym obie operacje w ramach transakcji zostaną cofnięte. Powinno się zatem stwierdzić, że adres newa4, który mógłby zostać legalnie usunięty, nie został usunięty.
Wynik wykonania jest następujący:
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- tabela adresów z testu 7 (wiersze 12–13) jest identyczna z tabelą z testu 6 (wiersze 4–5). Wygląda na to, że cofnięcie transakcji miało miejsce. Niemniej jednak komunikat o błędzie w wierszu 9 stanowi zagadkę i zasługuje na dokładniejsze zbadanie. Wydaje się, że wystąpił wyjątek inny niż oczekiwany. Aby uzyskać jaśniejszy obraz sytuacji, należy przekształcić logi Hibernate z formatu log4j.properties do formatu DEBUG:
# Opcja rejestratora głównego
log4j.rootLogger=ERROR, stdout
# Opcje logowania Hibernate (INFO wyświetla tylko komunikaty startowe)
log4j.logger.org.hibernate=DEBUG
Widać wówczas, że gdy adres a1 został umieszczony w kontekście trwałości, Hibernate umieścił tam również osobę p1, prawdopodobnie z powodu relacji jeden do jednego w @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
Chociaż w tym miejscu zażądano „LazyLoading”, to jednak natychmiast ładowana jest zależność [Personne]. Oznacza to prawdopodobnie, że atrybut fetch=FetchType.LAZY nie ma tutaj sensu. Następnie zauważamy, że podczas zatwierdzania transakcji Hibernate przygotował usunięcie adresów a1 i a4, ale także zapisanie osoby p1. I właśnie w tym momencie pojawia się wyjątek: ponieważ osoba o identyfikatorze p1 ma ustawioną kaskadę dla swojego adresu, Hibernate próbuje również zapisać adres a1, mimo że został on właśnie usunięty. To Hibernate generuje wyjątek, a nie sterownik JDBC. Stąd komunikat w wierszu 9 powyżej. Ponadto można zauważyć, że polecenie rollback z wiersza 25 nigdy nie zostało wykonane, ponieważ transakcja stała się nieaktywna. Test z wiersza 24 uniemożliwia zatem wykonanie polecenia rollback.
Nie osiągnięto zatem zamierzonego celu: pokazania cofnięcia transakcji. W rzeczywistości żadne polecenie SQL nie zostało wysłane do bazy danych. Należy zwrócić uwagę na kilka kwestii:
- korzyści z włączenia szczegółowych logów w celu zrozumienia działania polecenia ORM
- chociaż ORM może ułatwić pracę programisty, może również ją skomplikować, ukrywając zachowania, o których programista powinien wiedzieć. W tym przypadku chodzi o sposób ładowania zależności klasy @Entity.
2.3.7. Projekt Eclipse / Hibernate 2
Kopiujemy i wklejamy projekt Eclipse / Hibernate, aby nieznacznie zmodyfikować konfigurację obiektów @Entity:
![]() |
Projekt o nazwie [3] znajduje się w folderze przykładów [4]. Zaimportujemy go.
Modyfikujemy wyłącznie @Entity o numerze [Adresse], tak aby nie miała już odwrotnej relacji jeden do jednego z @Entity o numerze [Personne]:
package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {
// pola
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
...
@Column(length = 20, nullable = false)
private String pays;
// @OneToOne(mappedBy = „adres”, fetch=FetchType.LAZY)
// private Personne osoba;
// konstruktorzy
public Adresse() {
}
- wiersze 25–26: odwrotna relacja @OneToOne została usunięta. Należy dobrze zrozumieć, że relacja odwrotna nigdy nie jest niezbędna. Niezbędna jest jedynie relacja główna. Relacja odwrotna może być wykorzystywana dla wygody. W tym przypadku pozwalała ona w prosty sposób uzyskać właściciela adresu. Relację odwrotną zawsze można zastąpić zapytaniem JPQL. Pokażemy to w poniższym przykładzie.
Programy testowe zostały przejęte bez zmian. Interesuje nas wyłącznie test 7, w którym widzieliśmy relację odwrotną typu „jeden do jednego” w działaniu. Dodajemy ponadto test 8, aby pokazać, jak bez relacji odwrotnej Adres -> Osoba można mimo to uzyskać osobę posiadającą dany adres.
Test 7 pozostaje bez zmian. Jego wykonanie daje teraz następujące wyniki (logi wyłączone):
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- tym razem rzeczywiście pojawia się oczekiwany wyjątek: ten wygenerowany przez sterownik JDBC, ponieważ próbowano usunąć z tabeli [adresse] wiersz, do którego odwołuje się klucz obcy z wiersza w tabeli [personne]. Wiersz [10] jasno wskazuje przyczynę błędu.
- Cofnięcie zmian przebiegło pomyślnie: po zakończeniu testu 7 tabela [adresse] (wiersze 12–13) jest taka sama, jak po zakończeniu testu 6 (wiersze 4–5).
Czym różni się to od testu 7 z poprzedniego projektu Eclipse? Dlaczego w tym przypadku pojawia się wyjątek JDBC, którego nie wystąpił w poprzednim teście? Ponieważ @Entity [Adresse] nie ma już odwrotnej relacji jeden do jednego z @Entity [Personne], jest ona zarządzana przez Hibernate w sposób izolowany. Kiedy adres newa1 został wprowadzony do kontekstu trwałości, Hibernate nie umieścił w tym kontekście również osoby p1 posiadającej ten adres. Usunięcie adresów newa1 i newa4 nastąpiło zatem bez obecności encji Personne w kontekście.
Jak zatem na podstawie adresu newa1 można by uzyskać osobę p1, której przypisano ten adres? To uzasadnione pytanie. Odpowiedź na nie zawiera poniższy test 8:
// odwrotna relacja jeden do jednego
// zrealizowana za pomocą zapytania JPQL
public static void test8() {
EntityTransaction tx = null;
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
tx = em.getTransaction();
tx.begin();
// adres a1 zostaje ponownie przypisany do nowego kontekstu
newa1 = em.find(Adresse.class, a1.getId());
// pobieramy dane osoby będącej właścicielem tego adresu
Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
.getSingleResult();
// są wyświetlane
System.out.println("adresse=" + newa1);
System.out.println("personne=" + p1);
// koniec transakcji
tx.commit();
}
- wiersz 6: nowy, pusty kontekst trwałości
- wiersze 8–9: początek transakcji
- wiersz 11: adres a1 zostaje wprowadzony do kontekstu trwałości i odwołuje się do niego newa1.
- wiersz 13: pobierana jest osoba p1 o adresie newa1 za pomocą zapytania JPQL. Wiadomo, że [Personne] i [Adresse] są powiązane relacją klucza obcego. W klasie [Personne] to pole [adresse], opatrzone adnotacją @OneToOne, urzeczywistnia tę relację. Zapis JPQL „select p from Personne p join p.adresse a” realizuje połączenie między tabelami [personne] i [adresse]. Odpowiednik SQL wygenerowany w konsoli Hibernate (por. przykłady z paragrafu 2.1.12) wygląda następująco:
Wyraźnie widać połączenie obu tabel. Każda osoba jest teraz powiązana ze swoim adresem. Pozostaje jeszcze sprecyzować, że interesuje nas wyłącznie adres newa1. Zapytanie przyjmuje postać: „select p from Personne p join p.adresse a where a.id=:adresseId”. Warto zwrócić uwagę na użycie aliasów p i a. Zapytania dotyczące JPQL intensywnie wykorzystują aliasy. W ten sposób wyrażenie „from Personne p join p.adresse a” powoduje, że dana osoba jest reprezentowana przez alias p, a jej adres (p.adresse) przez alias a. Operacja ograniczenia „where a.id=:adresseId” ogranicza żądane wiersze wyłącznie do osób o aliasie p, których adres ma wartość:adresseId jako identyfikator ich adresu a. :adresseId nazywany jest parametrem, a polecenie JPQL – poleceniem z parametrami JPQL. Podczas wykonywania ten parametr musi otrzymać wartość. Jest to metoda
umożliwia nadanie wartości parametrowi zidentyfikowanemu przez jego nazwę. Należy zauważyć, że metoda setParameter zwraca obiekt Query, podobnie jak metoda createQuery. Dzięki temu można łączyć wywołania metod [em.createQuery(...).setParameter(...).getSingleResult(...)], przy czym metody [setParameter, getSingleResult] są metodami interfejsu Query. Metoda [getSingleResult] jest używana w przypadku zapytań Select, które zwracają tylko jeden wynik. Tak jest w tym przypadku.
- wiersze 16–17: wyświetlany jest adres newa1 oraz osoba p1 posiadająca ten adres, w celu weryfikacji.
Uzyskany wynik jest następujący:
Jest poprawny. Z tego przykładu wynika, że odwrotna relacja „jeden do jednego” między @entity [Adresse] a @entity [Personne] nie była niezbędna. Doświadczenie pokazało w tym przypadku, że jej usunięcie spowodowało bardziej przewidywalne zachowanie kodu. Tak jest często.
2.3.8. Konsola Hibernate
W poprzednim teście nr 8 użyto polecenia JPQL w celu wykonania połączenia między encjami Personne i Adresse. Chociaż są one analogiczne do języka SQL, języki JPQL, JPA lub HQL w Hibernate wymagają nauki, a konsola Hibernate doskonale się do tego nadaje. Korzystaliśmy z niej już w punkcie 2.1.12, aby przetwarzać pojedynczą tabelę. Teraz powtórzymy tę procedurę, aby przetwarzać dwie tabele połączone relacją klucza obcego.
Utwórzmy konsolę Hibernate dla naszego bieżącego projektu Eclipse:
![]() |
- [1]: przechodzimy do perspektywy [Hibernate Console] (Window / Open Perspective / Other)
- [2]: tworzymy nową konfigurację
- za pomocą przycisku [4], wybieramy projekt Java, dla którego tworzona jest konfiguracja Hibernate. Jego nazwa wyświetla się w polu [3].
- W [5] nadajemy tej konfiguracji wybraną nazwę. W tym przypadku wykorzystaliśmy nazwę projektu Java.
- W oknie [6] wskazujemy, że korzystamy z konfiguracji JPA, aby narzędzie wiedziało, że ma wykorzystać plik [META-INF/persistence.xml]
- W pliku [7]: w tym pliku wskazujemy, że w pliku [META-INF/persistence.xml] należy użyć jednostki trwałości o nazwie jpa.
- W pliku [8] zatwierdzamy konfigurację.
Następnie należy uruchomić plik SGBD. Tutaj chodzi o plik MySQL5.
![]() |
- w [1]: utworzona konfiguracja ma drzewo z trzema gałęziami
- w [2]: gałąź [Configuration] zawiera listę obiektów, których konsola użyła do skonfigurowania się: w tym przypadku są to @Entity Personne i Adresse.
- w [3]: Session Factory to pojęcie z Hibernate zbliżone do EntityManager z JPA. Realizuje ona pomost obiektowo-relacyjny dzięki obiektom z gałęzi [Configuration]. W [3] przedstawiono obiekty kontekstu trwałości, w tym przypadku ponownie @Entity Personne i Adresse.
- w [4]: baza danych, do której dostęp uzyskuje się za pomocą konfiguracji zawartej w [persistence.xml]. Znajdują się w niej tabele [jpa04_hb_*] wygenerowane przez nasz obecny projekt Eclipse.
![]() |
- w [1] tworzymy edytor HQL
- w edytorze HQL,
- w [2] wybieramy konfigurację Hibernate, której chcemy użyć, jeśli jest ich kilka (tak jest w tym przypadku)
- w edytorze [3] wpisujemy polecenie JPQL, które chcemy wykonać, w tym przypadku polecenie JPQL z testu 8
- w [4] uruchamia się je
- w [5] otrzymujemy wyniki zapytania w oknie [Hibernate Query Result].
- w [6] okno [Hibernate Dynamic SQL preview] pozwala zobaczyć zapytanie SQL, które zostało odtworzone.
Inny sposób uzyskania tego samego wyniku:
![]() |
- w [1]: polecenie JPQL wykonujące połączenie encji Personne i Adresse. [ref1] nazywa tę formę „połączeniem theta”.
- w [2]: odpowiednik SQL
- w [3]: wynik
Trzecia forma akceptowana wyłącznie przez Hibernate (HQL):
![]() |
- w [1]: polecenie HQL. JPQL nie akceptuje notacji p.adresse.id. Akceptuje tylko jeden poziom pośrednictwa.
- w [2]: odpowiednik SQL. Widać, że unika on połączenia między tabelami.
- w [3]: wynik
Oto inne przykłady:
![]() |
- w [1]: lista osób wraz z ich adresami
- w [2]: odpowiednik SQL.
- w formacie [3]: wynik
![]() |
- w pliku [1]: lista adresów wraz z ich właścicielami, jeśli tacy istnieją, a w przeciwnym razie – brak (prawa zewnętrzna łączność: jednostka Adresse, która dostarczy wiersze niepowiązane z Personne, znajduje się po prawej stronie słowa kluczowego join).
- w [2]: odpowiednik SQL.
- w [3]: wynik
Należy zauważyć, że tylko jednostka Personne ma relację z jednostką Adresse. Odwrotna sytuacja nie ma już miejsca od czasu usunięcia odwrotnej relacji jeden do jednego o nazwie personne w jednostce Adresse. Gdyby ta odwrotna relacja istniała, można by zapisać:
![]() |
- w [1]: lista adresów wraz z ich właścicielami, jeśli tacy istnieją, lub brak właściciela w przeciwnym razie (lewe połączenie zewnętrzne: jednostka Adresse, która dostarczy wiersze niepowiązane z Personne, znajduje się po lewej stronie słowa kluczowego join).
- w [2]: odpowiednik SQL.
- w [3]: wynik
Gorąco zachęcamy czytelnika do ćwiczenia języka JPQL za pomocą konsoli Hibernate.
2.3.9. Implementacja JPA / Toplink
Korzystamy teraz z implementacji JPA / Toplink:
![]() |
Nowy projekt testowy w Eclipse wygląda następująco:
![]() |
Kod Java jest identyczny jak w poprzednim projekcie Hibernate. Środowisko (biblioteki – persistence.xml – system zarządzania bazami danych – foldery conf, ddl – skrypt ant) jest takie samo jak omówione w punkcie 2.1.15.2. Projekt Eclipse o nazwie [3] znajduje się w folderze przykładów [4]. Zaimportujemy go.
Plik <persistence.xml> został zmodyfikowany w jednym miejscu, a mianowicie w zakresie zadeklarowanych elementów:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- dostawca -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- klasy trwałe -->
<class>entites.Personne</class>
<class>entites.Adresse</class>
<!-- właściwości jednostki trwałości -->
...
- wiersze 5 i 6: dwie obsługiwane jednostki
Uruchomienie programu [InitDB] wraz z plikami SGBD i MySQL5 daje następujące wyniki:
![]() |
W [1], wyświetlacz konsoli, w [2] – dwie wygenerowane tabele [jpa04_tl], a w [3] – wygenerowane skrypty SQL. Ich zawartość jest następująca:
create.sql
CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.4. Przykład 4: relacja „jeden do wielu”
2.4.1. Schemat bazy danych „ ”
1 ![]() | 2 |
- w [1] – baza danych, a w [2] – jej DDL (MySQL5)
Artykuł A(id, wersja, nazwa) należy dokładnie do jednej kategorii C(id, wersja, nazwa). Kategoria C może zawierać 0, 1 lub więcej artykułów. Mamy tu relację „jeden do wielu” (Kategoria -> Artykuł) oraz odwrotną relację „wiele do jednego” (Artykuł -> Kategoria). Relacja ta jest odzwierciedlona przez klucz obcy, który tabela [article] posiada w odniesieniu do tabeli [categorie] (wiersze 24–28 w tabeli DDL).
2.4.2. Obiekty @Entity reprezentujące bazę danych
Artykuł jest reprezentowany przez następującą instancję @Entity [Article]:
package entites;
...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {
// pola
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// główna relacja Artykuł (wiele) -> Kategoria (jeden)
// zrealizowana za pomocą klucza obcego (categorie_id) w klasie Artykuł
// 1 Artykuł musi mieć 1 Kategorię (nullable=false)
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
// konstruktory
public Article() {
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
}
}
- wiersze 9–11: klucz podstawowy @Entity
- wiersze 13–15: numer wersji
- wiersze 17–18: nazwa artykułu
- wiersze 20–25: relacja „wiele do jednego”, łącząca @Entity Article z @Entity Categorie:
- wiersz 23: adnotacja ManyToOne. „Many” odnosi się do @Entity Article, w którym się znajdujemy, a „One” do @Entity Categorie (wiersz 25). Jedna kategoria (One) może mieć wiele artykułów (Many).
- wiersz 24: adnotacja ManyToOne definiuje kolumnę klucza obcego w tabeli [article]. Będzie ona nosić nazwę (name) categorie_id, a każdy wiersz musi zawierać wartość w tej kolumnie (nullable=false).
- wiersz 25: kategoria, do której należy artykuł. Gdy artykuł zostanie umieszczony w kontekście trwałości, żądamy, aby jego kategoria nie była tam umieszczana od razu (fetch=FetchType.LAZY, wiersz 23). Nie wiadomo, czy to żądanie ma sens. Zobaczymy.
Kategoria jest reprezentowana przez następującą @Entity [Categorie]:
package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
// pola
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// odwrotna relacja Kategoria (jeden) -> Artykuł (wiele) w ramach relacji Artykuł (wiele) -> Kategoria (jeden)
// kaskadowe wstawianie Kategoria -> wstawianie Artykułów
// kaskadowa aktualizacja Kategoria -> aktualizacja Artykułów
// kaskadowe usuwanie: Kategoria -> usuwanie artykułów
@OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
private Set<Article> articles = new HashSet<Article>();
// konstruktory
public Categorie() {
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
return String.format("Categorie[%d,%d,%s]", id, version, nom);
}
// powiązanie dwukierunkowe Kategoria <--> Artykuł
public void addArticle(Article article) {
// artykuł zostaje dodany do zbioru artykułów w kategorii
articles.add(article);
// artykuł zmienia kategorię
article.setCategorie(this);
}
}
- wiersze 8–11: klucz podstawowy @Entity
- wiersze 12–14: jej wersja
- wiersze 16–17: nazwa kategorii
- wiersze 19–24: zbiór (set) artykułów należących do kategorii
- wiersz 23: adnotacja @OneToMany oznacza relację „jeden do wielu”. „One” oznacza @Entity [Categorie], w której się znajdujemy, a „Many” – typ [Article] z wiersza 24: jedna (One) kategoria ma wiele (Many) artykułów.
- wiersz 23: adnotacja ta jest odwrotnością (mappedBy) adnotacji ManyToOne umieszczonej na polu categorie w @Entity Article: mappedBy=categorie. Relacja ManyToOne umieszczona na polu categorie w @Entity Article jest relacją główną. Jest ona niezbędna. Odzwierciedla ona relację klucza obcego, która łączy @Entity Article z @Entity Categorie. Relacja OneToMany, zdefiniowana na polu articles w @Entity Categorie, jest relacją odwrotną. Nie jest ona niezbędna. Jest to udogodnienie służące do pobierania artykułów z danej kategorii. Bez tego udogodnienia artykuły te byłyby pobierane za pomocą zapytania JPQL.
- wiersz 23: cascadeType.ALL wymaga, aby operacje (persist, merge, remove) wykonywane na @Entity Categorie były kaskadowo stosowane na jej artykułach.
- wiersz 24: elementy danej kategorii zostaną umieszczone w obiekcie typu Set<Article>. Typ Set nie dopuszcza duplikatów. Nie można więc umieścić dwa razy tego samego elementu w obiekcie Set<Article>. Co oznacza „ten sam element”? Aby stwierdzić, że element a jest taki sam jak element b, Java używa wyrażenia a.equals(b). W klasie Object, będącej klasą nadrzędną wszystkich klas, wyrażenie a.equals(b) jest prawdziwe, jeśli a==b, c.a.d. jeśli obiekty a i b zajmują tę samą lokalizację w pamięci. Można by uznać, że przedmioty a i b są takie same, jeśli mają tę samą nazwę. W takim przypadku programista musi przedefiniować dwie metody w klasie [Article]:
- equals: która musi zwracać wartość true, jeśli oba elementy mają tę samą nazwę
- hashCode: musi zwracać identyczną wartość całkowitą dla dwóch obiektów [Article], które metoda equals uznaje za równe. W tym przypadku wartość ta będzie zatem tworzona na podstawie nazwy elementu. Wartość zwracana przez hashCode może być dowolną liczbą całkowitą. Jest ona wykorzystywana w różnych kontenerach obiektów, w szczególności w słownikach (Hashtable).
Relacja OneToMany może wykorzystywać inne typy niż Set do przechowywania elementu „Many”, na przykład obiekty typu List. Nie będziemy omawiać tych przypadków w niniejszym dokumencie. Czytelnik znajdzie je w [ref1].
- wiersz 38: metoda [addArticle] pozwala nam dodać artykuł do kategorii. Metoda ta dba o aktualizację obu końców relacji OneToMany, która łączy [Categorie] z [Article].
2.4.3. Projekt Eclipse / Hibernate 1
Wykorzystana tutaj implementacja JPA pochodzi z biblioteki Hibernate. Projekt Eclipse zawierający testy wygląda następująco:
![]() |
Projekt ten znajduje się pod nazwą [3] w folderze przykładów [4]. Zaimportujemy go.
2.4.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami z punktu 2.1.7, plik DDL uzyskany dla plików SGBD i MySQL5 jest tym samym plikiem, który pokazano na początku tego przykładu, w punkcie 2.4.1.
2.4.5. InitDB
Kod dla [InitDB] jest następujący:
package tests;
...
public class InitDB {
// stałe
private final static String TABLE_ARTICLE = "jpa05_hb_article";
private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";
public static void main(String[] args) {
// Kontekst trwałości
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// pobieramy EntityManager z poprzedniego EntityManagerFactory
em = emf.createEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zapytanie
Query sql1;
// usunięcie elementów z tabeli ARTICLE
sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
sql1.executeUpdate();
// usunięcie elementów z tabeli CATEGORIE
sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
sql1.executeUpdate();
// utwórz trzy kategorie
Categorie categorieA = new Categorie();
categorieA.setNom("A");
Categorie categorieB = new Categorie();
categorieB.setNom("B");
Categorie categorieC = new Categorie();
categorieC.setNom("C");
// utworzyć 3 artykuły
Article articleA1 = new Article();
articleA1.setNom("A1");
Article articleA2 = new Article();
articleA2.setNom("A2");
Article articleB1 = new Article();
articleB1.setNom("B1");
// przypisać je do odpowiednich kategorii
categorieA.addArticle(articleA1);
categorieA.addArticle(articleA2);
categorieB.addArticle(articleB1);
// zapisać kategorie i kaskadowo (wstawić) artykuły
em.persist(categorieA);
em.persist(categorieB);
em.persist(categorieC);
// wyświetlenie kategorii
System.out.println("[categories]");
for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
System.out.println(p);
}
// wyświetlenie artykułów
System.out.println("[articles]");
for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
System.out.println(p);
}
// koniec transakcji
tx.commit();
// koniec EntityManager
em.close();
// koniec EntityMangerFactory
emf.close();
// log
System.out.println("terminé...");
}
}
- wiersze 22–27: tabele [article] i [categorie] są opróżniane. Należy zauważyć, że konieczne jest rozpoczęcie od tabeli zawierającej klucz obcy. Gdybyśmy rozpoczęli od tabeli [categorie], usunęlibyśmy kategorie, do których odwołują się wiersze tabeli [article], a to spowodowałoby odrzucenie operacji przez tabelę SGBD.
- wiersze 29–34: tworzymy trzy kategorie A, B, C
- wiersze 36–41: tworzymy trzy artykuły: A1, A2, B1 (litera wskazuje kategorię)
- wiersze 43–45: trzy artykuły są umieszczane w odpowiednich kategoriach
- wiersze 47–49: trzy kategorie są umieszczane w kontekście trwałości. Ze względu na kaskadę „Kategoria → Artykuł” ich artykuły również zostaną tam umieszczone. Zatem wszystkie utworzone obiekty znajdują się teraz w kontekście trwałości.
- wiersze 50–59: kontekst trwałości jest wywoływany w celu uzyskania listy kategorii i artykułów. Wiadomo, że spowoduje to synchronizację kontekstu z bazą danych. W tym momencie kategorie i artykuły zostaną zapisane w odpowiednich tabelach.
Wykonanie programu [InitDB] wraz z programem MySQL5 daje następujące wyniki:
![]() |
- [1]: wyświetlanie w konsoli
- [2]: tabele [jpa05_hb_*] w widoku SQL Explorer
- [3]: tabela kategorii
- [4]: tabela artykułów. Należy zwrócić uwagę na powiązanie między tabelą [categorie_id] w widoku [4] a tabelą [id] w widoku [3] (klucz obcy).
2.4.6. Strona główna
Klasa [Main] zawiera sekwencję testów, które przeglądamy, z wyjątkiem testów 1 i 2, które wykorzystują kod z klasy [InitDB] do zainicjowania bazy danych.
2.4.6.1. Test3
Ten test wygląda następująco:
// wyszukaj konkretny element
public static void test3() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// ładowanie kategorii
Categorie categorie = em.find(Categorie.class, categorieA.getId());
// wyświetlanie kategorii i powiązanych z nią artykułów
System.out.format("Articles de la catégorie %s :%n", categorie);
for (Article a : categorie.getArticles()) {
System.out.println(a);
}
// zakończenie transakcji
tx.commit();
}
- wiersz 4: mamy nowy, a więc pusty kontekst trwałości
- wiersze 6–7: początek transakcji
- wiersz 9: kategoria A jest pobierana z bazy danych do kontekstu trwałości
- wiersz 11: wyświetlana jest kategoria A
- wiersze 12–14: wyświetlane są artykuły z kategorii A. Widać tu zalety relacji odwrotnej OneToMany – artykuły z @Entity Categorie. Jej obecność pozwala nam uniknąć wykonywania zapytania JPQL w celu pobrania artykułów z kategorii A. Aby je uzyskać, wykorzystujemy metodę get pola articles.
Wyniki są następujące:
- wiersz 20: kategoria A
- wiersze 21–22: oba artykuły z kategorii A
2.4.6.2. Test4
Ten test wygląda następująco:
// usunięcie produktu
@SuppressWarnings("unchecked")
public static void test4() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// ładowanie pozycji A1
Article newarticle1 = em.find(Article.class, articleA1.getId());
// usunięcie artykułu A1 (obecnie nie załadowano żadnej kategorii)
em.remove(newarticle1);
// toplink: artykuł musi zostać usunięty z kategorii, w przeciwnym razie test6 ulegnie awarii
// hibernate: nie jest to konieczne
newarticle1.getCategorie().getArticles().remove(newarticle1);
// koniec transakcji
tx.commit();
// zrzut artykułów
dumpArticles();
}
- test 4 usuwa artykuł A1
- wiersz 5: zaczynamy od nowego, pustego kontekstu
- wiersz 10: artykuł A1 zostaje przeniesiony do kontekstu trwałości. Będzie tam odwołany jako newarticle1.
- wiersz 12: jest on usuwany z kontekstu
- wiersz 15: kategorie A, B i C oraz artykuły A1, A2 i B1, nawet jeśli nie są już trwałe, pozostają jednak w pamięci. Są po prostu odłączone od kontekstu trwałości. Artykuł A1, który należy do artykułów kategorii A, zostaje z niej usunięty. Umożliwi to w przyszłości ponowne przypisanie kategorii A do kontekstu trwałości. Jeśli tego nie zrobimy, kategoria A zostanie przypisana do zbioru artykułów, z których jeden został usunięty. Nie wydaje się to przeszkadzać Hibernate, ale powoduje awarię Toplink.
- Wiersz 19: wyświetlamy wszystkie artykuły, aby sprawdzić, czy A1 zniknął.
Wyniki są następujące:
Artykuł A1 rzeczywiście zniknął.
2.4.6.3. Test5
Test ten wygląda następująco:
// modyfikacja 1 artykułu
public static void test5() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// zmiana articleA2
articleA2.setNom(articleA2.getNom() + "-");
// articleA2 zostaje przywrócony do kontekstu trwałości
em.merge(articleA2);
// koniec transakcji
tx.commit();
// zrzut pozycji
dumpArticles();
}
- test 5 zmienia nazwę artykułu A2
- wiersz 4: zaczynamy od nowego, pustego kontekstu
- wiersz 9: zmieniamy nazwę odłączonego elementu A2, który stanie się „A2-”.
- wiersz 11: odłączony element A2 zostaje ponownie przyłączony do kontekstu trwałości. Należy zauważyć, że A2 pozostaje nadal obiektem odłączonym. To właśnie obiekt em.merge (articleA2) stanowi teraz część kontekstu trwałości. Obiekt ten nie został tutaj zapisany w zmiennej, jak to zwykle bywa. Jest zatem niedostępny.
- wiersz 13: synchronizacja kontekstu trwałości z bazą danych. Artykuł A2 zostanie zmodyfikowany w bazie danych, a jego numer wersji zmieni się z N na N+1. Oddzielona wersja pamięciowa articleA2 nie jest już ważna. To samo dotyczy obiektu oddzielonego reprezentującego kategorię A, ponieważ zawiera on articleA2 wśród swoich artykułów.
- wiersz 15: wyświetlane są wszystkie pozycje w celu sprawdzenia zmiany nazwy pozycji A2
Wyniki są następujące:
Nazwa pozycji A2 została rzeczywiście zmieniona.
2.4.6.4. Test6
Test ten wygląda następująco:
// modyfikacja jednej kategorii i jej artykułów
public static void test6() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// wczytanie kategorii
categorieA = em.find(Categorie.class, categorieA.getId());
// lista artykułów z kategorii A
for (Article a : categorieA.getArticles()) {
a.setNom(a.getNom() + "-");
}
// zmiana nazwy kategorii
categorieA.setNom(categorieA.getNom() + "-");
// zakończenie transakcji
tx.commit();
// zrzut kategorii i artykułów
dumpCategories();
dumpArticles();
}
- test 6 zmienia nazwę kategorii A i wszystkich jej artykułów
- wiersz 4: zaczynamy od nowego, pustego kontekstu
- wiersz 9: wyszukujemy kategorię A w bazie danych. Nie wykonujemy operacji merge na oddzielonym obiekcie categorieA, ponieważ wiemy, że zawiera on odniesienie do artykułu A2, który stał się nieaktualny. Zaczynamy więc od zera.
- wiersze 11–12: zmieniamy nazwy wszystkich artykułów z kategorii A. Ponownie wykorzystujemy relację odwrotną OneToMany za pomocą metody getArticles.
- wiersz 15: nazwa kategorii również zostaje zmieniona
- wiersz 17: koniec transakcji. Przeprowadzana jest synchronizacja kontekstu z bazą danych. Wszystkie obiekty kontekstu, które zostały zmodyfikowane, zostaną zaktualizowane w bazie danych.
- wiersze 21–22: wyświetlane są artykuły i kategorie w celu weryfikacji
Wyniki są następujące:
Nazwa artykułu A2 została ponownie zmieniona, podobnie jak nazwa kategorii A.
2.4.6.5. Test7
Test ten wygląda następująco:
// usunięcie kategorii
public static void test7() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// zapisanie danych catégorieB oraz kaskadowe (merge) usunięcie powiązanych artykułów
Categorie mergedcategorieB = em.merge(categorieB);
// usunięcie kategorii oraz kaskadowo (delete) powiązanych artykułów
em.remove(mergedcategorieB);
// zakończenie transakcji
tx.commit();
// wyeksportowanie kategorii i artykułów
dumpCategories();
dumpArticles();
}
- Test 7 usuwa kategorię B, a co za tym idzie – również artykuły z tej kategorii
- wiersz 4: zaczynamy od nowego, pustego kontekstu
- wiersz 9: kategoria B istnieje w pamięci jako obiekt oddzielony od kontekstu trwałości. Ponownie włączamy ją (merge) do kontekstu trwałości. W konsekwencji jej artykuły (artykuł B1) zostaną poddane operacji merge, a tym samym ponownie włączone do kontekstu trwałości.
- wiersz 11: teraz, gdy kategoria B znajduje się w kontekście, można ją usunąć (remove). W wyniku efektu kaskadowego jej artykuły również zostaną poddane operacji remove. Operacja ta jest możliwa, ponieważ operacja merge z wiersza 9 przywróciła je do kontekstu trwałości.
- wiersz 13: koniec transakcji. Kontekst zostanie zsynchronizowany. Obiekty kontekstu, które przeszły operację remove, zostaną usunięte z bazy danych.
- wiersze 15–16: wyświetlane są artykuły i kategorie w celu weryfikacji
Wyniki są następujące:
Kategoria B i artykuł B1 rzeczywiście zniknęły.
2.4.6.6. Test8
Test ten wygląda następująco:
// zapytania
@SuppressWarnings("unchecked")
public static void test8() {
// nowy kontekst trwałości
EntityManager em = getNewEntityManager();
// transakcja
EntityTransaction tx = em.getTransaction();
tx.begin();
// lista produktów z kategorii A
List articles = em
.createQuery(
"select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
.getResultList();
// wyświetlenia artykułów
System.out.println("Articles de la catégorie A");
for (Object a : articles) {
System.out.println(a);
}
// zakończenie transakcji
tx.commit();
}
- Test 7 pokazuje, jak pobrać artykuły z kategorii bez korzystania z relacji odwrotnej. Świadczy to o tym, że relacja ta nie jest zatem niezbędna.
- wiersz 4: zaczynamy od nowego, pustego kontekstu
- wiersz 10: zapytanie JPQL, które pobiera wszystkie artykuły z kategorii, których nazwa zaczyna się na literę A
- wiersze 15–17: wyświetlenie wyniku zapytania.
Wyniki są następujące:
2.4.7. Projekt Eclipse / Hibernate 2
Kopiujemy i wklejamy projekt Eclipse / Hibernate, aby wyjaśnić kwestię relacji głównej i relacji odwrotnej, które utworzyliśmy w oparciu o adnotację @ManyToOne (główna) dla @Entity [Article] oraz relacji odwrotnej @OneToMany (odwrotna) dla @Entity [Categorie]. Chcemy pokazać, że jeśli ta ostatnia relacja nie zostanie zadeklarowana jako odwrotna względem drugiej, wówczas schemat wygenerowany dla bazy danych będzie zupełnie inny niż ten wygenerowany wcześniej.
![]() |
W [1] znajduje się nowy projekt Eclipse. W pliku [2] znajduje się kod Java, a w pliku [3] skrypt ant, który wygeneruje schemat bazy danych SQL. Projekt znajduje się w folderze przykładów [5] pod nazwą [4]. Zaimportujemy go.
Modyfikujemy wyłącznie @Entity o nazwie [Categorie], tak aby jej relacja @OneToMany z @Entity [Article] nie była już deklarowana jako odwrotna w stosunku do relacji @ManyToOne, jaką @Entity [Article] ma z @Entity [Categorie]:
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
// pola
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// relacja OneToMany nieodwrotna (brak mappedby) Kategoria (one) -> Artykuł (many)
// zrealizowana za pomocą tabeli łączącej Categorie_Article, tak aby na podstawie kategorii
// można było dotrzeć do artykułów z tej kategorii
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
// producentów
...
- wiersze 18–22: chcemy nadal zachować możliwość wyszukiwania artykułów z danej kategorii za pomocą relacji @OneToMany z wiersza 21. Chcemy jednak sprawdzić wpływ atrybutu mappedBy, który sprawia, że relacja staje się odwrotnością relacji głównej zdefiniowanej gdzie indziej, w innym @Entity. W tym przypadku usunięto mappedBy.
Uruchamiamy zadanie ant-DLL (patrz punkt 2.1.7) z atrybutami SGBD i MySQL5. Otrzymany schemat wygląda następująco:
![]() |
Należy zwrócić uwagę na następujące kwestie:
- utworzono nową tabelę o nazwie [categorie_article] [1]. Wcześniej nie istniała.
- Jest to tabela łącząca tabele [categorie] [2] oraz [article] [3]. Jeśli obiekty Artykuł a1 i a2 należą do kategorii c1, w tabeli łączącej znajdą się następujące wiersze:
gdzie c1, a1, a2 są kluczami głównymi odpowiadających im obiektów.
- Tabela połączeń [categorie_article] [1] została utworzona przez Hibernate, aby na podstawie obiektu Categorie c można było odnaleźć obiekty Article a należące do c. To relacja @OneToMany wymusiła utworzenie tej tabeli. Ponieważ nie zadeklarowano jej jako odwrotnej względem relacji głównej @ManyToOne dla @Entity Article, Hibernate nie wiedział, że może wykorzystać tę relację główną do pobrania artykułów z kategorii c. Znajdował więc inne rozwiązanie.
- Dzięki temu przykładowi łatwiej zrozumieć pojęcia relacji principale i inverse. Jedna z nich (odwrotna) wykorzystuje właściwości drugiej (głównej).
Schemat SQL tej bazy danych dla MySQL5 wygląda następująco:
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D26D17756;
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D424C61C9;
alter table jpa06_hb_article
drop
foreign key FK4547168FECCE8750;
drop table if exists jpa05_hb_categorie;
drop table if exists jpa05_hb_categorie_jpa06_hb_article;
drop table if exists jpa06_hb_article;
create table jpa05_hb_categorie (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
primary key (id)
) ENGINE=InnoDB;
create table jpa05_hb_categorie_jpa06_hb_article (
jpa05_hb_categorie_id bigint not null,
articles_id bigint not null,
primary key (jpa05_hb_categorie_id, articles_id),
unique (articles_id)
) ENGINE=InnoDB;
create table jpa06_hb_article (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
categorie_id bigint not null,
primary key (id)
) ENGINE=InnoDB;
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id),
add constraint FK79D4BA1D26D17756
foreign key (jpa05_hb_categorie_id)
references jpa05_hb_categorie (id);
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D424C61C9 (articles_id),
add constraint FK79D4BA1D424C61C9
foreign key (articles_id)
references jpa06_hb_article (id);
alter table jpa06_hb_article
add index FK4547168FECCE8750 (categorie_id),
add constraint FK4547168FECCE8750
foreign key (categorie_id)
references jpa05_hb_categorie (id);
- wiersze 19–24: utworzenie tabeli [categorie] oraz wiersze 33–39: utworzenie tabeli [article]. Należy zauważyć, że są one identyczne jak w poprzednim przykładzie.
- wiersze 26–31: utworzenie tabeli połączeń [categorie_article] wynikające z obecności relacji nieodwrotnej @OneToMany w @Entity Categorie. Wiersze tej tabeli mają typ [c,a], gdzie c jest kluczem głównym kategorii c, a a jest kluczem głównymartykułu a należącego do kategorii c. Klucz podstawowy tej tabeli łączącej składa się z dwóch połączonych kluczy podstawowych [c,a] (wiersz 29).
- wiersze 41–45: ograniczenie klucza obcego z tabeli [categorie_article] do tabeli [categorie]
- wiersze 47–51: ograniczenie klucza obcego z tabeli [categorie_article] do tabeli [article]
- wiersze 53–57: ograniczenie klucza obcego z tabeli [article] do tabeli [categorie]
Zachęcamy czytelnika do przeprowadzenia testów [InitDB] i [Main]. Dają one takie same wyniki jak poprzednio. Schemat bazy danych jest jednak nadmiarowy, a wydajność ulegnie pogorszeniu w porównaniu z poprzednią wersją. Niewątpliwie należałoby dokładniej zbadać kwestię relacji odwrotnych i głównych, aby sprawdzić, czy nowa konfiguracja nie powoduje dodatkowo konfliktów wynikających z faktu, że mamy dwie niezależne relacje reprezentujące to samo: relację wiele-do-jednego, jaką tabela [article] ma z tabelą [categorie].
2.4.8. Implementacja JPA / Toplink – 1
Obecnie korzystamy z implementacji JPA / Toplink:
![]() |
Projekt Eclipse z Toplinkiem jest kopią projektu Eclipse z Hibernate, wersja 1:
![]() |
Kod Java jest identyczny z kodem z poprzedniego projektu Hibernate – wersja 1. Środowisko (biblioteki – persistence.xml – system zarządzania bazami danych – foldery conf, ddl – skrypt ant) jest takie samo jak omówione w paragrafie 2.1.15.2. Projekt Eclipse o nazwie [3] znajduje się w folderze przykładów [4]. Należy go zaimportować.
Plik <persistence.xml> [2] został zmodyfikowany w jednym miejscu, a mianowicie w zakresie zadeklarowanych encji:
...
<!-- trwałe klasy -->
<class>entites.Categorie</class>
<class>entites.Article</class>
...
- wiersze 3 i 4: obie zarządzane jednostki
Uruchomienie programu [InitDB] wraz z programami SGBD i MySQL5 daje następujące wyniki:
![]() |
W [1], wyświetlany jest komunikat konsoli, w [2] – dwie wygenerowane tabele [jpa05_tl], a w [3] – wygenerowane skrypty SQL. Ich zawartość jest następująca:
create.sql
CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
Uruchomienie skryptu [Main] przebiega bezbłędnie.
2.4.9. Implementacja JPA / Toplink – 2
Ten projekt Eclipse powstał poprzez skopiowanie poprzedniego. Ponieważ został on zrealizowany przy użyciu Hibernate, usuwamy atrybut mappedBy z relacji @OneToMany w @Entity Categorie.
@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {
// pola
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private int version;
@Column(length = 30)
private String nom;
// relacja OneToMany nieodwrotna (brak mappedby) Kategoria (one) ->
// Artykuł (wiele)
// zrealizowana za pomocą tabeli łączącej Categorie_Article, tak aby na podstawie
// kategorii
// można uzyskać dostęp do wielu artykułów
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
Schemat SQL wygenerowany dla MySQL5 wygląda zatem następująco:
create.sql
CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- wiersz 2: tabela połączeń, która odzwierciedla poprzednią relację @OneToMany, która nie jest odwrotna.
Wykonanie [InitDB] przebiega bez błędów, ale wykonanie [Main] kończy się awarią na teście 7, generując następujące logi (FINEST):
- wiersz 3: merge w kategorii B
- wiersz 4: artykuł zależny B1 został umieszczony w kontekście
- wiersz 5: to samo dotyczy samej kategorii B
- wiersz 6: remove w kategorii B
- wiersz 7: remove dla artykułu B1 (kaskadowo)
- wiersz 8: kod Java żąda kodu commit z transakcji
- wiersz 9: rozpoczyna się transakcja – najwyraźniej wcześniej nie była uruchomiona.
- wiersz 10: pozycja B1 zostanie usunięta przez operację DELETE w tabeli [article]. Właśnie w tym tkwi problem. Tabela połączeniowa [categorie_article] zawiera odwołanie do wiersza B1 w tabeli [article]. Usunięcie wiersza B1 z tabeli [article] spowoduje naruszenie ograniczenia klucza obcego.
- wiersze 13 i kolejne: występuje wyjątek
Jakie wnioski?
- ponownie mamy do czynienia z problemem przenoszalności między Hibernate a Toplink: Hibernate pomyślnie przeszedł ten test
- Toplink nie radzi sobie dobrze z sytuacją, w której dwie relacje są w rzeczywistości odwrotne względem siebie, a jedna z nich nie jest zadeklarowana jako główna, a druga jako odwrotna. Można to zaakceptować, ponieważ ten przypadek stanowi w rzeczywistości błąd konfiguracji. W naszym przykładzie tabela [article] nie ma relacji z tabelą łączącą [categorie_article]. Wydaje się zatem naturalne, że podczas operacji na tabeli [article] Toplink nie próbuje korzystać z tabeli [categorie_article].
2.5. Przykład 5: relacja wiele-do-wielu z jawną tabelą łączącą
2.5.1. Schemat bazy danych
![]() |
- w [1], baza danych MySQL5
Znamy już tabele [personne], [2] oraz [adresse], [3]. Zostały one omówione w punkcie 2.3.1. Rozważamy wersję, w której adres osoby jest przedmiotem osobnej tabeli [adresse] i [3]. W tabeli [personne] relacja łącząca osobę z jej adresem jest wyrażona poprzez ograniczenie klucza obcego.
Osoba wykonuje określone czynności. Są one zapisane w tabelach [activite] i [4]. Jedna osoba może wykonywać wiele czynności, a jedna czynność może być wykonywana przez wiele osób. Tabele [personne] i [activite] łączy zatem relacja wiele-do-wielu. Relacja ta jest realizowana przez tabelę łączącą [personne_activite] [5].
2.5.2. Obiekty @Entity reprezentujące bazę danych
Powyższe tabele będą reprezentowane przez następujące obiekty @Entity:
- @Entity Personne będzie reprezentować tabelę [personne]
- @Entity Adresse będzie reprezentować tabelę [adresse]
- @Entity Activite będzie reprezentować tabelę [activite]
- @Entity PersonneActivite będzie reprezentować tabelę [personne_activite]
Relacje między tymi encjami są następujące:
- relacja jeden do jednego łączy encję Personne z encją Adresse: osoba p ma adres a. Entyteta Personne, która posiada klucz obcy, będzie miała relację główną, natomiast entyteta Adresse – relację odwrotną.
- Relacja wiele-do-wielu łączy encje Personne i Activite: jedna osoba wykonuje wiele czynności, a jedna czynność jest wykonywana przez wiele osób. Relację tę można by zrealizować bezpośrednio poprzez adnotację @ManyToMany w każdej z obu encji, przy czym jedna z nich zostałaby zadeklarowana jako odwrotna względem drugiej. Rozwiązanie to zostanie omówione w dalszej części. W tym przypadku relację wiele-do-wielu realizujemy za pomocą dwóch relacji jeden-do-wielu:
- relacja „jeden do wielu”, łącząca encję Personne z encją PersonneActivite: jeden wiersz (One) tabeli [personne] jest odwołany przez wiele (Many) wierszy tabeli [personne_activite]. Tabela [personne_activite], zawierająca klucz obcy, będzie posiadać relację główną @ManyToOne, a encja Personne – relację odwrotną @OneToMany.
- relacja typu „jeden do wielu”, łącząca encję Activite z encją PersonneActivite: jeden (One) wiersz tabeli [activite] jest odwołany przez wiele (Many) wierszy tabeli [personne_activite]. Tabela [personne_activite], zawierająca klucz obcy, będzie posiadać relację główną @ManyToOne, a encja Activite – relację odwrotną @OneToMany.
@Entity Personne ma następującą postać:
@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// główna relacja Osoba (one) -> Adres (one)
// zrealizowana przez klucz obcy Osoba (adresse_id) -> Adres
// wstawianie kaskadowe Osoba -> wstawianie Adres
// kaskadowa aktualizacja Osoba -> aktualizacja Adres
// kaskadowe usunięcie Osoby -> usunięcie Adresu
// każda osoba musi mieć 1 adres (nullable=false)
// 1 adres może należeć tylko do 1 osoby (unikalny=true)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// relacja Osoba (one) -> PersonneActivite (many)
// odwrotność istniejącej relacji PersonneActivite (wiele) -> Osoba (jeden)
// kaskadowe usunięcie Osoba -> usunięcie PersonneActivite
@OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();
// konstruktorzy
Ta @Entity jest znana. Omówimy jedynie relacje, jakie łączy ją z innymi encjami:
- wiersze 30–39: relacja jeden do jednego @OneToOne z @Entity Adresse, zrealizowana za pomocą klucza obcego [adresse_id] (wiersz 38), który tabela [personne] będzie posiadać w odniesieniu do tabeli [adresse].
- wiersze 41–45: relacja „jeden do wielu” @OneToMany z @Entity PersonneActivite. Jedna osoba (One) jest odwołana przez wiele (Many) wierszy w tabeli łączącej [personne_activite] reprezentowanej przez @Entity PersonneActivite. Obiekty PersonneActivite zostaną umieszczone w typie Set<PersonneActivite>, gdzie PersonneActivite jest typem, który wkrótce zdefiniujemy.
- wiersz 44: zdefiniowana tutaj relacja „jeden do wielu” jest relacją odwrotną do relacji głównej zdefiniowanej na polu personne w @Entity PersonneActivite (słowo kluczowe mappedBy). Mamy kaskadę „Osoba -> Działalność” w przypadku usuwania: usunięcie osoby p spowoduje usunięcie trwałych elementów typu PersonneActivite znajdujących się w zbiorze p.activites.
@Entity Adresse ma następującą postać:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// pola
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- wiersze 28–29: relacja @OneToOne, będąca relacją odwrotną do relacji @OneToOne, odnosi się do @Entity Personne (wiersze 37–38 w Personne).
@Entity Activite ma następujący wygląd
@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {
// pola
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// relacja Działalność (jeden) -> PersonneActivite (wiele)
// odwrotność istniejącej relacji PersonneActivite (wiele) -> Działalność (jeden)
// kaskadowe usunięcie „Aktywność” -> usunięcie PersonneActivite
@OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();
- wiersze 6–9: klucz podstawowy aktywności
- wiersze 11–13: numer wersji aktywności
- wiersze 15–16: nazwa aktywności
- wiersze 18–22: relacja „jeden do wielu”, łącząca @Entity Activite z @Entity PersonneActivite: jedna aktywność (One) jest odwołana przez wiele (Many) wierszy tabeli łączącej [personne_activite] reprezentowanej przez @Entity PersonneActivite. Obiekty PersonneActivite zostaną umieszczone w typie Set<PersonneActivite>.
- wiersz 22: zdefiniowana tutaj relacja „jeden do wielu” jest relacją odwrotną do relacji głównej zdefiniowanej na polu activite w @Entity PersonneActivite (słowo kluczowe mappedBy). W przypadku usunięć mamy kaskadę „Aktywność” → PersonneActivite: usunięcie tabeli [activite] zaktywności a spowoduje usunięcie tabeli łączącej [personne_activite] zawierającej elementy trwałe typu PersonneActivite znalezione w zbiorze a.personnes.
@Entity PersonneActivite ma następującą postać:
@Entity
// tabela połączeń
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {
@Embeddable
public static class Id implements Serializable {
// składniki klucza złożonego
// odwołuje się do osoby
@Column(name = "PERSONNE_ID")
private Long personneId;
// odnosi się do Działalności
@Column(name = "ACTIVITE_ID")
private Long activiteId;
// konstruktorzy
...
// metody pobierające i ustawiające
...
// toString
public String toString() {
return String.format("[%d,%d]", getPersonneId(), getActiviteId());
}
}
// pola klasy Personne_Activite
// klucz złożony
@EmbeddedId
private Id id = new Id();
// relacja główna PersonneActivite (wiele) -> Osoba (jedna)
// zrealizowana przez klucz obcy: personneId (PersonneActivite (wiele) -> Osoba (jedna)
// personneId jest jednocześnie elementem złożonego klucza głównego
// JPA nie musi obsługiwać tego klucza obcego (insertable = false, updatable = false), ponieważ robi to sama aplikacja w swoim konstruktorze
@ManyToOne
@JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
private Personne personne;
// relacja główna PersonneActivite -> Aktywność
// zrealizowana przez klucz obcy: activiteId (PersonneActivite (wiele) → Działalność (jeden)
// activiteId jest jednocześnie elementem złożonego klucza głównego
// JPA nie musi obsługiwać tego klucza obcego (insertable = false, updatable = false), ponieważ robi to sama aplikacja w swoim konstruktorze
@ManyToOne()
@JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
private Activite activite;
// konstruktory
public PersonneActivite() {
}
public PersonneActivite(Personne p, Activite a) {
// klucze obce są ustalane przez aplikację
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// powiązania dwukierunkowe
this.setPersonne(p);
this.setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
}
}
Ta klasa jest bardziej złożona niż poprzednie.
- Tabela [personne_activite] zawiera wiersze o postaci [p,a], gdzie p jest kluczem głównym osoby, a a kluczem głównym czynności. Każda tabela musi posiadać klucz główny, a [personne_activite] nie stanowi wyjątku od tej reguły. Do tej pory definiowaliśmy klucze główne generowane dynamicznie przez SGBD. Moglibyśmy to zrobić również w tym przypadku. Wykorzystamy jednak inną technikę, w której sama aplikacja definiuje wartości klucza głównego tabeli. W tym przypadku wiersz o numerze [p1,a1] oznacza, że osoba o numerze p1 uprawia aktywność o numerze a1. Ten sam wiersz nie może pojawić się w tabeli po raz drugi. Zatem para (p, a) jest dobrym kandydatem na klucz główny. Nazywamy to kluczem głównym złożonym.
- wiersze 30–31: klucz podstawowy złożony. Adnotacja @EmbeddedId (zwykle była to adnotacja @Id) jest analogiczna do notacji @Embedded zastosowanej do pola Adresse osoby. W tym ostatnim przypadku oznaczało to, że pole Adresse stanowiło obiekt klasy zewnętrznej, ale musiało zostać wstawione do tej samej tabeli co osoba. Tutaj znaczenie jest takie samo, z tą różnicą, że aby wskazać, iż mamy do czynienia z kluczem głównym, notacja zmienia się na @EmbeddedId.
- wiersz 31: pusty obiekt reprezentujący klucz główny id jest tworzony w momencie utworzenia obiektu [PersonneActivite]. Klasa reprezentująca klucz podstawowy jest zdefiniowana w wierszach 7–26 jako publiczna, statyczna klasa wewnętrzna klasy [PersonneActivite]. Fakt, że jest ona publiczna i statyczna, wynika z wymagań biblioteki Hibernate. Jeśli zastąpimy `public static` na `private,`, wystąpi wyjątek, a w powiązanym komunikacie o błędzie widać, że Hibernate próbował wykonać instrukcję `new PersonneActivite$Id`. Klasa Id musi więc być zarówno statyczna, jak i publiczna.
- Wiersz 6: klasa Id klucza głównego jest zadeklarowana jako @Embeddable. Przypomnijmy, że klucz główny id z wiersza 31 został zadeklarowany jako @EmbeddedId. Odpowiadająca mu klasa musi zatem posiadać adnotację @Embeddable.
- Stwierdziliśmy, że klucz główny tabeli [personne_activite] składa się z pary (p, a), gdzie p jest kluczem głównym osoby, a a – kluczem głównym czynności. Oba elementy (p, a) klucza złożonego znajdują się w wierszu 11 (personneId) oraz w wierszu 15 (activiteId). Kolumny powiązane z tymi dwoma polami noszą nazwy: PERSONNE_ID dla osoby oraz ACTIVITE_ID dla czynności.
- wiersz 31: zdefiniowano klucz główny wraz z dwiema kolumnami (PERSONNE_ID, ACTIVITE_ID). W tabeli [personne_activite] nie ma żadnych innych kolumn. Pozostaje tylko zdefiniować relacje istniejące między @Entity PersonneActivite, którą obecnie opisujemy, a innymi @Entity w schemacie relacyjnym. Relacje te odzwierciedlają ograniczenia kluczy obcych, jakie tabela [personne_activite] ma w stosunku do innych tabel.
- wiersze 33–39: definiują klucz obcy, jaki posiada tabela [personne_activite] w odniesieniu do tabeli [personne]
- wiersz 37: relacja jest typu @ManyToOne: jeden (One) wiersz tabeli [personne] jest odwołany przez wiele (Many) wierszy tabeli [personne_activite].
- wiersz 38: nadajemy nazwę kolumnie klucza obcego. Używamy tej samej nazwy, co w przypadku składowej „osoba” klucza obcego (wiersz 10). Atrybuty insertable=false, updatable=false mają na celu uniemożliwienie Hibernate zarządzania kluczem obcym. Jest on bowiem składową klucza głównego obliczanego przez aplikację, a Hibernate nie powinien w to ingerować.
- wiersze 41–47: definiują klucz obcy, który tabela [personne_activite] posiada w odniesieniu do tabeli [activite]. Wyjaśnienia są takie same jak podane wcześniej.
- wiersze 54–63: konstruktor obiektu PersonneActivite na podstawie osoby p i czynności a. Przypomnijmy, że podczas tworzenia obiektu PersonneActivite klucz podstawowy id z wiersza 31 wskazywał na pusty obiekt Id. Wiersze 56–57 nadają wartości każdemu z pól (personneId, activiteId) obiektu Id. Wartości te są odpowiednio kluczami głównymi osoby p oraz czynności a, przekazanymi jako parametry do konstruktora. Klucz główny id (wiersz 31) ma zatem teraz przypisaną wartość.
- wiersz 59: pole personne z wiersza 39 otrzymuje wartość p
- wiersz 60: pole activite z wiersza 47 otrzymuje wartość a
- Obiekt [PersonneActivite] został utworzony i zainicjowany. Aktualizujemy relacje odwrotne między @Entity Personne (wiersz 61) i Activite (wiersz 62) a właśnie utworzonym @Entity PersonneActivite.
Zakończyliśmy opis encji bazy danych. Znajdujemy się w złożonej, ale niestety częstej sytuacji. Zobaczymy, że istnieje inna możliwa konfiguracja warstwy JPA, która ukrywa część tej złożoności: tabela połączeń staje się domyślna, tworzona i zarządzana przez warstwę JPA. Wybraliśmy tutaj rozwiązanie najbardziej złożone, ale umożliwiające ewolucję schematu relacyjnego. Pozwala ono na dodawanie kolumn do tabeli połączeń, czego nie umożliwia konfiguracja, w której tabela połączeń nie jest jawną instancją @Entity. [ref1] zaleca rozwiązanie, które właśnie analizujemy. To właśnie w [ref1] znaleziono informacje, które umożliwiły opracowanie tego rozwiązania.
2.5.3. Projekt Eclipse / Hibernate
Wykorzystana tutaj implementacja JPA pochodzi z biblioteki Hibernate. Projekt Eclipse służący do testów wygląda następująco:

W pliku [1] znajduje się projekt Eclipse, a w pliku [2] – kod Java. Projekt ten znajduje się w pliku [3] w folderze przykładów [4]. Zaimportujemy go.
2.5.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami z punktu 2.1.7, plik DDL uzyskany dla plików SGBD i MySQL5 wygląda następująco:
alter table jpa07_hb_personne
drop
foreign key FKB5C817D45FE379D0;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B06CD852024;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B0668C7A284;
drop table if exists jpa07_hb_activite;
drop table if exists jpa07_hb_adresse;
drop table if exists jpa07_hb_personne;
drop table if exists jpa07_hb_personne_activite;
create table jpa07_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa07_hb_personne
add index FKB5C817D45FE379D0 (adresse_id),
add constraint FKB5C817D45FE379D0
foreign key (adresse_id)
references jpa07_hb_adresse (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B06CD852024 (ACTIVITE_ID),
add constraint FKD3E49B06CD852024
foreign key (ACTIVITE_ID)
references jpa07_hb_activite (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B0668C7A284 (PERSONNE_ID),
add constraint FKD3E49B0668C7A284
foreign key (PERSONNE_ID)
references jpa07_hb_personne (id);
- wiersze 21–26: tabela [activite]
- wiersze 28–39: tabela [adresse]
- wiersze 41–51: tabela [personne]
- wiersze 53–57: tabela połączeń [personne_activite]. Należy zwrócić uwagę na klucz złożony (wiersz 56)
- wiersze 59–63: klucz obcy z tabeli [personne] do tabeli [adresse]
- wiersze 65–69: klucz obcy z tabeli [personne_activite] do tabeli [activite]
- wiersze 71–75: klucz obcy z tabeli [personne_activite] do tabeli [personne]
2.5.5. InitDB
Kod tabeli [InitDB] jest następujący:
package tests;
...
public class InitDB {
// stałe
private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";
private final static String TABLE_PERSONNE = "jpa07_hb_personne";
private final static String TABLE_ACTIVITE = "jpa07_hb_activite";
private final static String TABLE_ADRESSE = "jpa07_hb_adresse";
public static void main(String[] args) throws ParseException {
// Kontekst trwałości
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// pobieramy EntityManager z EntityManagerFactory
// poprzedni
em = emf.createEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// zapytanie
Query sql1;
// usuń elementy z tabeli PERSONNE_ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
sql1.executeUpdate();
// usunięcie elementów z tabeli PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// usunięcie elementów z tabeli ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
sql1.executeUpdate();
// usunięcie elementów z tabeli ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// tworzenie działań
Activite act1 = new Activite();
act1.setNom("act1");
Activite act2 = new Activite();
act2.setNom("act2");
Activite act3 = new Activite();
act3.setNom("act3");
// trwałość działań
em.persist(act1);
em.persist(act2);
em.persist(act3);
// tworzenie osób
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// tworzenie adresów
Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
// powiązania osób <--> adresy
p1.setAdresse(adr1);
adr1.setPersonne(p1);
p2.setAdresse(adr2);
adr2.setPersonne(p2);
p3.setAdresse(adr3);
adr3.setPersonne(p3);
// trwałość osób, a tym samym powiązanych z nimi adresów
em.persist(p1);
em.persist(p2);
em.persist(p3);
// trwałość adresu a4 niepowiązanego z osobą
em.persist(adr4);
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// wyświetlanie adresów
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
// powiązania osoba <--> aktywność
PersonneActivite p1act1 = new PersonneActivite(p1, act1);
PersonneActivite p1act2 = new PersonneActivite(p1, act2);
PersonneActivite p2act1 = new PersonneActivite(p2, act1);
PersonneActivite p2act3 = new PersonneActivite(p2, act3);
// trwałość powiązań osoba <--> aktywność
em.persist(p1act1);
em.persist(p1act2);
em.persist(p2act1);
em.persist(p2act3);
// wyświetlanie osób
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// wyświetlanie adresów
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
System.out.println("[personnes/activites]");
for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
System.out.println(pa);
}
// zakończenie transakcji
tx.commit();
// koniec EntityManager
em.close();
// koniec EntityManagerFactory
emf.close();
// log
System.out.println("terminé...");
}
}
- wiersze 27–38: tabele [personne_activite], [personne], [adresse] i [activite] są opróżniane. Należy zauważyć, że konieczne jest rozpoczęcie od tabel zawierających klucze obce.
- wiersze 40–45: tworzone są trzy działania: act1, act2 i act3
- wiersze 47–49: są one umieszczane w kontekście trwałości.
- wiersze 51–53: tworzymy trzy osoby: p1, p2 i p3.
- wiersze 55–58: tworzymy cztery adresy od adr1 do adr4.
- wiersze 60–65: adresy od adri są przypisane do osób o adresach pi. W każdym przypadku należy wykonać dwie operacje, ponieważ relacja Osoba <-> Adres jest dwukierunkowa.
- wiersze 67–69: osoby o adresach od p1 do p3 są umieszczane w kontekście trwałości. Ze względu na kaskadę Osoba -> Adres dotyczy to również adresów od adr1 do adr3.
- wiersz 71: czwarty adres adr4, który nie jest powiązany z żadną osobą, zostaje wyraźnie umieszczony w kontekście trwałości.
- wiersze 73–85: wysyłane jest zapytanie do kontekstu trwałości w celu uzyskania listy encji typu [Personne], [Adresse] i [Activite]. Wiadomo, że zapytania te spowodują synchronizację kontekstu z bazą danych: utworzone encje zostaną wstawione do bazy i otrzymają swój klucz główny. Zrozumienie tego jest ważne dla dalszego przebiegu procesu.
- wiersze 87–90: tworzymy 4 powiązania Osoba <-> Działalność. Ich nazwy wskazują, która osoba jest powiązana z daną działalnością. Być może pamiętamy, że klucz podstawowy encji PersonneActivite jest kluczem złożonym, utworzonym z klucza podstawowego osoby oraz klucza podstawowego działalności. Operacja ta jest więc możliwa właśnie dlatego, że encje Personne i Activite uzyskały swoje klucze główne podczas poprzedniej synchronizacji.
- wiersze 92–95: te 4 powiązania są umieszczane w kontekście trwałości.
- wiersze 87–86: wysyłane jest zapytanie do kontekstu trwałości w celu uzyskania listy encji typu [Personne], [Adresse], [Activite] oraz [PersonneActivite]. Wiadomo, że zapytania te spowodują synchronizację kontekstu z bazą danych: utworzone encje typu PersonneActivite zostaną wstawione do bazy danych.
Wykonanie zapytania [InitDB] wraz z zapytaniem MySQL5 powoduje wyświetlenie następującego komunikatu w konsoli:
Można się zdziwić, widząc, że w wierszach 15–16 osoby p1 i p2 mają numery wersji równe 1, a to samo dotyczy trzech działań w wierszach 24–26. Spróbujmy to zrozumieć.
W wierszach 2–4 numery wersji osób wynoszą 0, a w wierszach 11–13 numery wersji działań wynoszą 0. Powyższe wyświetlenia mają miejsce przed utworzeniem relacji Osoba <-> Działanie. W wierszach 87–90 kodu Java tworzone są relacje między osobami p1 i p2 a działaniami act1, act2, act3. Są one tworzone za pomocą konstruktora klasy @Entity PersonneActivite (patrz punkt 2.5.2). Analiza kodu tego konstruktora pokazuje, że gdy osoba p jest powiązana z działaniem a:
- aktywność a jest dodawana do zbioru p.activites
- osoba p jest dodawana do zbioru a.personnes
Zatem gdy zapisujemy new PersonneActivite(p,a), osoba p i aktywność a ulegają modyfikacji w pamięci. W wierszach 97–113 obiektu [InitDB] kontekst trwałości jest synchronizowany z bazą danych, JPA / Hibernate wykrywa, że elementy trwałe p1, p2, act1, act2 i act3 zostały zmodyfikowane. Zmiany te należy wprowadzić w bazie danych. Są one faktycznie zapisane w tabeli połączeń [personne_activite], jednak JPA / Hibernate i tak zwiększa numer wersji każdego ze zmienionych elementów trwałych.
W widoku SQL Explorer wyniki są następujące:
![]() |
- [2]: tabele [jpa07_hb_*]
- [3]: tabela osób
- [4]: tabela adresów.
- [5]: tabela działań
- [6]: tabela połączeń osoba <-> aktywność
2.5.6. Strona główna
Klasa [Main] zawiera sekwencję testów, które przeanalizujemy, z wyjątkiem testu 1, który wykorzystuje kod z klasy [InitDB] do zainicjowania bazy danych.
2.5.6.1. Test 2
Test ten wygląda następująco:
// usunięcie osoby p1
public static void test2() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usunięcie zależności od p1: nie jest to konieczne dla Hibernate, ale
// niezbędne dla TopLink
act1.getPersonnes().remove(p1act1);
act2.getPersonnes().remove(p1act2);
// usunięcie osoby p1
em.remove(p1);
// koniec transakcji
tx.commit();
// wyświetlanie nowych tabel
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- wiersz 4: wykorzystuje się kontekst trwałości test1, w którym osoba p1 jest obiektem tego kontekstu.
- wiersz 13: usunięcie osoby p1. Z powodu atrybutu:
- cascadeType.ALL na Adresse, adres osoby p1 zostanie usunięty
- cascadeType.REMOVE na PersonneActivite, działania osoby p1 zostaną usunięte.
- wiersze 10–11: usuwa się zależności innych podmiotów od osoby p1, która zostanie usunięta w wierszu 13. Działania act1 i act2 są wykonywane przez osobę p1. Powiązania zostały utworzone przez konstruktora podmiotu PersonneActivite, którego kod jest następujący:
public PersonneActivite(Personne p, Activite a) {
// klucze obce są ustalane przez aplikację
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// powiązania dwukierunkowe
setPersonne(p);
setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
w wierszu 9 aktywność a otrzymuje dodatkowy element typu PersonneActivite w swoim zbiorze personnes. Element ten ma typ (p,a), co oznacza, że osoba p wykonuje czynność a. W elemencie test1, będącego częścią [Main], utworzono w ten sposób dwa powiązania: (p1,act1) i (p1,act2). Wiersze 10 i 11 w test2 usuwają te zależności. Należy zauważyć, że Hibernate działa bez usuwania tych zależności w przypadku osoby p1, ale Toplink nie.
- wiersze 17–20: wyświetlane są wszystkie tabele
Wyniki są następujące:
- osoba p1 występująca w test1 (wiersz 3) nie występuje już po zakończeniu operacji test2 (wiersze 22–23)
- adres adr1 osoby p1, występującej w test1 (wiersz 11) przestaje być poprawny po wykonaniu operacji test2 (wiersze 29–31)
- działania (p1,act1) (wiersz 16) oraz (p1,act2) (wiersz 18) osoby p1, obecne w test1, nie występują już po zakończeniu operacji test2 (wiersze 33–34)
2.5.6.2. Test3
Test ten wygląda następująco:
// usunięcie aktywności act1
public static void test3() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usunięcie zależności od act1: nie jest to konieczne w Hibernate, ale
// niezbędne dla TopLink
p2.getActivites().remove(p2act1);
// usunięcie aktywności act1
em.remove(act1);
// koniec transakcji
tx.commit();
// wyświetlanie nowych tabel
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- wiersz 4: wykorzystuje się kontekst trwałości z test2
- wiersz 12: usunięcie operacji act1. Z powodu atrybutu:
- cascadeType.REMOVE w odniesieniu do PersonneActivite, wiersze (p, act1) z tabeli [personne_activite] zostaną usunięte.
- wiersz 10: przed wyłączeniem obiektu act1 z kontekstu trwałości usuwane są zależności, jakie inne encje mogą mieć względem tego obiektu trwałego. Po usunięciu osoby p1 w poprzednim teście tylko osoba p2 wykonuje czynność act1.
- wiersze 13–16: wyświetlane są wszystkie tabele
Wyniki są następujące:
- w test2 istnieje czynność act1 (wiersz 6). W test3 już nie istnieje (wiersze 21–22)
- W pliku test2 istnieje odnośnik (p2,act1)) (wiersz 14). W pliku test3 już go nie ma (wiersz 28)
2.5.6.3. Test4
Ten test wygląda następująco:
// pobieranie działań danej osoby
public static void test4() {
// kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// pobieranie osoby p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
// przeglądanie jej działań
for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
System.out.println(pa);
}
// przechodzi się przez relację odwrotną do p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
// przeglądamy jej działania
for (PersonneActivite pa : p2.getActivites()) {
System.out.println(pa.getActivite().getNom());
}
// koniec transakcji
tx.commit();
}
- Test 4 wyświetla działania osoby p2.
- wiersz 4: zaczynamy od nowego, pustego kontekstu
- wiersze 12–14: wyświetlane są nazwy działań wykonywanych przez osobę p2 za pomocą zapytania JPQL.
- tworzone jest połączenie Activite (a) / PersonneActivite (pa) (join a.personnes)
- w wierszach tego połączenia (a,pa) wyświetlana jest nazwa czynności (a.nom) dla osoby p2 (pa.personne.nom='p2').
- wiersze 16–21: wykonujemy tę samą czynność co poprzednio, ale z wykorzystaniem relacji OneToMany p2.activites dotyczącej osoby p2. Zapytanie JPQL zostanie wygenerowane przez JPA. Widać tu zaletę relacji odwrotnej OneToMany: pozwala ona uniknąć zapytania JPQL.
Wyniki są następujące:
2.5.6.4. Test5
Test ten wygląda następująco:
// pobieranie osób wykonujących daną czynność
public static void test5() {
// kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// pobieranie działań użytkownika p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
System.out.println(pa);
}
// przechodzimy przez relację odwrotną do act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (PersonneActivite pa : act3.getPersonnes()) {
System.out.println(pa.getPersonne().getNom());
}
// koniec transakcji
tx.commit();
}
- Test 6 pokazuje osoby wykonujące czynność act3. Procedura jest analogiczna do tej z testu 6. Pozostawiamy czytelnikowi zadanie powiązania tych dwóch kodów.
Wyniki są następujące:
Testy 4 i 5 miały na celu ponowne wykazanie, że relacja odwrotna nigdy nie jest niezbędna i zawsze można ją zastąpić zapytaniem JPQL.
2.5.7. Implementacja JPA / Toplink
Obecnie korzystamy z implementacji JPA / Toplink:
![]() |
Projekt Eclipse z Toplinkiem jest kopią projektu Eclipse z Hibernate:
![]() |
Kod Java jest identyczny z kodem poprzedniego projektu z Hibernate, z wyjątkiem kilku szczegółów, które omówimy. Środowisko (biblioteki – persistence.xml – system zarządzania bazami danych – foldery conf, ddl – skrypt ant) jest takie samo jak omówione w paragrafie 2.1.15.2. Projekt Eclipse o nazwie [3] znajduje się w folderze przykładów [4]. Zaimportujemy go.
Plik <persistence.xml> [2] został zmodyfikowany w jednym miejscu, a mianowicie w zakresie zadeklarowanych encji:
<!-- klasy trwałe -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
<class>entites.PersonneActivite</class>
- wiersze 2–5: cztery zarządzane jednostki
Uruchomienie programu [InitDB] wraz z plikami SGBD i MySQL5 daje następujące wyniki:
![]() |
W [1], wyświetlacz konsoli, w [2] wygenerowane tabele [jpa07_tl], a w [3] wygenerowane skrypty SQL. Ich zawartość jest następująca:
create.sql
CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
Wykonanie skryptów [InitDB] i [Main] przebiega bez błędów.
2.6. Przykład 6: relacja wiele-do-wielu z domyślną tabelą połączeń
Wracamy do przykładu 4, ale tym razem przetwarzamy go z wykorzystaniem domyślnej tabeli połączeń wygenerowanej przez samą warstwę JPA.
2.6.1. Schemat bazy danych
![]() |
- w [1], baza danych MySQL5 – w [2]: tabela [personne] – na [3]: powiązana tabela [adresse] – na [4]: tabela [activite] zawierająca działania – w [5]: tabela łącząca [personne_activite], która łączy osoby z działaniami.
2.6.2. Obiekty @Entity reprezentujące bazę danych
Powyższe tabele będą reprezentowane przez następujące @Entity:
- @Entity Personne będzie reprezentować tabelę [personne]
- @Entity Adresse będzie reprezentować tabelę [adresse]
- @Entity Activite będzie reprezentować tabelę [activite]
- tabela [personne_activite] nie jest już reprezentowana przez @Entity
Relacje między tymi encjami są następujące:
- Relacja jeden do jednego łączy encję Personne z encją Adresse: osoba p ma adres a. Entyteta Personne, która posiada klucz obcy, będzie miała relację główną, natomiast entyteta Adresse – relację odwrotną.
- Relacja wiele-do-wielu łączy encje Personne i Activite: jedna osoba wykonuje wiele czynności, a jedna czynność jest wykonywana przez wiele osób. Relacja ta zostanie zrealizowana poprzez adnotację @ManyToMany w każdej z obu encji, przy czym jedna z nich zostanie zadeklarowana jako odwrotna względem drugiej.
@Entity Personne ma następujący wygląd:
@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver: @GeneratedValue (strategia = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// relacja główna Osoba (one) -> Adres (one)
// zrealizowana przez klucz obcy Osoba (adresse_id) -> Adres
// kaskadowe wstawianie Osoba -> wstawianie Adres
// kaskadowa aktualizacja Osoba -> aktualizacja Adres
// kaskadowe usunięcie Osoby -> usunięcie Adresu
// każda osoba musi mieć 1 adres (nullable=false)
// 1 adres może należeć tylko do 1 osoby (unikalny=true)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// relacja Osoba (wiele) -> Działalność (wiele) poprzez tabelę łączącą personne_activite
// personne_activite(PERSONNE_ID) jest kluczem obcym w tabeli Osoba(id)
// personne_activite(ACTIVITE_ID) jest kluczem obcym dla „Aktywności” (id)
// kaskada=CascadeType.PERSIST: trwałość jednej osoby pociąga za sobą trwałość jej działań
@ManyToMany(cascade={CascadeType.PERSIST})
@JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
// twórcy
public Personne() {
}
Omówimy jedynie relację @ManyToMany z wierszy 46–48, która łączy @Entity Personne z @Entity Activite:
- wiersz 48: osoba ma działania. Pole „activites” będzie je reprezentować. W poprzedniej wersji typ elementów zbioru activites to PersonneActivite. Tutaj jest to Activite. Mamy zatem bezpośredni dostęp do działań danej osoby, podczas gdy w poprzedniej wersji konieczne było przejście przez jednostkę pośredniczącą PersonneActivite.
- wiersz 46: relacja łącząca analizowaną przez nas @Entity Personne z @Entity Activite z zbioru activites z wiersza 48 jest relacją typu wiele-do-wielu (ManyToMany):
- jedna osoba (One) wykonuje wiele czynności (Many)
- jedna aktywność (One) jest wykonywana przez wiele osób (Many)
- w rezultacie @Entity Personne i Activite są połączone relacją ManyToMany. Podobnie jak w relacji OneToOne, w tej relacji występuje symetria encji. Można dowolnie wybrać @Entity, która będzie posiadać relację główną, oraz tę, która będzie posiadać relację odwrotną. W tym przypadku decydujemy, że @Entity Personne będzie posiadać relację główną.
- Jak widzieliśmy w poprzednim przykładzie, relacja @ManyToMany wymaga tabeli łączącej. Podczas gdy wcześniej zdefiniowaliśmy ją za pomocą @Entity, w tym przypadku tabela łącząca jest zdefiniowana za pomocą adnotacji @JoinTable w wierszu 47.
- Atrybut name nadaje nazwę tabeli.
- Tabela łącząca składa się z kluczy obcych z tabel, które łączy. W tym przypadku występują dwa klucze obce: jeden w tabeli [personne], a drugi w tabeli [activite]. Te kolumny kluczy obcych są zdefiniowane przez atrybuty joinColumns i inverseJoinColumns.
- Adnotacja @JoinColumn atrybutu joinColumns definiuje klucz obcy w tabeli @Entity zawierającej relację główną @ManyToMany, w tym przypadku w tabeli [personne]. Ta kolumna klucza obcego będzie nosić nazwę PERSONNE_ID.
- Adnotacja @JoinColumn przypisana do atrybutu inverseJoinColumns definiuje klucz obcy w tabeli klasy @Entity, która jest stroną odwrotną relacji @ManyToMany, w tym przypadku w tabeli [activite]. Ta kolumna klucza obcego będzie nosiła nazwę ACTIVITE_ID.
@Entity Adresse ma następującą postać:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// pola
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- wiersze 28–29: relacja @OneToOne jest relacją odwrotną do relacji @OneToOne, odnoszącej się do @Entity Personne (wiersze 37–38 w Personne).
@Entity Activite ma następującą postać
@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {
// pola
@Id()
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver: @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// relacja odwrotna: Działalność -> Osoba
@ManyToMany(mappedBy = "activites")
private Set<Personne> personnes = new HashSet<Personne>();
...
- wiersze 20–21: relacja wiele-do-wielu łącząca @Entity Activite z @Entity Personne. Relacja ta została już zdefiniowana w @Entity Personne. W tym miejscu wystarczy zatem zaznaczyć, że relacja ta jest odwrotna (mappedBy) do relacji @ManyToMany istniejącej w polu „activites” (mappedBy = „activites”) w @Entity Personne.
- Należy pamiętać, że relacja odwrotna jest zawsze opcjonalna. W tym przypadku wykorzystujemy ją, aby uzyskać osoby uprawiające bieżącą aktywność. Zbiór Set<Personne> „osoby” pozwoli na uzyskanie tych osób. Sposób ładowania zależności Personne z @Entity Activite nie został określony. Nie określiliśmy go również w poprzednim przykładzie. Domyślnie jest to fetch=FetchType.LAZY.
Zakończyliśmy opis encji bazy danych. Było to prostsze niż w przypadku, gdy tabela łącząca [personne_activite] jest reprezentowana przez jawną tabelę. To prostsze rozwiązanie może z czasem okazać się niekorzystne: nie pozwala na dodawanie kolumn do tabeli łączącej. Może to jednak okazać się konieczne, aby sprostać nowym wymaganiom, na przykład dodaniu do tabeli [personne_activite] kolumny wskazującej datę zapisania się osoby na zajęcia.
2.6.3. Projekt Eclipse / Hibernate
Wykorzystana tutaj implementacja JPA pochodzi z biblioteki Hibernate. Projekt Eclipse zawierający testy wygląda następująco:
![]() |
W pliku [1] znajduje się projekt Eclipse, a w pliku [2] kod Java. Projekt ten znajduje się w pliku [3] w folderze przykładów [4]. Zaimportujemy go.
2.6.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami z punktu 2.1.7, plik DDL uzyskany dla plików SGBD i MySQL5 wygląda następująco:
alter table jpa08_hb_personne
drop
foreign key FKA44B1E555FE379D0;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A5CD852024;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A568C7A284;
drop table if exists jpa08_hb_activite;
drop table if exists jpa08_hb_adresse;
drop table if exists jpa08_hb_personne;
drop table if exists jpa08_hb_personne_activite;
create table jpa08_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa08_hb_personne
add index FKA44B1E555FE379D0 (adresse_id),
add constraint FKA44B1E555FE379D0
foreign key (adresse_id)
references jpa08_hb_adresse (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A5CD852024 (ACTIVITE_ID),
add constraint FK5A6A55A5CD852024
foreign key (ACTIVITE_ID)
references jpa08_hb_activite (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A568C7A284 (PERSONNE_ID),
add constraint FK5A6A55A568C7A284
foreign key (PERSONNE_ID)
references jpa08_hb_personne (id);
Ten kod DDL jest analogiczny do kodu uzyskanego przy użyciu tabeli jawnych połączeń i odpowiada przedstawionemu wcześniej schematowi:
![]() |
2.6.5. InitDB
Nie będziemy się zbytnio rozwodzić nad klasą [InitDB], która jest identyczna z poprzednią wersją i daje te same wyniki. Skupmy się po prostu na poniższym kodzie, który wyświetla połączenie Personne <-> Activite:
// wyświetlanie osób/działalności
System.out.println("[personnes/activites]");
Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
- wiersz 3: polecenie JPQL, które wykonuje połączenie. Wynik polecenia select zwraca identyfikatory encji Personne i Activite, połączonych ze sobą za pomocą tabeli połączeń. Lista zwrócona przez select składa się z wierszy zawierających dwa obiekty typu Long. Aby przejrzeć tę listę, w wierszu 3 żądany jest obiekt Iterator z listy.
- wiersze 4–7: za pomocą poprzedniego obiektu typu Iterator przegląda się listę.
- wiersz 5: każdy element listy jest tablicą zawierającą wiersz wynikowy funkcji select
- wiersz 6: pobierane są elementy bieżącego wiersza wynikowego funkcji select po wprowadzeniu odpowiednich zmian typów.
Wynik operacji [InitDB] jest następujący:
2.6.6. Main
Klasa [Main] zawiera sekwencję testów, z których niektóre omówimy poniżej.
2.6.6.1. Test3
Ten test wygląda następująco:
// usunięcie działania act1
public static void test3() {
// kontekst trwałości
EntityManager em = getEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usunięcie działania act1 z p2
p2.getActivites().remove(act1);
// usuwanie czynności act1 z kontekstu trwałości
em.remove(act1);
// koniec transakcji
tx.commit();
// wyświetlanie nowych tabel
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- wiersz 11: aktywność act1 zostaje usunięta z kontekstu trwałości
- wiersz 9: aktywność act1 należy do aktywności jedynej osoby pozostałej w kontekście, czyli osoby p2. Wiersz 9 usuwa aktywność act1 z aktywności osoby p2. Robimy to, aby zachować spójność kontekstu trwałości, ponieważ zachowujemy go na dalsze potrzeby.
Wyniki są następujące:
- czynność act1, występująca w wierszu 26 w test2, zniknęła z czynności test3 (wiersze 40–41)
- osoba o numerze p2 miała w test2 działanie act1 (wiersz 33). Po zakończeniu działania test3 nie posiadała go już (wiersz 47)
2.6.6.2. Test6
Test ten wygląda następująco:
// zmiana zakresu działalności danej osoby
public static void test6() {
// kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// pobieranie osoby p2
p2 = em.find(Personne.class, p2.getId());
// pobieranie czynności act2
act2 = em.find(Activite.class, act2.getId());
// p2 wykonuje już wyłącznie czynność act2
p2.getActivites().clear();
p2.getActivites().add(act2);
// koniec transakcji
tx.commit();
// wyświetla się nowe tabele
dumpPersonne();
dumpActivite();
dumpPersonne_Activite();
}
- wiersz 4: używany jest nowy, pusty kontekst trwałości
- wiersz 9: osoba p2 jest pobierana z bazy danych do kontekstu trwałości
- wiersz 11: aktywność act2 jest pobierana z bazy danych do kontekstu trwałości
- wiersz 13: działania osoby p2 (act3) są pobierane z bazy do kontekstu (fetchType.LAZY). To wywołanie [getActivites] powoduje to załadowanie. Usuwane są działania osoby p2. Nie jest to faktyczne usunięcie działań (remove), lecz zmiana statusu osoby p2. Osoba ta nie wykonuje już żadnych działań.
- wiersz 14: do osoby p2 dodajemy aktywność act2. Ostatecznie zbiór nowych aktywności osoby p2 to zbiór {act2}.
- wiersz 16: koniec transakcji. Synchronizacja przejrzy obiekty kontekstu (p2, act2, act3) i wykryje, że stan obiektu p2 uległ zmianie. Zostaną wykonane polecenia SQL, które odzwierciedlają tę zmianę w bazie danych.
- wiersze 18–20: wyświetlane są wszystkie tabele
Wyniki są następujące:
- po zakończeniu testu 4 osoba p2 wykonywała czynność act3 (wiersz 3).
- Po zakończeniu testu 6 (wiersz 19) osoba o identyfikatorze p2 nie wykonuje już czynności act3 (wiersz 3), a wykonuje czynność act2.
2.6.7. Implementacja JPA / Toplink
Obecnie korzystamy z implementacji JPA / Toplink:
![]() |
Projekt Eclipse z Toplinkiem jest kopią projektu Eclipse z Hibernate:
![]() |
Plik <persistence.xml> [2] został zmodyfikowany w jednym miejscu, a mianowicie w zakresie zadeklarowanych encji:
<!-- dostawca -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- klasy trwałe -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
...
- wiersze 4–6: zarządzane encje
Uruchomienie programu [InitDB] wraz z plikami SGBD i MySQL5 daje następujące wyniki:
![]() |
W [1], wyświetlanie konsoli, w [2] wygenerowane tabele [jpa07_tl], w [3] wygenerowane skrypty SQL. Ich zawartość jest następująca:
create.sql
CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
Wykonanie plików [InitDB] i [Main] przebiega bez błędów.
2.6.8. Projekt Eclipse / Hibernate 2
Tworzymy projekt Eclipse na podstawie poprzedniego poprzez skopiowanie:
![]() |
W pliku [1] znajduje się projekt Eclipse, a w pliku [2] – kod Java. Projekt ten znajduje się w pliku [3] w folderze przykładów [4]. Zaimportujemy go.
Modyfikujemy relację łączącą plik Personne z plikiem Activité w następujący sposób:
Osoba
// relacja Osoba (wiele) -> Działalność (wiele) poprzez tabelę łączącą personne_activite
// personne_activite(PERSONNE_ID) jest kluczem obcym w tabeli Osoba (id)
// personne_activite(ACTIVITE_ID) jest kluczem obcym dla „Aktywności” (id)
// brak kaskadowania w przypadku działań
// @ManyToMany(kaskada={CascadeType.PERSIST})
@ManyToMany()
@JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
- wiersz 6: relacja główna @ManyToMany nie ma już kaskady trwałości Osoba -> Działanie (por. poprzednia wersja, wiersz 5)
Działalność
// brak relacji odwrotnej z osobą
// @ManyToMany(mappedBy = "activites")
// private Set<Osoba> osoby = new HashSet<Osoba>();
- wiersze 2–3: relacja odwrotna @ManyToMany Działalność -> Osoba została usunięta
Chcemy pokazać, że usunięte atrybuty (kaskada i relacja odwrotna) nie są niezbędne. Pierwsza zmiana wynikająca z tej nowej konfiguracji znajduje się w [InitDB]:
// powiązania osoby <--> działania
p1.getActivites().add(act1);
p1.getActivites().add(act2);
p2.getActivites().add(act1);
p2.getActivites().add(act3);
// trwałość działań
em.persist(act1);
em.persist(act2);
em.persist(act3);
// trwałość osób
em.persist(p1);
em.persist(p2);
em.persist(p3);
// oraz adresu a4 niepowiązanego z żadną osobą
em.persist(adr4);
- wiersze 7–9: konieczne jest jawne umieszczenie działań od act1 do act3 w kontekście trwałości. Gdy istniała kaskada trwałości „Osoba” -> Działanie, wiersze 11–13 zapisywały zarówno osoby o numerach od p1 do p3, jak i działania tych osób o numerach od act1 do act3.
Drugą zmianę widać w przypadku [Main]:
// wyszukiwanie osób wykonujących daną aktywność
public static void test5() {
// kontekst trwałości
EntityManager em = getNewEntityManager();
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// żądanie informacji o działaniach p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
System.out.println(pa);
}
// koniec transakcji
tx.commit();
}
- wiersze 9–12: zapytanie JPQL, które pozwala uzyskać osoby wykonujące czynność act3
- w poprzedniej wersji ten sam wynik uzyskano również poprzez odwrotną relację „Działalność” → „Osoba”, która została obecnie usunięta:
// przechodzimy przez relację odwrotną do act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (Personne p : act3.getPersonnes()) {
System.out.println(p.getNom());
}
2.6.9. Projekt Eclipse / Toplink 2
Tworzymy projekt Eclipse na podstawie poprzedniego projektu Eclipse / Toplink poprzez skopiowanie:
![]() |
W pliku [1] znajduje się projekt Eclipse, a w pliku [2] – kod Java. Projekt ten znajduje się w pliku [3] w folderze przykładów [4]. Zaimportujemy go.
Kod Java jest identyczny jak w wersji Hibernate.
2.7. Przykład 7: korzystanie z nazwanych zapytań
Zakończymy tę obszerną prezentację encji JPA, rozpoczętą w akapicie 2, ostatnim przykładem ilustrującym wykorzystanie zapytań JPQL umieszczonych w pliku konfiguracyjnym. Przykład ten pochodzi z następującego źródła:
[ref2]: „Getting started With JPA in Spring 2.0” autorstwa Marka Fishera, dostępny pod adresem
[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].
2.7.1. Przykładowa baza danych
Baza danych wygląda następująco:
![]() |
- w [1]: lista restauracji wraz z ich nazwami i adresami
- w [2]: tabela adresów restauracji, ograniczona do numeru i nazwy ulicy. Mamy relację jeden do jednego między tabelami restaurant i adresse: każda restauracja ma jeden adres i tylko jeden.
- w [3]: tabela dań zawierająca ich nazwy oraz wskaźnik „prawda/fałsz” określający, czy danie jest wegetariańskie, czy nie
- w tabeli [4]: tabela łącząca restauracje i dania: jedna restauracja serwuje wiele dań, a to samo danie może być serwowane przez wiele restauracji. Pomiędzy tabelami restaurant i plat występuje relacja wiele-do-wielu.
2.7.2. Obiekty @Entity reprezentujące bazę danych
Powyższe tabele będą reprezentowane przez następujące @Entity:
- @Entity Restaurant będzie reprezentować tabelę [restaurant]
- @Entity Adresse będzie reprezentować tabelę [adresse]
- @Entity Plat będzie reprezentować tabelę [plat]
Relacje między tymi encjami są następujące:
- relacja jeden do jednego łączy encję Restaurant z encją Adresse: restauracja r ma adres a. Entyteta Restaurant, która posiada klucz obcy, będzie miała relację główną. Entyteta Adresse nie będzie miała relacji odwrotnej.
- Relacja wiele-do-wielu łączy encje Restaurant i Plat: restauracja serwuje wiele dań, a to samo danie może być serwowane przez wiele restauracji. Relacja ta zostanie odzwierciedlona poprzez adnotację @ManyToMany w encji Restaurant. Encja Plat nie będzie miała relacji odwrotnej.
@Entity Restaurant ma następującą postać:
package entites;
...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique = true, length = 30, nullable = false)
private String nom;
@OneToOne(cascade = CascadeType.ALL)
private Adresse adresse;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
private Set<Plat> plats = new HashSet<Plat>();
// konstruktory
public Restaurant() {
}
public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
String signature = "R[" + getNom() + "," + getAdresse();
for (Plat e : getPlats()) {
signature += "," + e;
}
return signature + "]";
}
}
- wiersz 17: relacja jeden do jednego między encją Restaurant a encją Adresse. Wszystkie operacje związane z trwałością danych dotyczące restauracji są kaskadowo stosowane do jej adresu.
- wiersz 20: relacja łącząca @Entity Restaurant z @Entity Plat z zbioru plats z wiersza 22 jest typu wiele-do-wielu (ManyToMany):
- jedna restauracja (One) ma wiele dań (Many)
- jedno danie (One) może być serwowane przez wiele restauracji (Many)
- w rezultacie @Entity Restaurant i Plat są połączone relacją ManyToMany. Ustalamy, że @Entity Restaurant będzie miało relację główną, a @Entity Plat nie będzie miało relacji odwrotnej.
- Relacja @ManyToMany wymaga tabeli łączącej. Jest ona zdefiniowana za pomocą adnotacji @JoinTable w wierszu 47.
- Atrybut name nadaje nazwę tabeli.
- Tabela łącząca składa się z kluczy obcych z tabel, które łączy. W tym przypadku występują dwa klucze obce: jeden w tabeli [restaurant], a drugi w tabeli [plat]. Te kolumny kluczy obcych są zdefiniowane przez atrybuty joinColumns i inverseJoinColumns.
- Atrybut joinColumns definiuje klucz obcy w tabeli @Entity zawierającej relację główną @ManyToMany, w tym przypadku w tabeli [restaurant]. Atrybut joinColumns nie występuje w tym przypadku. W tym przypadku JPA ma wartość domyślną: [table]_[clé_primaire_de_table], w tym przypadku [jpa10_hb_restaurant_id].
- Adnotacja @JoinColumn atrybutu inverseJoinColumns definiuje klucz obcy w tabeli @Entity zawierającej relację odwrotną @ManyToMany, w tym przypadku w tabeli [plat]. Ta kolumna klucza obcego będzie nosiła nazwę plat_id.
@Entity Adresse ma następującą postać:
package entites;
...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(name = "NUMERO_RUE")
private int numeroRue;
@Column(name = "NOM_RUE", length=30, nullable=false)
private String nomRue;
// metody pobierające i ustawiające
...
// konstruktory
public Adresse(int streetNumber, String streetName){
...
}
public Adresse(){
}
// toString
public String toString(){
return "A["+getNumeroRue()+","+getNomRue()+"]";
}
}
- @Entity Adresse jest encją bez bezpośredniego powiązania z innymi encjami. Można ją zapisać w bazie danych wyłącznie za pośrednictwem encji Restaurant.
- Adres jest definiowany przez nazwę ulicy (wiersz 16) oraz numer przy ulicy (wiersz 13).
@Entity Plat ma następujący wygląd
package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique=true, length=50, nullable=false)
private String nom;
private boolean vegetarien;
// konstruktory
public Plat() {
}
public Plat(String name, boolean vegetarian) {
...
}
// metody pobierające i ustawiające
...
// toString
public String toString() {
return "E[" + getNom() + "," + isVegetarien() + "]";
}
}
- @Entity Plat jest encją bez bezpośredniego powiązania z innymi encjami. Można ją zapisać w bazie danych wyłącznie za pośrednictwem encji Restaurant.
- Danie jest definiowane przez nazwę (wiersz 12) oraz typ – wegetariańskie lub nie (wiersz 14).
2.7.3. Projekt Eclipse / Hibernate
Wykorzystana tutaj implementacja JPA pochodzi z biblioteki Hibernate. Projekt Eclipse służący do testów wygląda następująco:
![]() |
W [1] znajduje się projekt Eclipse, w [2] kod Java, a w JPA konfiguracja warstwy. Warto zwrócić uwagę na obecność pliku [orm.xml], którego dotychczas nie spotkaliśmy. Projekt ten znajduje się w pliku [3] w folderze przykładów [4]. Zaimportujemy go.
2.7.4. Generowanie pliku DDL z bazy danych
Postępując zgodnie z instrukcjami z punktu 2.1.7, plik DDL uzyskany dla plików SGBD i MySQL5 wygląda następująco:
alter table jpa10_hb_restaurant
drop
foreign key FK3E8E4F5D5FE379D0;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D11F0F78A4;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D1AFAC3E44;
drop table if exists jpa10_hb_adresse;
drop table if exists jpa10_hb_plat;
drop table if exists jpa10_hb_restaurant;
drop table if exists jpa10_hb_restaurant_plat;
create table jpa10_hb_adresse (
id bigint not null auto_increment,
NUMERO_RUE integer,
NOM_RUE varchar(30) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_plat (
id bigint not null auto_increment,
nom varchar(50) not null unique,
vegetarien bit not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant (
id bigint not null auto_increment,
nom varchar(30) not null unique,
adresse_id bigint,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant_plat (
jpa10_hb_restaurant_id bigint not null,
plat_id bigint not null,
primary key (jpa10_hb_restaurant_id, plat_id)
) ENGINE=InnoDB;
alter table jpa10_hb_restaurant
add index FK3E8E4F5D5FE379D0 (adresse_id),
add constraint FK3E8E4F5D5FE379D0
foreign key (adresse_id)
references jpa10_hb_adresse (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D11F0F78A4 (plat_id),
add constraint FK1D2D06D11F0F78A4
foreign key (plat_id)
references jpa10_hb_plat (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id),
add constraint FK1D2D06D1AFAC3E44
foreign key (jpa10_hb_restaurant_id)
references jpa10_hb_restaurant (id);
- wiersze 21–26: tabela [adresse]
- wiersze 28–33: tabela [plat]
- wiersze 35–40: tabela [restaurant]
- wiersze 42–46: tabela połączeń [restaurant_plat]. Należy zwrócić uwagę na klucz złożony (wiersz 45)
- wiersze 48–52: klucz obcy z tabeli [restaurant] do tabeli [adresse]
- wiersze 54–58: klucz obcy z tabeli [restaurant_plat] do tabeli [plat]
- wiersze 60–64: klucz obcy z tabeli [restaurant_plat] do tabeli [restaurant]
Ta tabela DDL odpowiada przedstawionemu wcześniej schematowi:
![]() |
W widoku SQL Explorer baza danych wygląda następująco:
![]() |
- w [1]: 4 tabele bazy danych
- w [2]: adresy
- w [3]: dania
- w [4]: restauracje. [adresse_id] odwołuje się do adresów z [2].
- w [5]: tabela połączeń [restaurant,plat]. [jpa10_hb_restaurant_id] odnosi się do restauracji z [4], a [plat_id] – do dań z [3]. Zatem [1,1] oznacza, że restauracja „Burger Barn” serwuje danie „CheeseBurger”.
Aby uzyskać powyższe dane, uruchomiono program [QueryDB] z projektu Eclipse.
2.7.5. Zapytania JPQL z wykorzystaniem konsoli Hibernate
Tworzymy konsolę Hibernate powiązaną z poprzednim projektem Eclipse. Postępujemy zgodnie z procedurą opisaną już dwukrotnie, w szczególności w paragrafie 2.1.12.
![]() |
- w [1] i [2]: konfiguracja konsoli Hibernate
![]() |
- w [3]: zapytanie JPQL oraz w [4] wynik.
- w [5]: równoważne polecenie SQL
Przedstawiamy teraz serię zapytań JPQL. Zachęcamy czytelnika do ich uruchomienia i odkrycia kolejności SQL wygenerowanej przez Hibernate w celu ich wykonania.
Pobierz wszystkie restauracje wraz z ich daniami:
![]() | ![]() |
Pobierz restauracje serwujące co najmniej jedno danie wegetariańskie:
![]() | ![]() |
Wyświetl nazwy restauracji, które serwują wyłącznie dania wegetariańskie:
![]() | ![]() |
Pobierz listę restauracji serwujących burgery:
![]() | ![]() |
2.7.6. QueryDB
Zwracamy teraz uwagę na program [QueryDB] z projektu Eclipse, który:
- wypełnia bazę
- wysyła do niej pewną liczbę zapytań JPQL. Są one zapisywane w pliku [META-INF/orm.xml] projektu Eclipse:
![]() |
Plik [orm.xml] może służyć do konfiguracji warstwy JPA zamiast adnotacji Java. Zapewnia to elastyczność w konfiguracji warstwy JPA. Można ją modyfikować bez konieczności ponownej kompilacji kodu Java. Można korzystać z obu metod jednocześnie: adnotacji Java oraz pliku [orm.xml]. Konfiguracja JPA jest najpierw tworzona za pomocą adnotacji Java, a następnie za pomocą pliku [orm.xml]. Jeśli więc chcemy zmodyfikować konfigurację utworzoną za pomocą adnotacji Java bez ponownej kompilacji, wystarczy umieścić tę konfigurację w pliku [orm.xml]. To właśnie ten plik będzie miał ostatnie słowo.
W naszym przykładzie plik [orm.xml] służy do zapisywania tekstów zapytań z pliku JPQL. Jego zawartość jest następująca:
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
<description>Restaurants</description>
<named-query name="supprimer le contenu de la table restaurant">
<query>delete from Restaurant</query>
</named-query>
<named-query name="supprimer le contenu de la table plat">
<query>delete from Plat</query>
</named-query>
<named-query name="obtenir tous les restaurants">
<query>select r from Restaurant r order by r.nom asc</query>
</named-query>
<named-query name="obtenir toutes les adresses">
<query>select a from Adresse a order by a.nomRue asc</query>
</named-query>
<named-query name="obtenir tous les plats">
<query>select p from Plat p order by p.nom asc</query>
</named-query>
<named-query name="obtenir tous les restaurants avec leurs plats">
<query>select r.nom,p.nom from Restaurant r join r.plats p</query>
</named-query>
<named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
<query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
</named-query>
<named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
<query>
select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
p1.vegetarien=false)
</query>
</named-query>
<named-query name="obtenir les restaurants d'une certaine rue">
<query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
</named-query>
<named-query name="obtenir les restaurants qui servent des burgers">
<query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
</named-query>
<named-query name="obtenir les plats du restaurant untel">
<query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
</named-query>
</entity-mappings>
- katalogiem głównym pliku [orm.xml] jest <entity-mappings> (wiersz 2).
- wiersze 5–7: wymienione zapytania JPQL są otoczone tagami <named-query name= „... ”>tekst</namedquery>.
- Atrybut name tego tagu to nazwa zapytania.
- Treść texte znacznika to tekst zapytania.
QueryDB wykona powyższe zapytania. Jego kod wygląda następująco:
package tests;
...
public class QueryDB {
// Kontekst trwałości
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = emf.createEntityManager();
public static void main(String[] args) {
// rozpoczęcie transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// usuwanie elementów z tabeli [restaurant]
em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
// usunięcie elementów z tabeli [plat]
em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
// tworzenie obiektów Address
Adresse adr1 = new Adresse(10, "Main Street");
Adresse adr2 = new Adresse(20, "Main Street");
Adresse adr3 = new Adresse(123, "Dover Street");
// tworzenie obiektów typu „Entree”
Plat ent1 = new Plat("Hamburger", false);
Plat ent2 = new Plat("Cheeseburger", false);
Plat ent3 = new Plat("Tofu Stir Fry", true);
Plat ent4 = new Plat("Vegetable Soup", true);
// tworzenie obiektów typu „Restaurant”
Restaurant restaurant1 = new Restaurant();
restaurant1.setNom("Burger Barn");
restaurant1.setAdresse(adr1);
restaurant1.getPlats().add(ent1);
restaurant1.getPlats().add(ent2);
Restaurant restaurant2 = new Restaurant();
restaurant2.setNom("Veggie Village");
restaurant2.setAdresse(adr2);
restaurant2.getPlats().add(ent3);
restaurant2.getPlats().add(ent4);
Restaurant restaurant3 = new Restaurant();
restaurant3.setNom("Dover Diner");
restaurant3.setAdresse(adr3);
restaurant3.getPlats().add(ent1);
restaurant3.getPlats().add(ent2);
restaurant3.getPlats().add(ent4);
// zachowanie obiektów typu „Restaurant” (oraz innych obiektów w ramach kaskady)
em.persist(restaurant1);
em.persist(restaurant2);
em.persist(restaurant3);
// zakończenie transakcji
tx.commit();
// zrzut bazy
dumpDataBase();
// koniec EntityManager
em.close();
// koniec EntityManagerFactory
emf.close();
}
// wyświetlanie zawartości bazy
@SuppressWarnings("unchecked")
private static void dumpDataBase() {
// test2
log("données de la base");
// początek transakcji
EntityTransaction tx = em.getTransaction();
tx.begin();
// wyświetlanie restauracji
log("[restaurants]");
for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
System.out.println(restaurant);
}
// wyświetlenia adresów
log("[adresses]");
for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
System.out.println(adresse);
}
// wyświetlenia dań
log("[plats]");
for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
System.out.println(plat);
}
// wyświetlenia powiązań restauracje <--> dania
log("[restaurants/plats]");
Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
}
log("[Liste des restaurants avec au moins un plat végétarien]");
for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
System.out.println(r);
}
// zapytanie
log("[Liste des restaurants avec seulement des plats végétariens]");
for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
System.out.println(r);
}
// zapytanie
log("[Liste des restaurants dans Dover Street]");
for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
System.out.println(r);
}
// zapytanie
log("[Liste des restaurants ayant un plat de type burger]");
record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
}
// zapytanie
log("[Plats de Veggie Village]");
for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
System.out.println(r);
}
// zakończenie transakcji
tx.commit();
}
// logi
private static void log(String message) {
System.out.println(" -----------" + message);
}
}
Wynik wykonania [QueryDB] jest następujący:
Pozostawiamy czytelnikowi zadanie powiązania kodu z wynikami. W tym celu zalecamy uruchomienie zapytań JPQL w konsoli Hibernate oraz przeanalizowanie odpowiadającego im kodu SQL.
2.7.7. Projekt Eclipse / Toplink
Zainteresowany czytelnik znajdzie w przykładach dostępnych do pobrania wraz z tym samouczkiem poprzedni projekt zaimplementowany przy użyciu Toplink:
![]() |
Projekt Eclipse z Toplinkiem jest kopią projektu Eclipse z Hibernate:
![]() |
Plik <persistence.xml> [2] definiuje zarządzane encje:
<!-- dostawca -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- klasy trwałe -->
<class>entites.Restaurant</class>
<class>entites.Adresse</class>
<class>entites.Plat</class>
...
- wiersze 4–6: encje zarządzane
Zapytania JPQL zapisane w pliku [orm.xml] są poprawnie wykonywane przez Toplink. W tym celu w poprzednim projekcie zadbano o to, aby nie używać zapytań HQL (Hibernate Query Language), które w rzeczywistości stanowią nadzbiór JPQL, a których niektóre elementy składniowe nie są akceptowane przez JPQL.
2.8. Conclusion
Na tym kończymy nasze omówienie encji JPA. Było to długie omówienie, a mimo to nie poruszyliśmy niektórych ważnych kwestii (dla zaawansowanych programistów). Ponownie zalecamy zapoznanie się z podręcznikiem, takim jak ten, który został wykorzystany w niniejszym samouczku:
[ref1]: Java Persistence with Hibernate, autorstwa Christiana Bauera i Gavina Kinga, wydawnictwo Manning.


















































































































