4. Podsumowanie JPA „ ”
Zamierzamy przedstawić JPA (Java Persistence API) na podstawie kilku przykładów. JPA jest omawiane w ramach kursu:
- Praktyczne zastosowanie trwałości danych w Javie 5: [http://tahe.developpez.com/java/jpa] – dostarcza narzędzi do tworzenia warstwy dostępu do danych przy użyciu JPA
4.1. Miejsce JPA w architekturze warstwowej
Zachęcamy czytelnika do ponownego zapoznania się z początkiem niniejszego dokumentu (akapit 2), w którym wyjaśniono rolę warstwy JPA w architekturze warstwowej. Warstwa JPA wpisuje się w warstwy dostępu do danych:
![]() |
Warstwa [DAO] współpracuje ze specyfikacją JPA. Niezależnie od produktu, który ją implementuje, interfejs warstwy JPA udostępniany warstwie [DAO] pozostaje taki sam. Poniżej przedstawiamy kilka przykładów zaczerpniętych z [ref1], które pozwolą nam zbudować naszą własną warstwę JPA.
4.2. JPA – przykłady
4.2.1. Przykład 1 – Reprezentacja obiektowa pojedynczej tabeli
4.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 dane osoby są modyfikowane, jej numer wersji jest zwiększany. | |
imię i nazwisko osoby | |
imię tej osoby | |
data urodzenia | |
liczba całkowita 0 (nieżonaty) lub 1 (żonaty) | |
liczba dzieci tej osoby |
4.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 do momentu pojawienia się wersji JDK 1.5
- za pomocą adnotacji Java od wersji JDK 1.5
W niniejszym dokumencie będziemy korzystać wyłącznie z drugiej metody.
Obiekt [Personne], będący obrazem 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;
// producenci
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órą reprezentuje dana klasa. 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 zrobi to, 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 w kolumnie NOM tabeli [personne].
- length=30 ustawia liczbę znaków w kolumnie NOM na 30. Oznacza to, że typ tej kolumny będzie brzmiał 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. sérialisation obiektu polega na przekształceniu go w ciąg bitów. 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 uzyskiwać 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 uzyska dostęp do pól poprzez metody 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.
4.2.2. Konfiguracja warstwy JPA
Testy warstwy JPA można przeprowadzić przy użyciu następującej architektury:
![]() |
- 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
Konfiguracja warstwy JPA jest zapewniona przez plik [META-INF/persistence.xml]:
![]() |
Podczas uruchamiania program wyszukuje plik [META-INF/persistence.xml] w pliku Classpath aplikacji.
Przyjrzyjmy się konfiguracji warstwy JPA zdefiniowanej 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 z wykorzystaniem 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 (transakcja Java 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ż korzystamy z 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ąć, jakie polecenia SQL wygeneruje Hibernate 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
- wiersz 22: Hibernate musi znać identyfikator SGBD, z którym ma do czynienia. Wszystkie identyfikatory 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 podaje się maksymalną liczbę przygotowanych poleceń SQL, które może pomieścić pamięć podręczna, uwzględniając wszystkie połączenia (przygotowane polecenie SQL należy do jednego połączenia).
- wiersz 28: częstotliwość sprawdzania ważności połączeń w milisekundach. Połączenie z puli może stać się nieważne z różnych powodów (sterownik JDBC unieważnia połączenie, ponieważ trwa ono zbyt długo, sterownik JDBC wykazuje „błędy”, ...).
- 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 wydawania poleceń SQL dotyczących 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żyty w tym przykładzie 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 danych w środowisku produkcyjnym...
4.2.3. Przykład 2: relacja „jeden do wielu”
4.2.3.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).
4.2.3.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;
// relacja główna Artykuł (wiele) -> Kategoria (jeden)
// zrealizowana za pomocą klucza obcego (categorie_id) w Artykule
// 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órej 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ła 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 określa, że operacje (persist, merge, remove) wykonywane na @Entity Categorie mają być kaskadowo stosowane na jej artykułach.
- wiersz 24: artykuły z 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 artykułu w obiekcie Set<Article>. Co oznacza „ten sam artykuł”? Aby stwierdzić, że artykuł a jest taki sam jak artykuł 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 mają tę samą lokalizację w pamięci. Można by chcieć stwierdzić, że elementy a i b są takie same, jeśli mają tę samą nazwę. W takim przypadku programista musi przedefiniować dwie metody w klasie [Article]:
- equals: musi zwracać wartość prawdziwą, 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 artykułu. 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].
4.3. API warstwy JPA
Wyjaśnijmy ś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 danych na podstawie jego klucza głównego. Typ T obiektu pozwala warstwie JPA ustalić, 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, o ile z tą różnicą, że pobiera obiekty zamiast tabel. | |
metoda analogiczna do poprzedniej, z tą różnicą, że queryText jest polecenie SQL, a nie JPQL. | |
metoda identyczna jak createQuery, z tą różnicą, że kolejność JPQL queryText została 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 ten sposób 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("nom d'une unité de persistance");
- javax.persistence.Persistence to klasa statyczna służąca do uzyskania 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="elections-dao-jpa-mysql-01PU" transaction-type="RESOURCE_LOCAL">
W powyższym przykładzie jednostka trwałości nosi nazwę elections-dao-jpa-mysql-01PU. Towarzyszy jej cała specyficzna dla niej konfiguracja, w szczególności obiekt SGBD, z którym współpracuje. Instrukcja [Persistence.createEntityManagerFactory("elections-dao-jpa-mysql-01PU")] 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 elections-dao-jpa-mysql-01PU. 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]:
Istnieją dwie możliwe wartości dla flushmode: FlushModeType.AUTO (domyślnie): synchronizacja odbywa się przed każdego zapytania SELECT skierowanego do bazy danych. FlushModeType.COMMIT: synchronizacja odbywa się dopiero po zakończeniu transakcji w bazie danych. | |
określa 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:
![]() |
- 1 – metoda getResultList wykonuje polecenie SELECT, które 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()) {
// wywołanie 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) {
// wykorzystaj 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.
4.4. Zapytania JPQL
JPQL (Java Persistence Query Language) to język zapytań warstwy JPA. Język JPQL jest powiązany z językiem SQL stosowanym w bazach danych. Podczas gdy SQL obsługuje tabele, JPQL obsługuje obiekty obrazów z tych tabel. Przyjrzyjmy się przykładowi w ramach następującej architektury:
![]() |
Baza danych, którą nazwiemy [dbrdvmedecins2] , jest bazą danych MySQL5 zawierającą cztery tabele:
![]() |
Zawiera informacje umożliwiające zarządzanie wizytami grupy lekarzy.
4.4.1. Tabela [MEDECINS]
Zawiera informacje o lekarzach.
![]() | ![]() |
- ID: numer identyfikacyjny lekarza – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko lekarza
- PRENOM: jego imię
- TITRE: jego tytuł (panna, pani, pan)
4.4.2. Tabela [CLIENTS]
Pacjenci poszczególnych lekarzy są zapisani w tabeli [CLIENTS]:
![]() | ![]() |
- ID: numer identyfikacyjny pacjenta – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko klienta
- PRENOM: imię klienta
- TITRE: tytuł (panna, pani, pan)
4.4.3. Tabela [CRENEAUX]
Zawiera listę przedziałów czasowych, w których możliwe jest użycie RV:
![]() |
![]() |
- ID: numer identyfikujący przedział czasowy – klucz główny tabeli (wiersz 8)
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS (ID).
- HDEBUT: godzina rozpoczęcia terminu
- MDEBUT: minuty początku przedziału czasowego
- HFIN: godzina zakończenia przedziału czasowego
- MFIN: minuty zakończenia przedziału czasowego
Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).
4.4.4. Tabela [RV]
Zawiera listę RV przypisanych każdemu lekarzowi:
![]() |
- ID: numer jednoznacznie identyfikujący RV – klucz główny
- JOUR: dzień, w którym wystawiono RV
- ID_CRENEAU: przedział czasowy dla RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
- ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]
Ta tabela posiada , która wymusza unikalność wartości połączonych kolumn (JOUR, ID_CRENEAU):
Jeśli wiersz w tabeli [RV] ma wartość (JOUR1, ID_CRENEAU1) w kolumnach (JOUR, ID_CRENEAU), to ta wartość nie może występować nigdzie indziej. W przeciwnym razie oznaczałoby to, że dwa rekordy o wartości RV zostały zarejestrowane w tym samym czasie dla tego samego lekarza. Z punktu widzenia programowania w Javie sterownik bazy danych o wartości JDBC uruchamia sterownik o wartości SQLException, gdy wystąpi taka sytuacja.
Wiersz id równy 3 (por. [1] powyżej) oznacza, że rezerwacja o numerze RV została dokonana na przedział czasowy nr 20 dla klienta nr 4 w dniu 23.08.2006 r. Z tabeli [CRENEAUX] wynika, że przedział nr 20 odpowiada przedziałowi czasowemu 16:20–16:40 i należy do lekarza nr 1 (pani Marie PELISSIER). Z tabeli [CLIENTS] wynika, że klient nr 4 to panna Brigitte BISTROU.
4.4.5. Tworzenie bazy danych
Aby utworzyć tabele i je wypełnić, można użyć skryptu [dbrdvmedecins2.sql]. Korzystając ze skryptu [WampServer], można postępować w następujący sposób:
![]() |
- w [1] należy kliknąć ikonę [WampServer] i wybrać opcję [PhpMyAdmin] [2],
- w [3], w oknie, które się otworzyło, należy wybrać link [Bases de données],
![]() |
- do [2], tworzymy bazę danych o nazwie [4] i kodowaniu [5],
- w [7] baza została utworzona. Klikamy na jej link,
![]() |
- w [8] importujemy plik SQL,
- który wybieramy w systemie plików za pomocą przycisku [9],
![]() |
- w pliku [11] wybieramy skrypt SQL, a w pliku [12] uruchamiamy go,
- w [13] utworzono cztery tabele bazy danych. Klikamy jeden z linków,
![]() |
- w [14], zawartość tabeli.
W dalszej części nie będziemy już wracać do tej bazy danych. Zachęcamy jednak czytelnika do śledzenia jej rozwoju w miarę postępów w programach, zwłaszcza gdy coś nie działa.
4.4.6. Warstwa [JPA]
Wróćmy do architektury przykładu:
![]() |
Teraz tworzymy projekt Maven dla warstwy [JPA].
4.4.7. Projekt NetBeans
Wygląda on następująco:
![]() |
- w [1] tworzymy projekt Maven typu [Java Application] [2],
- w [3] nadajemy nazwę projektowi,
![]() |
- w [4] generowany jest projekt.
4.4.8. Generowanie warstwy [JPA]
Wróćmy do architektury, którą musimy zbudować:
![]() |
Za pomocą NetBeans można automatycznie wygenerować warstwę [JPA]. Warto zapoznać się z tymi metodami automatycznego generowania, ponieważ wygenerowany kod dostarcza cennych wskazówek dotyczących sposobu pisania elementów JPA.
4.4.9. Tworzenie połączenia NetBeans z bazą danych
- uruchom SGBD MySQL 5, aby BD był dostępny,
- utwórz połączenie NetBeans z bazą danych [dbrdvmedecins2],
![]() |
- w zakładce [Services] [1], w gałęzi [Databases] [2], należy wybrać sterownik JDBC MySQL [3],
- a następnie wybrać opcję [4] „Connect Using”, umożliwiającą nawiązanie połączenia z bazą danych MySQL,
- w polu [5] należy podać wymagane informacje. W polu [6] należy wpisać nazwę bazy danych, w polu [7] – nazwę użytkownika bazy danych i hasło,
- w [8] można sprawdzić podane dane,
- w [9] wyświetla się oczekiwany komunikat, jeśli dane są poprawne,
![]() |
- w [10] nawiązano połączenie. Widoczne są cztery tabele podłączonej bazy danych.
4.4.10. Tworzenie jednostki trwałości
Wróćmy do tworzonej architektury:
![]() |
Tworzymy właśnie warstwę [JPA]. Jej konfiguracja odbywa się w pliku [persistence.xml], w którym definiuje się jednostki trwałości. Każda z nich wymaga następujących informacji:
- charakterystyki JDBC dostępu do bazy danych (URL, nazwa użytkownika, hasło),
- klasy, które będą odzwierciedlać tabele bazy danych,
- wykorzystaną implementację JPA. JPA jest bowiem specyfikacją implementowaną przez różne produkty. W tym przypadku wykorzystamy Hibernate.
NetBeans może wygenerować ten plik trwałości za pomocą kreatora.
![]() |
- kliknij prawym przyciskiem myszy na projekt i wybierz opcję utworzenia jednostki trwałości [1],
- w [2], utworzyć jednostkę trwałości,
![]() |
- w [3] nadać nazwę tworzonej jednostce trwałości,
- w [4], wybrać implementację JPA Hibernate (JPA 2.0),
- w [5] należy zaznaczyć, że tabele z BD zostały już utworzone i dlatego nie będą tworzone ponownie. Potwierdzamy działania kreatora,
- w pliku [6], w nowym projekcie,
- w [7] plik [persistence.xml] został wygenerowany w folderze [META-INF],
- w [8] do projektu Maven dodano nowe zależności.
Wygenerowany plik [META-INF/persistence.xml] ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
</properties>
</persistence-unit>
</persistence>
Zawiera on informacje podane w kreatorze:
- wiersz 3: nazwa jednostki trwałości,
- wiersz 3: typ transakcji z bazą danych. W tym przypadku RESOURCE_LOCAL oznacza, że aplikacja będzie samodzielnie zarządzać transakcjami,
- wiersze 6–9: właściwości źródła danych JDBC.
W zakładce [Design] można uzyskać ogólny przegląd pliku [persistence.xml]:
![]() |
Aby uzyskać logi Hibernate, uzupełniamy plik [persistence.xml] w następujący sposób:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
- wiersz 11: wyświetlane są polecenia SQL wygenerowane przez Hibernate,
- wiersz 12: ta właściwość umożliwia sformatowane wyświetlenie tych poleceń.
Do projektu dodano zależności. Plik [pom.xml] ma następującą treść:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-jpql-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-rdvmedecins-jpql-hibernate</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.0.GA</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.1_spec</artifactId>
<version>1.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>2.7.7</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.15.0-GA</version>
</dependency>
<dependency>
<groupId>org.hibernate.common</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>4.0.1.Final</version>
</dependency>
</dependencies>
</project>
Wszystkie dodane zależności dotyczą biblioteki Hibernate o nazwie ORM. Dodamy zależność od sterownika JDBC z biblioteki MySQL:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
4.4.11. Generowanie elementów JPA
Elementy JPA można wygenerować za pomocą kreatora w NetBeans:
![]() |
- w [1] tworzy się elementy JPA na podstawie bazy danych,
![]() |
- w pliku [2] wybiera się utworzone wcześniej połączenie [dbrdvmedecins2],
- w [3] wybiera się wszystkie tabele powiązanej bazy danych,
![]() |
- w [4] nadajemy nazwę klasom Java powiązanym z czterema tabelami,
- a także nazwę pakietu [5],
- w [6], JPA grupuje wiersze tabel z BD w kolekcjach. Wybieramy listę jako kolekcję,
![]() |
- w [7], klasy Java utworzone przez kreatora.
4.4.12. Wygenerowane encje JPA
Entyteta [Medecin] jest odwzorowaniem tabeli [medecins]. Klasa Java jest przepełniona adnotacjami, które na pierwszy rzut oka utrudniają czytanie kodu. Jeśli zachowamy tylko to, co jest niezbędne do zrozumienia roli tej entytety, otrzymamy następujący kod:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// konstruktory
....
// metody pobierające i ustawiające
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- W wierszu 4 adnotacja @Entity sprawia, że klasa [Medecin] staje się encją JPA, c.a.d. klasa powiązana z tabelą BD poprzez API i JPA,
- w wierszu 5 podano nazwę tabeli BD powiązanej z encją JPA. Każde pole tabeli odpowiada polu w klasie Java,
- w wierszu 6 klasa implementuje interfejs Serializable. Jest to konieczne w aplikacjach typu klient/serwer, gdzie encje są serializowane między klientem a serwerem.
- wiersze 10–11: pole id klasy [Medecin] odpowiada polu [ID] (wiersz 10) tabeli [medecins],
- wiersze 13–14: pole „tytuł” klasy [Medecin] odpowiada polu [TITRE] (wiersz 13) w tabeli [medecins],
- wiersze 16–17: pole „nazwa” klasy [Medecin] odpowiada polu [NOM] (wiersz 16) w tabeli [medecins],
- wiersze 19–20: pole „wersja” klasy [Medecin] odpowiada polu [VERSION] (wiersz 19) w tabeli [medecins]. W tym przypadku kreator nie rozpoznaje, że kolumna ta jest w rzeczywistości kolumną wersji, której wartość powinna być zwiększana przy każdej modyfikacji wiersza, do którego należy. Aby nadać jej tę rolę, należy dodać adnotację @Version. Zrobimy to w kolejnym kroku,
- wiersze 22–23: pole „prenom” klasy [Medecin] odpowiada polu [PRENOM] w tabeli [medecins],
- wiersze 10–11: pole „id” odpowiada kluczowi pierwotnemu [ID] tabeli. Adnotacje w wierszach 8–9 wyjaśniają tę kwestię,
- wiersz 8: adnotacja @Id wskazuje, że adnotowane pole jest powiązane z kluczem głównym tabeli,
- wiersz 9: warstwa [JPA] wygeneruje klucz główny dla wierszy, które wstawi do tabeli [Medecins]. Istnieje kilka możliwych strategii. W tym przypadku strategia GenerationType.IDENTITY wskazuje, że warstwa JPA będzie korzystać z trybu auto_increment tabeli MySQL,
- wiersze 25–26: tabela [creneaux] posiada klucz obcy do tabeli [medecins]. Jeden termin należy do jednego lekarza. I odwrotnie, jeden lekarz ma przypisanych kilka terminów. Mamy zatem relację „jeden (lekarz) do wielu (terminów)”, relację określoną przez adnotację @OneToMany przez JPA (wiersz 25). Pole w wierszu 26 będzie zawierało wszystkie terminy tego lekarza. Odbywa się to bez konieczności programowania. Aby w pełni zrozumieć wiersz 25, musimy przedstawić klasę [Creneau].
Wygląda ona następująco:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Komentujemy tylko nowe adnotacje:
- wspomnieliśmy, że tabela [creneaux] posiada klucz obcy do tabeli [medecins]: jeden termin jest powiązany z jednym lekarzem. Z tym samym lekarzem może być powiązanych kilka terminów. Mamy relację z tabeli [creneaux] do tabeli [medecins], która jest określona jako relacja typu „wiele (terminów)” do „jednego (lekarza)”. Adnotacja @ManyToOne w wierszu 32 służy do określenia klucza obcego,
- Wiersz 31 z adnotacją @JoinColumn określa relację klucza obcego: kolumna [ID_MEDECIN] w tabeli [creneaux] jest kluczem obcym w kolumnie [ID] w tabeli [medecins],
- wiersz 33: odniesienie do lekarza będącego właścicielem terminu. Uzyskuje się to również bez konieczności programowania.
Powiązanie klucza obcego między encją [Creneau] a encją [Medecin] jest zatem odzwierciedlone przez dwa adnotacje:
- w encji [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- w encji [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Oba adnotacje odzwierciedlają tę samą relację: relację klucza obcego z tabeli [creneaux] do tabeli [medecins]. Mówi się, że są one odwrotne względem siebie. Jedynie relacja @ManyToOne jest niezbędna. Jednoznacznie określa ona relację klucza obcego. Relacja @OneToMany jest opcjonalna. Jeśli występuje, ogranicza się jedynie do odwołania do relacji @ManyToOne, z którą jest powiązana. Takie jest znaczenie atrybutu mappedBy w wierszu 1 encji [Medecin]. Wartością tego atrybutu jest nazwa pola w encji [Creneau], które posiada adnotację @ManyToOne określającą klucz obcy. Również w tym samym wierszu 1 encji [Medecin] atrybut cascade=CascadeType.ALL określa zachowanie encji [Medecin] względem encji [Creneau]:
- jeśli do bazy zostanie wstawiony nowy element [Medecin], wówczas elementy [Creneau] z pola w wierszu 2 również muszą zostać wstawione,
- jeśli zmodyfikuje się entytę [Medecin] w bazie, wówczas entytę [Creneau] z pola w wierszu 2 również należy zmodyfikować,
- jeśli usunie się encję [Medecin] z bazy, wówczas encje [Creneau] z pola w wierszu 2 również muszą zostać usunięte.
Podajemy kod dwóch pozostałych jednostek bez szczególnych komentarzy, ponieważ nie wprowadzają one nowych oznaczeń.
Jednostka [Client]
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- wiersze 24–25 odzwierciedlają relację klucza obcego między tabelą [rv] a tabelą [clients].
Entyteta [Rv]:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- wiersz 13 określa pole „dzień” typu Java w tabeli Date. Wskazuje się, że w tabeli [rv] kolumna [JOUR] (wiersz 12) jest typu data (bez godziny),
- wiersze 16–18: definiują relację klucza obcego między tabelą [rv] a tabelą [creneaux],
- wiersze 20–22: określają relację klucza obcego między tabelą [rv] a tabelą [clients].
Automatyczne wygenerowanie encji JPA pozwala nam uzyskać bazę roboczą. Czasami jest to wystarczające, a czasami nie. Tak jest w tym przypadku:
- należy dodać adnotację @Version do poszczególnych pól wersji encji,
- należy napisać metody toString bardziej jednoznaczne niż te wygenerowane,
- entytety [Medecin] i [Client] są analogiczne. Wyprowadzimy je z klasy [Personne],
- usuniemy relacje @OneToMany, które są odwrotne do relacji @ManyToOne. Nie są one niezbędne i powodują komplikacje programistyczne,
- usuwamy walidację @NotNull dotyczącą kluczy głównych. Kiedy zapisujemy encję JPA wraz z MySQL, encja ta ma początkowo klucz główny null. Dopiero po zapisaniu w bazie danych klucz podstawowy zapisanego elementu otrzymuje wartość.
Zgodnie z tymi specyfikacjami poszczególne klasy przedstawiają się następująco:
Klasa „Osoba” służy do reprezentowania lekarzy i klientów:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@Column(name = "PRENOM")
private String prenom;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- wiersz 6: należy zauważyć, że klasa [Personne] sama w sobie nie jest encją (@Entity). Będzie ona klasą nadrzędną dla encji. Adnotacja @MappedSuperClass wskazuje na tę sytuację.
Entyteta [Client] zawiera wiersze tabeli [clients]. Wywodzi się ona z poprzedniej klasy [Personne]:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// konstruktory
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- wiersz 6: klasa [Client] jest encją JPA,
- wiersz 7: jest powiązana z tabelą [clients],
- wiersz 8: wywodzi się z klasy [Personne].
Entyteta [Medecin], która hermetyzuje wiersze tabeli [medecins], jest zbudowana według tego samego wzorca:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// konstruktory
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
Entyteta [Creneau] zawiera wiersze tabeli [creneaux]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Ostrzeżenie – ta metoda nie zadziała, jeśli pola id nie są ustawione
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- wiersze 40–42 modelują relację „wiele do jednego”, która istnieje między tabelą [creneaux] a tabelą [medecins] w bazie danych: jeden lekarz ma kilka terminów, a jeden termin należy do jednego lekarza.
Entyteta [Rv] zawiera wiersze tabeli [rv]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// konstruktory
...
// metody pobierające i ustawiające
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- wiersze 27–29 modelują relację „wiele do jednego”, która istnieje między tabelą [rv] a tabelą [clients] (jeden klient może występować w wielu rezerwacjach) w bazie danych, a wiersze 23–25 modelują relację „wiele do jednego” występującą między tabelą [rv] a tabelą [creneaux] (jeden przedział czasowy może występować w wielu rezerwacjach).
4.4.13. Kod dostępu do danych
Teraz dodamy do projektu kod umożliwiający dostęp do danych za pośrednictwem warstwy JPA:
![]() |
![]() |
Klasa [MainJpql] wygląda następująco:
package rdvmedecins.console;
import java.util.Scanner;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class MainJpql {
public static void main(String[] args) {
// EntityManagerFactory
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-rdvmedecins-jpql-hibernatePU");
// entityManager
EntityManager em = emf.createEntityManager();
// skaner klawiatury
Scanner clavier = new Scanner(System.in);
// pętla wprowadzania zapytań JPQL
System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
String requete = clavier.nextLine();
while (!requete.trim().equals("*")) {
try {
// wyświetlanie wyników zapytania
for (Object o : em.createQuery(requete).getResultList()) {
System.out.println(o);
}
} catch (Exception e) {
System.out.println("L'exception suivante s'est produite : " + e);
}
// czyści się kontekst trwałości
em.clear();
// nowe zapytanie
System.out.println("---------------------------------------------");
System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
requete = clavier.nextLine();
}
// zamknięcie zasobów
em.close();
emf.close();
}
}
- wiersz 12: utworzenie klasy EntityManagerFactory powiązanej z jednostką trwałości, którą utworzyliśmy wcześniej. Parametrem metody createEntityManagerFactory jest nazwa tej jednostki trwałości:
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
...
</persistence-unit>
- wiersz 14: utworzenie EntityManager, który zarządza warstwą trwałości,
- wiersz 19: wprowadzenie zapytania JPQL select,
- wiersze 23–28: wyświetlenie wyniku zapytania,
- wiersz 20: wprowadzanie danych zostaje przerwane, gdy użytkownik wpisze *.
Pytanie: podaj zapytania JPQL pozwalające uzyskać następujące informacje:
- lista lekarzy w porządku malejącym według nazwisk
- lista lekarzy, których tytuł to „Mr”
- lista terminów wizyt pani Pelissier
- lista umówionych wizyt w porządku rosnącym według dni
- lista klientów (nazwiska), którzy umówili się na wizytę z panią PELISSIER w dniu 24.08.2006 r.
- liczba klientów pani PELISSIER w dniu 24.08.2006
- klienci, którzy nie umówili się na wizytę
- lekarze, którzy nie mają umówionych wizyt
Należy kierować się przykładem z paragrafu 2.7 dokumentu [ref1]. Oto przykładowe wykonanie:
- wiersz 2: zapytanie JPQL,
- wiersze 3–11: odpowiadające zapytanie SQL,
- wiersze 12–15: wynik zapytania JPQL.
4.5. Powiązania między kontekstem trwałości a SGBD
4.5.1. Klasa Osoba
4.5.2. Program testowy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
4.5.3. Konfiguracja Hibernate
4.5.4. Konfiguracja log4j.properties
4.5.5. Wyniki
Pytanie: proszę powiązać kod Java z wyświetlonymi wynikami.





































