5. Wersja 1: Architektura Spring / JPA
Proponujemy napisanie aplikacji konsolowej oraz aplikacji graficznej umożliwiającej sporządzenie listy płac opiekunek zatrudnionych przez „Dom Małego Dziecka” w danej gminie. Aplikacja ta będzie miała następującą architekturę:
![]() |
5.1. BD Baza danych
Dane statyczne potrzebne do sporządzenia listy płac zostaną umieszczone w bazie danych, którą w dalszej części będziemy nazywać dbpam. Baza ta może zawierać następujące tabele:
Struktura:
klucz główny | |
numer wersji – zwiększa się przy każdej zmianie w wierszu | |
numer ubezpieczenia społecznego pracownika – unikalny | |
imię i nazwisko pracownika | |
jego imię | |
jego adres | |
jego miasto | |
jego kod pocztowy | |
klucz obcy w polu [ID] tabeli [INDEMNITES] |
Jego zawartość mogłaby wyglądać następująco:

Struktura:
klucz główny | |
numer wersji – zwiększa się przy każdej zmianie w wierszu | |
procent: ogólna składka społeczna + składka na spłatę długu społecznego | |
procent: ogólna składka na ubezpieczenie społeczne podlegająca odliczeniu | |
procent: ubezpieczenie społeczne, renta wdowia, emerytura | |
procent: dodatkowa emerytura + ubezpieczenie od bezrobocia |
Jego treść mogłaby wyglądać następująco:
![]()
Stawki składek na ubezpieczenie społeczne są niezależne od pracownika. Powyższa tabela zawiera tylko jeden wiersz.
klucz główny | ||
numer wersji – zwiększa się przy każdej zmianie wiersza | ||
indeks przetwarzania – unikalny | ||
Cena netto w euro za godzinę dyżuru | ||
dodatek na utrzymanie w euro za dzień opieki | ||
dodatek na posiłki w euro za dzień opieki | ||
dodatek urlopowy. Jest to procent, który należy zastosować do wynagrodzenia podstawowego. | ||
Jego treść mogłaby wyglądać następująco:

Należy zauważyć, że dodatki mogą się różnić w zależności od opiekunki. Są one bowiem powiązane z konkretną opiekunką poprzez jej wskaźnik wynagrodzenia. Tak więc pani Marie Jouveinal, której wskaźnik wynagrodzenia wynosi 2 (tabela EMPLOYES), otrzymuje wynagrodzenie godzinowe w wysokości 2,1 euro (tabela INDEMNITES).
5.2. Sposób obliczania wynagrodzenia opiekunki do dzieci
Poniżej przedstawiamy sposób obliczania miesięcznego wynagrodzenia opiekunki do dzieci. Nie jest to sposób stosowany w rzeczywistości. Jako przykład posłużymy się wynagrodzeniem pani Marie Jouveinal, która przepracowała 150 godzin w ciągu 20 dni w miesiącu rozliczeniowym.
Uwzględniono następujące elementy: | | |
Wynagrodzenie podstawowe opiekunki do dzieci oblicza się według następującego wzoru: | ||
Pewna kwota składek na ubezpieczenie społeczne należy potrącić z tego wynagrodzenia : | | |
Łączna kwota składek na ubezpieczenie społeczne: | ||
Ponadto opiekunka ma prawo do dodatku na utrzymanie oraz dodatku na posiłki za każdy przepracowany dzień. W związku z tym otrzymuje następujące dodatki: | | |
Ostatecznie wynagrodzenie netto przysługujące opiekunce nad dziećmi wynosi: |
5.3. Działanie aplikacji konsolowej
Oto przykład uruchomienia aplikacji konsolowej w oknie DOS:
Napiszemy program, który będzie pobierał następujące informacje:
- numer ubezpieczenia społecznego opiekunki (w przykładzie 254104940426058 – wiersz 1)
- łączna liczba przepracowanych godzin (w przykładzie 150 – wiersz 1)
- łączna liczba przepracowanych dni (w przykładzie 20 – wiersz 1)
Widać, że:
- wiersze 9–14: wyświetlają informacje dotyczące pracownika, którego numer ubezpieczenia społecznego został podany
- wiersze 17–20: zawierają stawki różnych składek
- wiersze 23–26: zawierają dodatki związane z indeksem wynagrodzenia pracownika (w tym przypadku indeks 2)
- wiersze 29–33: zawierają składniki wynagrodzenia do wypłaty
Aplikacja sygnalizuje ewentualne błędy:
Wywołanie bez parametrów:
dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar
Syntaxe : pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés
Wywołanie z błędnymi danymi:
dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150x 20x
Le nombre d'heures travaillées [150x] est erroné
Le nombre de jours travaillés [20x] est erroné
Wezwanie z błędnym numerem ubezpieczenia społecznego:
dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar xx 150 20
L'erreur suivante s'est produite : L'employé de n°[xx] est introuvable
5.4. Działanie aplikacji graficznej
Aplikacja graficzna umożliwia obliczanie wynagrodzeń opiekunek do dzieci za pomocą formularza Swing:
![]() |
- informacje przekazywane wcześniej jako parametry do programu konsolowego są teraz wprowadzane za pomocą pól wprowadzania danych [1, 2, 3].
- Przycisk [4] uruchamia obliczenie wynagrodzenia
- formularz wyświetla różne składniki wynagrodzenia, aż do kwoty wynagrodzenia netto do wypłaty [5]
Lista rozwijana [1, 6] nie wyświetla numerów identyfikacyjnych SS pracowników, lecz ich nazwiska i imiona. Zakłada się tutaj, że nie ma dwóch pracowników o tym samym nazwisku i imieniu.
5.5. Tworzenie bazy danych
Uruchamiamy WampServer i korzystamy z narzędzia PhpMyAdmin [1]:
![]() |
- w [2] wybieramy opcję [Bases de données],
![]() |
- w [3] tworzymy bazę danych [dbpam_hibernate],
- w [4] pojawia się utworzona baza danych. Wybieramy ją,
![]() |
- w [5], chcemy zaimportować skrypt SQL,
- w [6], używamy przycisku [Parcourir], aby wskazać plik,
![]() |
- w [7,8] wybieramy skrypt SQL,
- w [9] uruchamia się go,
![]() |
- w [10], tabele zostały utworzone. Ich zawartość jest następująca:
tabela EMPLOYES

tabela INDEMNITES

tabela COTISATIONS
![]()
5.6. Implementacja JPA
5.6.1. Warstwa JPA / Hibernate
Skonfigurujemy warstwę JPA w następującym środowisku:
![]() |
Program konsolowy będzie współpracował z bazą danych. W tym celu konieczne jest:
- posiadać bazę danych,
- posiadać sterownik JDBC dla SGBD, w tym przypadku MySQL,
- zaimplementować warstwę JPA przy użyciu Hibernate,
- napisać program konsolowy.
Tworzymy projekt Maven o nazwie [mv-pam-jpa-hibernate] [1]:
![]() |
W architekturze naszej aplikacji potrzebujemy następujących elementów:
- baza danych,
- sterownik JDBC dla SGBD MySQL,
- warstwa JPA / Hibernate (entities i konfiguracja),
- program konsolowy do testowania.
5.6.1.1. Baza danych
Najpierw utwórzmy pustą bazę danych. Uruchamiamy WampServer i korzystamy z narzędzia PhpMyAdmin oraz [1]:
![]() |
- w [2] wybieramy opcję [Bases de données],
![]() |
- w [3] tworzy się bazę danych [dbpam_hibernate],
- w przypadku [4] – utworzona baza danych.
5.6.1.2. Konfiguracja warstwy JPA
Połączenie między warstwą JDBC a bazą danych odbywa się za pośrednictwem pliku [persistence.xml], który konfiguruje warstwę JPA. Plik ten można utworzyć za pomocą programu NetBeans:
![]() |
- w zakładce [services] [1] nawiązuje się połączenie z bazą danych za pomocą sterownika JDBC z MySQL [2],
- w [3] należy podać nazwę bazy danych, z którą chcemy się połączyć.
- w [4] – nazwę bazy danych,
- w [5] logujemy się jako root bez hasła,
- w [6] można przetestować połączenie,
- w [7] połączenie się powiodło.
![]() |
- połączenie pojawia się w [8] i w [9],
- w [10] dodajemy nowy element do projektu,
![]() |
- w [11] wybiera się kategorię [Persistence], a w [12] element [Persistence Unit],
- w [13] nadajemy nazwę tej jednostce trwałości,
- w [14] wybieramy implementację Hibernate,
- w [15] wskazujemy właśnie utworzone połączenie z bazą danych MySQL,
- w [16] określamy, że podczas instancjonowania warstwy JPA ma ona utworzyć (create) tabele odpowiadające encjom JPA projektu.
Na zakończenie pracy kreatora generowany jest plik [persistence.xml]:
![]() |
- plik pojawia się w nowej gałęzi projektu, w folderze [META-INF] [1],
- który odpowiada folderowi [src/main/resources] w projekcie [2,3].
Jego zawartość jest następująca:
<?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-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
<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.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
</persistence>
- wiersz 3: nazwa jednostki trwałości i typ transakcji. RESOURCE_LOCAL wskazuje, że projekt samodzielnie zarządza transakcjami. W tym przypadku zadanie to musi wykonać program konsolowy,
- wiersz 4: zastosowaną implementacją JPA jest Hibernate,
- wiersze 6–9: parametry połączenia z bazą danych JDBC,
- wiersz 11: żądanie utworzenia tabel odpowiadających encjom JPA. W rzeczywistości NetBeans generuje tutaj błędną konfigurację. Konfiguracja powinna wyglądać następująco:
<property name="hibernate.hbm2ddl.auto" value="create"/>
Dzięki opcji „create” Hibernate, podczas instancjonowania warstwy JPA, usuwa, a następnie tworzy tabele odpowiadające encjom JPA. Opcja „create-drop” działa tak samo, ale po zakończeniu cyklu życia warstwy JPA usuwa wszystkie tabele. Istnieje jeszcze jedna opcja:
<property name="hibernate.hbm2ddl.auto" value="update"/>
Ta opcja tworzy tabele, jeśli jeszcze nie istnieją, ale nie usuwa ich, jeśli już istnieją.
Dodamy trzy kolejne właściwości do konfiguracji Hibernate:
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
Wymagają one od Hibernate wyświetlenia poleceń SQL, które wysyła do bazy danych. Pełny plik wygląda zatem następująco:
<?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-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>jpa.Cotisation</class>
<class>jpa.Employe</class>
<class>jpa.Indemnite</class>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
<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.hbm2ddl.auto" value="create"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
5.6.1.3. Zależności
Wróćmy do architektury projektu:
![]() |
Skonfigurowaliśmy warstwę JPA za pomocą pliku [persistence.xml]. Jako implementację wybrano bibliotekę Hibernate. Spowodowało to pojawienie się zależności w projekcie:
![]() |
Zależności te wynikają z włączenia biblioteki Hibernate do projektu. Musimy dodać kolejną zależność – sterownik JDBC od MySQL, który implementuje warstwę JDBC architektury. Modyfikujemy plik [pom.xml] w następujący sposób:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.2</version>
</dependency>
...
<dependency>
<groupId>org.hibernate.common</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>4.0.1.Final</version>
</dependency>
</dependencies>
Wiersze 8–12 dodają zależność sterownika JDBC od MySQL.
5.6.1.4. Elementy JPA
![]() |
Pytanie: Postępując zgodnie z procedurą przedstawioną w przykładzie z paragrafu 4.4, wygeneruj elementy [Cotisation, Indemnite, Employe].
Uwagi:
- entytety będą częścią pakietu o nazwie [jpa],
- każda jednostka będzie miała numer wersji,
- jeśli dwa elementy są powiązane relacją, utworzona zostanie wyłącznie relacja główna @ManyToOne. Relacja odwrotna @OneToMany nie zostanie utworzona.
5.6.1.5. Kod klasy głównej
Do projektu dołączamy wcześniej opracowane encje JPA i [1]:
![]() |
a następnie dodajemy [2], następującą klasę [main.Main]:
package main;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class Main {
public static void main(String[] args) {
// wystarczy utworzyć Entity Manager, aby zbudować warstwę JPA
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
EntityManager em=emf.createEntityManager();
// zwolnienie zasobów
em.close();
emf.close();
}
}
- wiersz 10: tworzymy klasę EntityManagerFactory dla jednostki trwałości o nazwie [mv-pam-jpa-hibernatePU]. Nazwa ta pochodzi z pliku [persistence.xml]:
<persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
...
</persistence-unit>
- wiersz 12: tworzony jest plik EntityManager. Ta operacja powoduje utworzenie warstwy JPA. Plik [persistence.xml] zostanie przetworzony, w związku z czym zostaną utworzone tabele bazy danych,
- wiersze 14–15: zwolniono zasoby.
5.6.1.6. Tests
Wróćmy do architektury naszego projektu:
![]() |
Wszystkie warstwy zostały zaimplementowane. Uruchamiamy projekt [2].
![]() |
Wyniki wyświetlane w konsoli są następujące:
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 | |
W konsoli znajdują się wyłącznie logi Hibernate, ponieważ uruchomiony program nie wykonuje żadnych innych czynności poza instancjonowaniem warstwy JPA. Należy zwrócić uwagę na następujące kwestie:
- wiersz 43: Hibernate próbuje usunąć klucz obcy z tabeli [EMPLOYES],
- wiersze 51–55: usunięcie trzech tabel,
- wiersz 57: utworzenie tabeli [COTISATIONS],
- wiersz 67: utworzenie tabeli [EMPLOYES],
- wiersz 80: utworzenie tabeli [INDEMNITES],
- wiersz 91: utworzenie klucza obcego tabeli [EMPLOYES].
W programie NetBeans można wyświetlić tabele w utworzonym wcześniej połączeniu:
![]() |
Utworzone tabele zależą zarówno od implementacji warstwy JPA, jak i od używanego SGBD. W związku z tym implementacja JPA / EclipseLink z tą samą bazą danych może generować różne tabele. Teraz to właśnie sprawdzimy.
5.6.2. Warstwa JPA / EclipseLink
Stworzymy nowy projekt Maven w następującym środowisku:
![]() |
Postępujemy zgodnie z instrukcjami z poprzedniego akapitu:
- utworzymy bazę danych o nazwie MySQL [dbpam_eclipselink]. Do jej wygenerowania użyjemy skryptu [dbpam_eclipselink.sql],
- utworzymy plik projektu o nazwie [persistence.xml]. Wybierzemy implementację JPA 2.0 EclipseLink,
- dodać do wygenerowanych zależności zależność od sterownika JDBC z MySQL,
- dodać encje JPA oraz program konsolowy,
- przeprowadzić testy.
Plik [persistence.xml] będzie wyglądał następująco:
<?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="pam-jpa-eclipselinkPU" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>jpa.Cotisation</class>
<class>jpa.Employe</class>
<class>jpa.Indemnite</class>
<properties>
<property name="eclipselink.target-database" value="MySQL"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink"/>
<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="eclipselink.logging.level" value="FINE"/>
<property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>
- właściwości 9–13 zostały wygenerowane przez kreatora NetBeans,
- wiersz 14: ta właściwość pozwala nam ustawić poziom logowania dla EclipseLink. Poziom FINE pozwala nam poznać polecenia SQL, które EclipseLink wyśle do bazy danych,
- wiersz 15: podczas instancjonowania warstwy JPA / EclipseLink tabele encji JPA zostaną usunięte, a następnie utworzone.
Uzyskane wyniki w konsoli są następujące:
- wiersze 26–30: połączenie z bazą danych MySQL,
- wiersze 31–34: potwierdzenie pomyślnego nawiązania połączenia,
- wiersz 36: usunięcie klucza obcego z tabeli [EMPLOYES],
- wiersz 37: usunięcie tabeli [COTISATIONS],
- wiersz 38: utworzenie tabeli [COTISATIONS]. Warto zauważyć, że klucz podstawowy ID nie posiada atrybutu MySQL auto_increment. Oznacza to, że to nie MySQL generuje wartości klucza głównego,
- wiersz 39: usunięcie tabeli [EMPLOYES],
- wiersz 40: utworzenie tabeli [EMPLOYES]. Jej klucz podstawowy ID nie posiada atrybutu MySQL auto_increment,
- wiersz 41: usunięcie tabeli [INDEMNITES],
- wiersz 42: utworzenie tabeli [INDEMNITES]. Jej klucz główny ID nie posiada atrybutu MySQL auto_increment,
- wiersz 43: utworzenie klucza obcego z tabeli [EMPLOYES] do tabeli [INDEMNITES],
- wiersz 44: utworzenie tabeli [SEQUENCE]. Będzie ona służyć do generowania kluczy głównych dla trzech poprzednich tabel,
- wiersz 47: wystąpił wyjątek, ponieważ ta tabela już istniała,
- wiersze 51–53: inicjalizacja tabeli [SEQUENCE].
Istnienie wygenerowanych tabel można sprawdzić w NetBeans [1]:
![]() |
Zatem na podstawie tych samych encji JPA implementacje JPA, Hibernate i EclipseLink nie generują tych samych tabel. W dalszej części dokumentu, gdy stosowana jest implementacja JPA:
- Hibernate, będzie wykorzystywana baza danych [dbpam_hibernate],
- EclipseLink, będzie używana baza danych [dbpam_eclipselink].
5.6.3. Zadanie do wykonania
Postępując tak samo jak poprzednio,
- należy utworzyć i przetestować projekt [mv-pam-jpa-hibernate-oracle], wykorzystujący implementację JPA dla Hibernate oraz SGBD dla Oracle,
- utworzyć i przetestować projekt [mv-pam-jpa-hibernate-mssql] wykorzystujący implementację JPA z Hibernate oraz serwer SGBD i SQL,
- utworzenie i przetestowanie projektu [mv-pam-jpa-eclipselink-oracle] z wykorzystaniem implementacji JPA EclipseLink oraz serwera Oracle SGBD,
- utworzenie i przetestowanie projektu [mv-pam-jpa-eclipselink-mssql] z wykorzystaniem implementacji JPA, EclipseLink oraz serwera SGBD, SQL,
5.6.4. Lazy czy Eager?
Wróćmy do jednej z możliwych definicji encji [Employe]:
package jpa;
...
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="SS", nullable=false, unique=true, length=15)
private String SS;
@Column(name="NOM", nullable=false, length=30)
private String nom;
@Column(name="PRENOM", nullable=false, length=20)
private String prenom;
@Column(name="ADRESSE", nullable=false, length=50)
private String adresse;
@Column(name="VILLE", nullable=false, length=30)
private String ville;
@Column(name="CP", nullable=false, length=5)
private String codePostal;
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
...
}
Wiersze 27–29 definiują klucz obcy z tabeli [EMPLOYES] do tabeli [INDEMNITES]. Atrybut fetch w wierszu 27 określa strategię wyszukiwania pola indemnite z wiersza 29. Istnieją dwa tryby:
- FetchType.LAZY: podczas wyszukiwania pracownika nie jest zwracane odpowiadające mu świadczenie. Zostanie ono zwrócone, gdy po raz pierwszy zostanie odwołane do pola [Employe].indemnite.
- FetchType.EAGER: podczas wyszukiwania pracownika wyświetlane jest odpowiadające mu świadczenie. Jest to tryb domyślny, gdy nie określono żadnego trybu.
Aby zrozumieć zalety opcji FetchType.LAZY, można posłużyć się następującym przykładem. Lista pracowników bez informacji o dodatkach jest wyświetlana na stronie internetowej z linkiem [Details]. Kliknięcie tego linku powoduje wyświetlenie dodatków wybranego pracownika. Widać, że:
- do wyświetlenia pierwszej strony nie są potrzebne dane pracowników wraz z ich dodatkami. W tym przypadku odpowiedni jest tryb FetchType.LAZY,
- aby wyświetlić drugą stronę ze szczegółami, należy wykonać dodatkowe zapytanie do bazy danych w celu uzyskania dodatków wybranego pracownika.
Tryb FetchType.LAZY pozwala uniknąć pobierania zbyt dużej ilości danych, których aplikacja nie potrzebuje od razu. Spójrzmy na przykład.
Projekt [mv-pam-jpa-hibernate] jest duplikatem:
![]() |
- do [1], kopiuje się projekt,
- w [2] wskazujemy folder, do którego ma trafić kopia, a w [3] jej nazwę,
- w [4] nowy projekt nosi tę samą nazwę co poprzedni. Zmieniamy to:
![]() |
- w [1] zmieniamy nazwę projektu,
- na [2], zmieniamy nazwę projektu, a jego nazwę na artifactId,
- na [3] – nowy projekt.
Modyfikujemy program [Main.java] w następujący sposób:
package main;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;
public class Main {
// poniższe zapytanie JPQL zwraca pracownika
// klucz obcy [Employe].indemnite znajduje się w FetchType.LAZY
public static void main(String[] args) {
// wystarczy utworzyć Entity Manager, aby zbudować warstwę JPA
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
// pierwsza próba
EntityManager em = emf.createEntityManager();
Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
em.close();
// wyświetla się pracownik
try {
System.out.println(employe);
} catch (Exception ex) {
System.out.println(ex);
}
// druga próba
em = emf.createEntityManager();
employe = (Employe) em.createQuery("select e from Employe e left join fetch e.indemnite where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
// zwolnienie zasobów
em.close();
// wyświetlanie pracownika
try {
System.out.println(employe);
} catch (Exception ex) {
System.out.println(ex);
}
// zwolnienie zasobów
emf.close();
}
}
- wiersz 15: tworzymy plik EntityManagerFactory na podstawie warstwy JPA,
- wiersz 17: uzyskujemy program EntityManager, który umożliwia nam komunikację z warstwą JPA,
- wiersz 18: żądamy pracownika o nazwie Jouveinal,
- wiersz 19: zamykamy EntityManager. Powoduje to zamknięcie kontekstu trwałości.
- wiersz 22: wyświetlamy pobranego pracownika.
Klasa [Employe] wygląda następująco:
package jpa;
...
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="SS", nullable=false, unique=true, length=15)
private String SS;
@Column(name="NOM", nullable=false, length=30)
private String nom;
@Column(name="PRENOM", nullable=false, length=20)
private String prenom;
@Column(name="ADRESSE", nullable=false, length=50)
private String adresse;
@Column(name="VILLE", nullable=false, length=30)
private String ville;
@Column(name="CP", nullable=false, length=5)
private String codePostal;
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
/**
* Returns a string representation of the object. This implementation constructs
* that representation based on the id fields.
* @return a string representation of the object.
*/
@Override
public String toString() {
return "jpa.Employe[id=" + getId()
+ ",version="+getVersion()
+",SS="+getSS()
+ ",nom="+getNom()
+ ",prenom="+getPrenom()
+ ",adresse="+getAdresse()
+",ville="+getVille()
+",code postal="+getCodePostal()
+",indice="+getIndemnite().getIndice()
+"]";
}
...
}
- wiersz 27: pole indemnite jest przywracane do trybu LAZY,
- wiersz 47: wykorzystuje pole indemnite. Jeśli metoda toString zostanie wywołana, a pole indemnite nie zostało jeszcze przywrócone, nastąpi to w tym momencie. Chyba że kontekst trwałości został zamknięty, tak jak w przykładzie.
Wróćmy do kodu metody [Main]:
- wiersze 21–25: powinno wystąpić wyjątek. Metoda toString zostanie bowiem wywołana. Będzie ona korzystać z pola indemnite. Pole to zostanie wyszukane. Ponieważ kontekst trwałości został zamknięty, zwrócona encja [Employe] już nie istnieje, stąd wyjątek.
- wiersz 27: tworzony jest nowy obiekt EntityManager,
- wiersz 28: pobieramy pracownika Jouveinal, wyraźnie żądając w zapytaniu JPQL powiązanego z nim dodatku. To wyraźne żądanie jest konieczne, ponieważ trybem wyszukiwania tego dodatku jest LAZY,
- wiersz 30: zamyka się EntityManager,
- wiersze 32–36: ponownie wyświetla się pracownika. Nie powinno wystąpić żadnych wyjątków.
Aby uruchomić projekt, potrzebna jest wypełniona baza danych. Utworzymy ją, postępując zgodnie z instrukcjami zawartymi w paragrafie 5.5. Ponadto należy zmodyfikować plik [persistence.xml]:
<?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-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>jpa.Cotisation</class>
<class>jpa.Employe</class>
<class>jpa.Indemnite</class>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
<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>
- usunięto opcję, która tworzyła tabele. Baza danych już tutaj istnieje i jest wypełniona,
- usunięto opcje, które powodowały, że Hibernate rejestrował polecenia SQL wysyłane do bazy danych.
Po uruchomieniu projektu w konsoli pojawiają się dwa następujące komunikaty:
- wiersz 1: wyjątek, który wystąpił podczas próby wyszukania brakującej premii, gdy sesja była zamknięta. Widać, że premia nie została pobrana z powodu trybu LAZY,
- wiersz 2: pracownik wraz z dodatkiem uzyskanym w wyniku zapytania, które ominęło tryb LAZY.
5.6.5. Zadanie do wykonania
Postępując analogicznie do powyższego, utwórz projekt [mv-pam-pa-eclipselink-lazy], który pokazuje zachowanie projektu EclipseLink w porównaniu z trybem LAZY.
Otrzymujemy następujące wyniki:
W trybie LAZY oba zapytania zwróciły odszkodowanie wraz z danymi pracownika. Szukając informacji w Internecie na temat tej nieprawidłowości, odkrywamy, że adnotacja [FetchType.LAZY] (wiersz 1):
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
nie jest poleceniem, lecz zaleceniem. Implementator JPA nie ma obowiązku się do niego stosować. Widać zatem, że kod staje się czasami zależny od używanej implementacji JPA. Możliwe jest skonfigurowanie EclipseLink tak, aby zachowywał się zgodnie z oczekiwaniami dla trybu LAZY.
5.6.6. W dalszej części
Architektura tworzonej aplikacji przedstawia się następująco:
![]() |
W dalszej części dokumentu skopiujemy projekt Maven [mv-pam-jpa-hibernate] do projektu [mv-pam-spring-hibernate] [1, 2, 3]:
![]() |
- a następnie zmienimy nazwę nowego projektu na [4, 5, 6].
Zmienimy zależności nowego projektu. Plik [pom.xml] przyjmuje następującą postać:
<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-pam-spring-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-pam-spring-hibernate</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>swing-layout</id>
<layout>default</layout>
<name>Repository for library Library[swing-layout]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swing-layout</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
</project>
- wiersze 25–31: zależność od testów JUnit,
- wiersze 32–41: zależności dotyczące puli połączeń Apache DBCP,
- wiersze 42–65: zależności od frameworka Spring,
- wiersze 67–71: zależności dla implementacji JPA / Hibernate,
- wiersze 72–76: zależność od sterownika JDBC dla MySQL,
- wiersze 77–81: zależność od interfejsu Swing. Jest ona automatycznie dodawana przez NetBeans po dodaniu interfejsu Swing do projektu.
Ponadto wygenerowane zostaną dwie bazy danych MySQL:
- [dbpam_hibernate] na podstawie skryptu [dbpam_hibernate.sql],
- [dbpam_eclipselink] na podstawie skryptu [dbpam_eclipselink.sql],
5.7. 'y interfejsów warstw [metier] i [DAO]
Wróćmy do architektury aplikacji:
![]() |
W powyższej architekturze, jaki interfejs powinna udostępniać warstwa [DAO] warstwie [metier], a jaki interfejs powinna udostępniać warstwa [metier] warstwie [ui]? Pierwszym podejściem do zdefiniowania interfejsów poszczególnych warstw jest przeanalizowanie różnych przypadków użycia (use cases) aplikacji. W tym przypadku mamy dwa, w zależności od wybranego interfejsu użytkownika: konsola lub formularz graficzny.
Przyjrzyjmy się sposobowi korzystania z aplikacji konsolowej:
Aplikacja otrzymuje od użytkownika trzy informacje (patrz wiersz 1 powyżej)
- numer ubezpieczenia społecznego opiekunki
- liczba godzin przepracowanych w miesiącu
- liczba dni przepracowanych w miesiącu
Na podstawie tych danych oraz innych zapisanych w plikach konfiguracyjnych aplikacja wyświetla następujące informacje:
- wiersze 4–6: wprowadzone wartości
- wiersze 8–10: informacje dotyczące pracownika, którego numer ubezpieczenia społecznego został podany
- wiersze 12–14: stawki różnych składek na ubezpieczenie społeczne
- wiersze 16–17: różne dodatki wypłacane opiekunce dziecięcej
- wiersze 19–24: elementy listy płac opiekunki do dzieci
Warstwa [metier] musi dostarczyć warstwie [ui] pewne informacje:
- informacje dotyczące opiekunki do dzieci identyfikowanej na podstawie jej numeru ubezpieczenia społecznego. Informacje te znajdują się w tabeli [EMPLOYES]. Umożliwia to wyświetlenie wierszy 6–8.
- kwoty różnych stawek składek na ubezpieczenie społeczne, które należy potrącić z wynagrodzenia brutto. Informacje te znajdują się w tabeli [COTISATIONS]. Pozwala to na wyświetlenie wierszy 10–12.
- kwoty różnych dodatków związanych z wykonywaniem zawodu opiekunki do dzieci. Informacje te znajdują się w tabeli [INDEMNITES]. Pozwala to na wyświetlenie wierszy 14–15.
- składniki wynagrodzenia wyświetlane w wierszach 18–22.
Na tej podstawie można by zdecydować się na pierwszy zapis z interfejsu [IMetier], przedstawionego przez warstwę [metier] do warstwy [ui]:
- wiersz 1: elementy warstwy [metier] są umieszczane w pakiecie [metier]
- wiersz 5: metoda [ calculerFeuilleSalaire ] przyjmuje jako parametry trzy informacje pozyskane przez warstwę [ui] i zwraca obiekt typu [FeuilleSalaire] zawierający informacje, które warstwa [ui] wyświetli na konsoli. Klasa [FeuilleSalaire] mogłaby wyglądać następująco:
- wiersz 9: pracownik, którego dotyczy lista płac – informacja nr 1 wyświetlana przez warstwę [ui]
- wiersz 10: różne stawki składek – informacja nr 2 wyświetlana przez warstwę [ui]
- wiersz 11: różne dodatki związane z indeksem pracownika – informacja nr 3 wyświetlana przez warstwę [ui]
- wiersz 12: składniki wynagrodzenia – informacja nr 4 wyświetlana przez warstwę [ui]
Drugi przykład zastosowania warstwy [métier] pojawia się w interfejsie graficznym:
![]() |
Jak widać powyżej, lista rozwijana [1, 2] zawiera wszystkich pracowników. Lista ta musi zostać pobrana z warstwy [métier]. Wygląd interfej y tej warstwy zmienia się wówczas w następujący sposób:
- wiersz [10]: metoda, która umożliwi warstwie [ui] zażądanie listy wszystkich pracowników od warstwy [métier].
Warstwa [metier] może zainicjować pola [Employe, Cotisation, Indemnite] powyższego obiektu [FeuilleSalaire] wyłącznie poprzez wysłanie zapytania do warstwy [DAO], ponieważ informacje te znajdują się w tabelach bazy danych. To samo dotyczy uzyskania listy wszystkich pracowników. Można utworzyć jeden interfejs [DAO] zarządzający dostępem do trzech encji [Employe, Cotisation, Indemnite]. W tym przypadku decydujemy się jednak na utworzenie osobnego interfejsu [DAO] dla każdej encji.
Interfejs [DAO], umożliwiający dostęp do encji [Cotisation] z tabeli [COTISATIONS], będzie wyglądał następująco:
- W wierszu 6 interfejs [ICotisationDao] obsługuje dostęp do encji [Cotisation], a tym samym do tabeli [COTISATIONS] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [COTISATIONS]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
- wiersz 8: metoda [create] tworzy nową encję [Cotisation]
- wiersz 10: metoda [edit] modyfikuje istniejącą encję [Cotisation]
- wiersz 12: metoda [destroy] usuwa istniejącą encję [Cotisation]
- wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Cotisation] na podstawie jej identyfikatora id
- wiersz 16: metoda [findAll] zwraca listę wszystkich istniejących encji [Cotisation]
Przyjrzyjmy się sygnaturze metody [create]:
Metoda create posiada parametr cotisation typu Cotisation. Parametr cotisation musi zostać zapisany w pamięci trwałej, c.a.d. który jest umieszczany w tabeli [COTISATIONS]. Przed tym zapisaniem parametr cotisation ma identyfikator id bez wartości. Po zapisaniu w bazie pole id ma wartość, która jest kluczem głównym rekordu dodanego do tabeli [COTISATIONS]. Parametr cotisation jest zatem parametrem wejściowym/wyjściowym metody create. Nie wydaje się konieczne, aby metoda create zwracała dodatkowo parametr cotisation jako wynik. Metoda wywołująca posiada odwołanie do obiektu [Cotisation cotisation], więc jeśli obiekt ten zostanie zmodyfikowany, ma ona dostęp do zmodyfikowanego obiektu, ponieważ posiada do niego odwołanie. Może zatem poznać wartość, jaką metoda create przypisała do pola id obiektu [Cotisation cotisation]. Sygnatura metody mogłaby zatem wyglądać prościej:
Podczas pisania interfejsu warto pamiętać, że może on być używany w dwóch różnych kontekstach: local oraz distant. W kontekście local metoda wywołująca i metoda wywoływana są wykonywane w tym samym JVM:
![]() |
Jeśli warstwa [metier] wywołuje metodę create z warstwy [DAO], to rzeczywiście posiada odwołanie do parametru [Cotisation cotisation], który przekazuje do tej metody.
W kontekście distant metoda wywołująca i metoda wywoływana są wykonywane w różnych JVM:
![]() |
W powyższym przykładzie warstwa [metier] jest wykonywana w JVM 1, a warstwa [DAO] w JVM 2 na dwóch różnych maszynach. Obie warstwy nie komunikują się bezpośrednio. Pomiędzy nimi znajduje się warstwa, którą nazwiemy warstwą komunikacyjną [1]. Składa się ona z warstwy nadawczej [2] oraz warstwy odbiorczej [3]. Programista zazwyczaj nie musi samodzielnie pisać tych warstw komunikacyjnych. Są one generowane automatycznie przez narzędzia programistyczne. Warstwa [metier] jest napisana tak, jakby była wykonywana w tej samej warstwie JVM co warstwa [DAO]. Nie ma zatem żadnych zmian w kodzie.
Mechanizm komunikacji między warstwą [metier] a warstwą [DAO] wygląda następująco:
- warstwa [metier] wywołuje metodę create warstwy [DAO], przekazując jej parametr [Cotisation cotisation1]
- parametr ten jest w rzeczywistości przekazywany do warstwy wysyłającej [2]. Warstwa ta przekaże do sieci wartość parametru cotisation1, a nie jego odwołanie. Dokładna postać tej wartości zależy od zastosowanego protokołu komunikacyjnego.
- Warstwa odbiorcza [3] pobierze tę wartość i na jej podstawie odtworzy obiekt [Cotisation cotisation2], będący odzwierciedleniem pierwotnego parametru wysłanego przez warstwę [metier]. Mamy teraz dwa identyczne (pod względem treści) obiekty w dwóch różnych warstwach JVM: cotisation1 i cotisation2.
- Warstwa odbiorcza przekaże obiekt cotisation2 do metody create warstwy [DAO], która zapisze go w bazie danych. Po tej operacji pole id obiektu cotisation2 zostało zainicjowane kluczem głównym rekordu dodanego do tabeli [COTISATIONS]. Nie dotyczy to obiektu cotisation1, do którego warstwa [metier] posiada odniesienie. Jeśli chcemy, aby warstwa [metier] posiadała odwołanie do obiektu cotisation2, należy jej go przesłać. W związku z tym konieczna jest zmiana sygnatury metody create warstwy [DAO]:
- Dzięki tej nowej sygnaturze metoda create zwróci jako wynik obiekt trwały cotisation2. Wynik ten jest przekazywany do warstwy odbiorczej [3], która wywołała warstwę [DAO]. Ta z kolei zwróci wartość (a nie odwołanie) obiektu cotisation2 do warstwy wysyłającej [2].
- Warstwa wysyłająca [2] pobierze tę wartość i na jej podstawie odtworzy obiekt [Cotisation cotisation3], będący odzwierciedleniem wyniku zwróconego przez metodę create warstwy [DAO].
- Obiekt [Cotisation cotisation3] jest przekazywany do metody warstwy [metier], której wywołanie metody create warstwy [DAO] zainicjowało cały ten mechanizm. Warstwa [metier] może zatem poznać wartość klucza głównego przypisanego do obiektu [Cotisation cotisation1], o którego trwałość wnioskowała: jest to wartość pola id obiektu cotisation3.
Powyższa architektura nie jest najczęściej spotykana. Częściej spotyka się warstwy [metier] i [DAO] w tej samej warstwie JVM:
![]() |
W tej architekturze to metody warstwy [metier] powinny zwracać wyniki, a nie metody warstwy [DAO]. Niemniej jednak następująca sygnatura metody create z warstwy [DAO]:
pozwala nam nie wysuwać żadnych hipotez dotyczących faktycznie wdrożonej architektury. Stosowanie sygnatur, które będą działać niezależnie od wybranej architektury – lokalnej czy zdalnej – oznacza, że w przypadku, gdy wywoływana metoda modyfikuje niektóre ze swoich parametrów:
- muszą one również stanowić część wyniku wywołanej metody
- metoda wywołująca musi wykorzystywać wynik wywołanej metody, a nie odwołania do zmodyfikowanych parametrów, które przekazała do wywołanej metody.
W ten sposób zachowujemy możliwość przejścia z architektury locale na architekturę distante bez konieczności modyfikacji kodu. Przyjrzyjmy się zatem ponownie, w tym kontekście, interfejsowi [ICotisationDao]:
- wiersz 8: omówiono przypadek metody create
- wiersz 10: metoda edit wykorzystuje swój parametr [Cotisation cotisation1] do aktualizacji rekordu w tabeli [COTISATIONS] o tym samym kluczu głównym co obiekt cotisation. Jako wynik zwraca obiekt cotisation2, będący obrazem zmodyfikowanego rekordu. Parametr cotisation1 nie ulega natomiast zmianie. Metoda musi zwracać obiekt cotisation2 jako wynik, niezależnie od tego, czy znajduje się w kontekście architektury distante, czy locale.
- wiersz 12: metoda destroy usuwa rekord z tabeli [COTISATIONS] posiadający ten sam klucz główny co obiekt cotisation przekazany jako parametr. Obiekt ten nie ulega zmianie. Nie ma zatem potrzeby zwracania go.
- wiersz 14: parametr id metody find nie jest modyfikowany przez tę metodę. Nie musi on być częścią wyniku.
- wiersz 16: metoda findAll nie ma parametrów. Nie ma zatem potrzeby jej analizowania.
Ostatecznie jedynie sygnatura metody create musi zostać dostosowana, aby można ją było wykorzystać w ramach architektury distante. Powyższe rozważania mają zastosowanie również do pozostałych interfejsów [DAO]. Nie będziemy ich powtarzać i bezpośrednio wykorzystamy sygnatury, które mogą być stosowane zarówno w ramach architektury distante, jak i locale.
Interfejs [DAO] służący do uzyskiwania dostępu do elementów [Indemnite] z tabeli [INDEMNITES] będzie wyglądał następująco:
- W wierszu 6 interfejs [IIndemniteDao] zarządza dostępem do encji [Indemnite], a tym samym do tabeli [INDEMNITES] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [INDEMNITES]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
- wiersz 8: metoda [create] tworzy nową encję [Indemnite]
- wiersz 10: metoda [edit] modyfikuje istniejącą encję [Indemnite]
- wiersz 12: metoda [destroy] usuwa istniejącą encję [Indemnite]
- wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Indemnite] na podstawie jej identyfikatora id
- wiersz 16: metoda [findAll] zwraca listę wszystkich istniejących encji [Indemnite]
Interfejs [DAO] służący do uzyskiwania dostępu do encji [Employe] z tabeli [EMPLOYES] będzie wyglądał następująco:
- w wierszu 6 interfejs [IEmployeDao] obsługuje dostęp do encji [Employe], a tym samym do tabeli [EMPLOYES] w bazie danych. Nasza aplikacja potrzebuje jedynie metody [findAll] z wiersza 16, która pozwala pobrać całą zawartość tabeli [EMPLOYES]. Chcieliśmy tutaj uwzględnić bardziej ogólny przypadek, w którym wszystkie operacje CRUD (Create, Read, Update, Delete) są wykonywane na tej encji.
- wiersz 8: metoda [create] tworzy nową encję [Employe]
- wiersz 10: metoda [edit] modyfikuje istniejącą encję [Employe]
- wiersz 12: metoda [destroy] usuwa istniejącą encję [Employe]
- wiersz 14: metoda [find] umożliwia odnalezienie istniejącej jednostki [Employe] na podstawie jej identyfikatora id
- wiersz 16: metoda [find(String SS)] pozwala odnaleźć istniejącą encję [Employe] na podstawie jej numeru SS. Widzieliśmy już, że metoda ta była niezbędna dla aplikacji konsolowej.
- wiersz 18: metoda [findAll] zwraca listę wszystkich istniejących encji [Employe]. Widzieliśmy już, że ta metoda jest niezbędna dla aplikacji graficznej.
5.8. Klasa [PamException]
Warstwa [DAO] będzie współpracować z klasą API i JDBC w Javie. Ta klasa API generuje kontrolowane wyjątki typu [SQLException], które mają dwie wady:
- zwiększają objętość kodu, który musi obowiązkowo obsługiwać te wyjątki za pomocą bloków try/catch;
- muszą być zadeklarowane w sygnaturze metod interfejsu [IDao] za pomocą „throws SQLException”. W konsekwencji uniemożliwia to implementację tego interfejsu przez klasy, które zgłaszałyby wyjątek kontrolowany typu innego niż [SQLException].
Aby rozwiązać ten problem, warstwa [DAO] będzie „przekazywać dalej” wyłącznie niekontrolowane wyjątki typu [PamException].
![]() |
- warstwa [JDBC] generuje wyjątki typu [SQLException]
- warstwa [JPA] generuje wyjątki specyficzne dla używanej implementacji JPA
- warstwa [DAO] generuje niekontrolowane wyjątki typu [PamException]
Ma to dwie konsekwencje:
- warstwa [metier] nie będzie miała obowiązku obsługi wyjątków z warstwy [DAO] za pomocą instrukcji try / catch. Będzie mogła po prostu pozwolić, by przepłynęły one aż do warstwy [ui].
- metody interfejsu [IDao] nie muszą określać w swojej sygnaturze rodzaju wyjątku [PamException], co pozostawia możliwość implementacji tego interfejsu za pomocą klas, które zgłaszałyby inny typ niekontrolowanego wyjątku.
Klasa [PamException] zostanie umieszczona w pakiecie [exception] projektu NetBeans:
![]() |
Jej kod wygląda następująco:
- wiersz 4: [PamException] wywodzi się z [RuntimeException]. Jest to zatem typ wyjątków, których kompilator nie wymaga od nas obsługi za pomocą try/catch ani umieszczania w sygnaturze metod. Z tego powodu [PamException] nie występuje w sygnaturze metod interfejsu [IDao]. Dzięki temu interfejs ten może być zaimplementowany przez klasę rzucającą inny typ wyjątków, pod warunkiem, że klasa ta również dziedziczy po [RuntimeException].
- Aby rozróżnić błędy, które mogą wystąpić, wykorzystuje się kod błędu z linii 7. Trzy konstruktory z linii 14, 19 i 24 pochodzą z klasy nadrzędnej [RuntimeException], do których dodano jeden parametr: kod błędu, który ma zostać przypisany do wyjątku.
Działanie aplikacji z punktu widzenia wyjątków będzie wyglądało następująco:
- warstwa [DAO] zamknie każdy napotkany wyjątek w wyjątku typu [PamException] i ponownie rzuci ten wyjątek do warstwy [métier].
- Warstwa [métier] pozwoli na przekazanie w górę wyjątków wygenerowanych przez warstwę [DAO]. Będzie ona enkapsulować każdy wyjątek występujący w warstwie [métier] w wyjątku typu [PamException] i ponownie rzucać ten wyjątek do warstwy [ui].
- Warstwa [ui] przechwytuje wszystkie wyjątki, które są przekazywane z warstw [métier] i [DAO]. Ogranicza się ona jedynie do wyświetlenia wyjątku na konsoli lub w interfejsie graficznym.
Przeanalizujmy teraz kolejno implementację warstw [DAO] i [metier].
5.9. Warstwa [DAO] aplikacji [PAM]
Poruszamy się w ramach następującej architektury:
![]() |
5.9.1. Implementacja
Zalecana lektura: paragraf 3.1.3 dokumentu [ref1]
Pytanie: Korzystając z integracji Spring / JPA, należy napisać klasy [CotisationDao, IndemniteDao, EmployeDao] implementujące interfejsy [ICotisationDao, IIndemniteDao, IEmployeDao]. Każda metoda klasy będzie przechwytywać ewentualny wyjątek i enkapsulować go w wyjątku typu [PamException] z kodem błędu odpowiadającym przechwyconemu wyjątkowi.
Klasy implementacyjne będą częścią pakietu [dao]:
![]() |
5.9.2. Konfiguracja
Zalecana lektura: paragraf 3.1.5 dokumentu [ref1]
Integracja DAO / JPA jest konfigurowana za pomocą pliku Spring [spring-config-dao.xml] oraz plików JPA i [persistence.xml]:
![]() |
Pytanie: proszę zapisać zawartość tych dwóch plików. Zakładamy, że wykorzystywana baza danych to baza MySQL5 [dbpam_hibernate] wygenerowana przez skrypt SQL [dbpam_hibernate.sql]. Plik Spring zdefiniuje trzy następujące bean: employeDao typu EmployeDao, indemniteDao typu IndemniteDao, cotisationDao typu CotisationDao. Ponadto wykorzystana implementacja JPA będzie miała typ Hibernate.
5.9.3. Testy
Zalecana lektura: paragrafy 3.1.6 i 3.1.7 dokumentu [ref1]
Teraz, gdy warstwa [DAO] została napisana i skonfigurowana, możemy ją przetestować. Architektura testów będzie wyglądać następująco:
![]() |
5.9.4. InitDB
Stworzymy dwa programy testujące warstwę [DAO]. Zostaną one umieszczone w pakiecie [dao] [2] w gałęzi [Test Packages] [1] projektu NetBeans. Ta gałąź nie jest uwzględniona w projekcie wygenerowanym przez opcję [Build project], co gwarantuje, że umieszczone w niej programy testowe nie zostaną uwzględnione w ostatecznym pliku .jar projektu.
![]() |
Klasy umieszczone w gałęzi [Test Packages] mają dostęp do klas znajdujących się w gałęzi [Source Packages], a także do bibliotek klas projektu. Jeśli testy wymagają bibliotek innych niż te należące do projektu, należy je zadeklarować w gałęzi [Test Libraries] [2].
Klasy testowe korzystają z narzędzia do testów jednostkowych JUnit:
- [JUnitInitDB] nie przeprowadza żadnych testów. Wypełnia bazę danych kilkoma rekordami, a następnie wyświetla je na konsoli.
- [JUnitDao] przeprowadza serię testów i weryfikuje ich wyniki.
Szkielet klasy [JUnitInitDB] wygląda następująco:
- metoda [init] jest wykonywana przed rozpoczęciem serii testów (adnotacja @BeforeClass). Tworzy ona instancję warstwy [DAO].
- Metoda [clean] jest wykonywana przed każdym testem (adnotacja @Before). Opróżnia ona bazę danych.
- Metoda [initDB] jest testem (adnotacja @Test). Jest to jedyny test. Test musi zawierać instrukcje asercji Assert.assertCondition. W tym przypadku nie będzie żadnej. Metoda ta jest zatem fałszywym testem. Jej zadaniem jest wypełnienie bazy danych kilkoma wierszami, a następnie wyświetlenie zawartości bazy na konsoli. Wykorzystywane są tutaj metody create i findAll z warstw [DAO].
Zadanie: uzupełnij kod klasy [JUnitInitDB]. Skorzystaj z przykładu z paragrafu 3.1.6 klasy [ref1]. Kod wygeneruje treść przedstawioną w paragrafie 5.1.
5.9.5. Wdrożeni owanie testów
Jesteśmy teraz gotowi do uruchomienia [InitDB]. Opisujemy procedurę na przykładzie SGBD i MySQL5:
![]() |
- klasy [1], pliki konfiguracyjne [2] oraz klasy testowe warstwy [DAO] i [3] zostały skonfigurowane,
![]() |
- projekt zostaje skompilowany [4]
- klasa [JUnitInitDB] jest uruchamiana [5]. SGBD MySQL5 jest uruchamiany z istniejącą bazą [dbpam_hibernate],
- okno [Test Results] i [6] informuje, że testy zakończyły się powodzeniem. Komunikat ten nie ma w tym przypadku znaczenia, ponieważ program [JUnitInitDB] nie zawiera żadnych instrukcji asercji Assert.assertCondition, które mogłyby spowodować niepowodzenie testu. Niemniej jednak wskazuje to, że podczas wykonywania testu nie wystąpił żaden wyjątek.
Okno [Output] zawiera logi wykonania, logi Springa oraz logi samego testu. Wyjścia generowane przez klasę [JUnitInitDB] są następujące:
Tabele [EMPLOYES, INDEMNITES, COTISATIONS] zostały wypełnione. Można to sprawdzić, łącząc się z bazą danych [dbpam_hibernate] za pomocą NetBeans.
![]() |
- w [1], w zakładce [services] wyświetlane są dane z tabeli [employes] z połączenia [dbpam_hibernate] [2],
- a w [3] – wynik.
5.9.6. JUnitDao
Teraz zajmiemy się drugą klasą testów [JUnitDao]:
![]() |
Szkielet tej klasy będzie wyglądał następująco:
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
W poprzedniej klasie testów baza danych jest czyszczona przed każdym testem.
Zadanie: napisz następujące metody:
1 – test02: należy wzorować się na test01
2 – test03: pracownik posiada pole typu Indemnite. Należy zatem utworzyć encję Indemnite oraz encję Employe
3 – test04.
Postępując w taki sam sposób jak w przypadku klasy testów [JUnitInitDB], otrzymujemy następujące wyniki:
![]() |
- w [1] uruchamia się klasę testów
- w przypadku klasy testowej [2] wyniki testów w oknie [Test Results]
Wywołajmy błąd, aby sprawdzić, jak zostanie on zgłoszony na stronie wyników:
Wiersz 13 – asercja spowoduje błąd, ponieważ wartość Csgrds wynosi 3,49 (wiersz 8). Wykonanie klasy testów daje następujące wyniki:
![]() |
- Strona wyników [1] pokazuje teraz, że niektóre testy zakończyły się niepowodzeniem.
- W pliku [2] znajduje się podsumowanie wyjątku, który spowodował niepowodzenie testu. Zawiera ono numer linii kodu Java, w której wystąpił wyjątek.
5.10. Warstwa [metier] aplikacji [PAM]
Teraz, gdy warstwa [DAO] została już napisana, przechodzimy do analizy warstwy biznesowej [2]:
![]() |
5.10.1. Interfejs Java [IMetier]
Został on opisany w punkcie 5.7. Przytaczamy go poniżej:
Implementacja warstwy [metier] zostanie zrealizowana w pakiecie [metier]:
![]() |
Pakiet [metier] będzie zawierał, oprócz interfejsu [IMetier] i jego implementacji [Metier], dwie inne klasy: [FeuilleSalaire] i [ElementsSalaire]. Klasa [FeuilleSalaire] została pokrótce omówiona w paragrafie 5.7. Powrócimy do niej teraz.
5.10.2. Klasa [FeuilleSalaire]
Metoda [calculerFeuilleSalaire] interfejsu [IMetier] zwraca obiekt typu [FeuilleSalaire], który reprezentuje różne elementy listy płac. Jej definicja jest następująca:
- wiersz 7: klasa implementuje interfejs Serializable, ponieważ jej instancje mogą być przesyłane przez sieć.
- wiersz 9: pracownik, którego dotyczy lista płac
- wiersz 10: różne stawki składek
- wiersz 11: różne dodatki związane z indeksem pracownika
- wiersz 12: składniki jego wynagrodzenia
- wiersze 14–22: dwa konstruktory klasy
- wiersze 25–27: metoda [toString] identyfikująca konkretny obiekt [FeuilleSalaire]
- wiersze 29 i kolejne: publiczne akcesory do pól prywatnych klasy
Klasa [ElementsSalaire], do której odwołuje się wiersz 11 powyższej klasy [FeuilleSalaire], zawiera elementy składające się na kartę płacową. Jej definicja jest następująca:
- wiersz 3: klasa implementuje interfejs Serializable, ponieważ jest składnikiem klasy FeuilleSalaire, która musi być serializowalna.
- wiersz 6: wynagrodzenie podstawowe
- wiersz 7: składki na ubezpieczenie społeczne odprowadzane od tego wynagrodzenia podstawowego
- wiersz 8: dzienne zasiłki na utrzymanie dziecka
- wiersz 9: dzienne dodatki na wyżywienie dziecka
- wiersz 10: wynagrodzenie netto należne opiekunce
- wiersze 12–24: elementy składowe klasy
- wiersze 27–31: metoda [toString] identyfikująca konkretny obiekt [ElementsSalaire]
- wiersze 34 i kolejne: publiczne akcesory do pól prywatnych klasy
5.10.3. Klasa implementacyjna [Metier] warstwy [metier]
Klasa implementacyjna [Metier] warstwy [metier] mogłaby wyglądać następująco:
- wiersz 5: adnotacja Spring @Transactional sprawia, że każda metoda tej klasy będzie wykonywana w ramach transakcji.
- wiersze 9–10: odwołania do warstw [DAO] w encjach [Cotisation, Employe, Indemnite]
- wiersze 14–17: metoda [calculerFeuilleSalaire]
- wiersze 20–22: metoda [findAllEmployes]
- wiersz 24 i kolejne: publiczne akcesory do pól prywatnych klasy
Zadanie: napisz kod metody [findAllEmployes].
Zadanie: napisz kod metody [calculerFeuilleSalaire].
Należy zwrócić uwagę na następujące kwestie:
- sposób obliczania wynagrodzenia został wyjaśniony w paragrafie 5.2.
- jeśli parametr [SS] nie odpowiada żadnemu pracownikowi (warstwa [DAO] zwróciła wskaźnik null), metoda wygeneruje wyjątek typu [PamException] z odpowiednim kodem błędu.
5.10.4. Testy warstwy [metier]
Tworzymy dwa programy testowe:
![]() |
Klasy testowe [3] są tworzone w pakiecie [metier] [2] w gałęzi [Test Packages] [1] projektu.
Klasa [JUnitMetier_1] mogłaby wyglądać następująco:
W klasie nie ma asercji o nazwie Assert.assertCondition. Chodzi po prostu o obliczenie kilku wynagrodzeń, aby następnie zweryfikować je ręcznie. Wynik wyświetlany na ekranie po uruchomieniu poprzedniej klasy wygląda następująco:
- wiersz 4: lista płac Justine Laverti
- wiersz 5: zestawienie wynagrodzenia Marie Jouveinal
- wiersz 6: wyjątek spowodowany tym, że pracownik o numerze SS „xx” nie istnieje.
Pytanie: w wierszu 17 pliku [JUnitMetier_1] wykorzystywany jest bean Spring o nazwie metier. Podaj definicję tego beana w pliku [spring-config-metier-dao.xml].
Klasa [JUnitMetier_2] mogłaby wyglądać następująco:
Klasa [JUnitMetier_2] jest kopią klasy [JUnitMetier_1], z tą różnicą, że tym razem w metodzie test01 umieszczono asercje.
Zadanie: napisz metodę test01.
Podczas wykonywania klasy [JUnitMetier_2], jeśli wszystko przebiegnie pomyślnie, otrzymujemy następujące wyniki:

5.11. Warstwa [ui] aplikacji [PAM] – wersja console
Teraz, gdy warstwa [metier] została już napisana, pozostaje nam jeszcze napisać warstwę [ui] [1]:
![]() |
Stworzymy dwie różne implementacje warstwy [ui]: wersję tekstową console oraz wersję graficzną swing:
![]() |
5.11.1. Klasa [ui.console.Main]
W pierwszej kolejności zajmiemy się aplikacją konsolową zaimplementowaną przez powyższą klasę [ui.console.Main]. Jej działanie opisano w punkcie 5.3. Szkielet klasy [Main] mógłby wyglądać następująco:
Zadanie: uzupełnij powyższy kod.
5.11.2. Wykonanie
Aby uruchomić klasę [ui.console.Main], należy postępować w następujący sposób:
![]() |
- w [1], należy wybrać właściwości projektu,
- w [2] należy wybrać właściwość projektu [Run],
- użyj przycisku [3], aby wskazać klasę (tzw. klasę główną) do wykonania,
- wybrać klasę [4],
- klasa pojawi się w polu [5]. Do uruchomienia tej klasy potrzebne są trzy argumenty (nr SS, liczba przepracowanych godzin, liczba przepracowanych dni). Argumenty te należy umieścić w polu [6],
- po wykonaniu tej czynności można uruchomić projekt [7]. Poprzednia konfiguracja powoduje, że uruchomiona zostanie klasa [ui.console.Main].
Wyniki wykonania są wyświetlane w oknie [output]:
![]() | ![]() |
5.12. Warstwa [ui] aplikacji [PAM] – wersja graficzna
Teraz wdrażamy warstwę [ui] z interfejsem graficznym:
![]() |
![]() |
- w [1], klasa [PamJFrame] interfejsu graficznego
- w [2]: interfejs graficzny
5.12.1. Krótki samouczek
Aby utworzyć interfejs graficzny, można postępować w następujący sposób:
![]() |
- [1]: tworzymy nowy plik za pomocą przycisku [1] [New File...]
- [2]: wybieramy kategorię pliku [Swing GUI Forms], c.a.d. formularze graficzne
- [3]: wybieramy typ [JFrame Form], czyli pusty typ formularza
![]() |
- [5]: nadajemy formularzowi nazwę, która będzie jednocześnie nazwą klasy
- [6]: umieszczamy formularz w pakiecie
- [8]: formularz zostaje dodany do drzewa projektu
- [9]: formularz jest dostępny w dwóch perspektywach: [Design] i [9], które umożliwiają projektowanie różnych komponentów formularza, oraz [Source] i [10 ci-dessous], które zapewniają dostęp do kodu Java formularza. Ostatecznie formularz jest klasą Java, tak jak każda inna. Perspektywa [Design] ułatwia projektowanie formularza. Przy każdym dodaniu komponentu w trybie [Design] do perspektywy [Source] dodawany jest kod Java, aby uwzględnić ten komponent.
![]() |
- [11]: lista komponentów Swing dostępnych dla formularza znajduje się w oknie [Palette].
- [12]: okno [Inspector] przedstawia drzewo komponentów formularza. Komponenty posiadające reprezentację wizualną znajdują się w gałęzi [JFrame], pozostałe w gałęzi [Other Components].
![]() |
- w formacie [13], wybieramy komponent [JLabel] jednym kliknięciem
- w [14], umieszczamy go w formularzu w trybie [Design]
- w [15] definiujemy właściwości JLabel (tekst, czcionka).
![]() |
- w [16] – uzyskany wynik.
- w pliku [17] wyświetlamy podgląd formularza
- w pliku [18] – wynik
- w [19] etykieta [JLabel1] została dodana do drzewa komponentów w oknie [Inspector]
![]() |
- w [20] i [21]: w perspektywie formularza [Source] dodano kod Java do obsługi dodanego elementu JLabel.
Samouczek dotyczący tworzenia formularzy w programie NetBeans jest dostępny pod adresem [http://www.netbeans.org/kb/trails/matisse.html].
5.12.2. Interfejs graficzny [PamJFrame]
Stworzymy następujący interfejs graficzny:
![]() |
- w pliku [1], interfejs graficzny
- w [2], drzewo jego komponentów: jeden JLabel i sześć kontenerów JPanel
JLabel1
![]() |
JPanel1
![]() | ![]() |
JPanel2
![]() | ![]() |
JPanel3
![]() | ![]() |
JPanel4
![]() | ![]() |
JPanel5
![]() | ![]() |
Zadanie praktyczne: zbuduj powyższy interfejs graficzny, korzystając z samouczka [http://www.netbeans.org/kb/trails/matisse.html].
5.12.3. Zdarzenia w interfejsie graficznym
Zalecana lektura: rozdział [Interfaces graphiques] z [ref2].
Zajmiemy się obsługą kliknięcia przycisku [jButtonSalaire]. Aby utworzyć metodę obsługującą to zdarzenie, można postępować w następujący sposób:
![]() |
Generowana jest procedura obsługi kliknięcia przycisku [JButtonSalaire]:
Wygenerowany zostaje również kod Java, który powiązuje powyższą metodę z kliknięciem przycisku [JButtonSalaire]:
Wiersze 2–5 wskazują, że kliknięcie (zdarzenie typu ActionPerformed) przycisku [jButtonSalaire] (wiersz 2) musi być obsługiwane przez metodę [jButtonSalaireActionPerformed] (wiersz 4).
Będziemy również obsługiwać zdarzenie [caretUpdate] (przesunięcie kursora) w polu wprowadzania danych [jTextFieldHT]. Aby utworzyć procedurę obsługi tego zdarzenia, postępujemy tak samo jak poprzednio:
![]() |
Generowany jest procedor obsługi zdarzenia [caretUpdate] dla pola wprowadzania danych [jTextFieldHT]:
Wygenerowano również kod Java, który powiązuje powyższą metodę ze zdarzeniem [caretUpdate] w polu wprowadzania danych [jTextFieldHT]:
Wiersze 1–4 wskazują, że zdarzenie [caretUpdate] (wiersz 2) dotyczące przycisku [jTextFieldHT] (wiersz 1) ma być obsługiwane przez metodę [ jTextFieldHTCaretUpdate] (wiersz 3).
5.12.4. Inicjalizacja interfejsu graficznego
Wróćmy do architektury naszej aplikacji:
![]() |
Warstwa [ui] wymaga odwołania do warstwy [metier]. Przypomnijmy, w jaki sposób uzyskano to odwołanie w aplikacji console:
W aplikacji graficznej metoda jest taka sama. Konieczne jest, aby podczas jej inicjalizacji odwołanie [IMetier metier] z wiersza 3 powyżej również zostało zainicjowane. Kod wygenerowany dla interfejsu graficznego wygląda na razie następująco:
- wiersze 29–35: statyczna metoda [main], która uruchamia aplikację
- wiersz 32: tworzona jest instancja interfejsu graficznego [PamJFrame] i jest ona wyświetlana.
- wiersze 7–9: konstruktor interfejsu graficznego.
- wiersz 8: wywołanie metody [initComponents] zdefiniowanej w wierszu 17. Metoda ta jest generowana automatycznie na podstawie pracy wykonanej w trybie [Design]. Nie należy jej modyfikować.
- wiersz 21: metoda, która będzie obsługiwać przesuwanie kursora wprowadzania danych w polu [jTextFieldHT]
- wiersz 25: metoda obsługująca kliknięcie przycisku [jButtonSalaire]
Aby dodać do powyższego kodu nasze własne inicjalizacje, możemy postępować w następujący sposób:
- wiersz 4: wywołujemy metodę własną, aby przeprowadzić nasze własne inicjalizacje. Są one zdefiniowane w kodzie w wierszach 10–42
Pytanie: Korzystając z komentarzy, uzupełnij kod procedury [doMyInit].
5.12.5. Obsługa zdarzeń
Zadanie: Napisz metodę [jTextFieldHTCaretUpdate]. Metoda ta musi sprawić, że jeśli dane w polu [jTextFieldHT] nie są liczbą rzeczywistą >=0, wówczas przycisk [jButtonSalaire] musi być nieaktywny.
Zadanie: napisz metodę [jButtonSalaireActionPerformed], która ma wyświetlać listę płac pracownika wybranego w polu [jComboBoxEmployes].
5.12.6. Uruchomienie interfejsu graficznego
Aby uruchomić interfejs graficzny, należy zmodyfikować konfigurację [Run] projektu:
![]() |
- na [1], należy ustawić klasę interfejsu graficznego
Projekt musi być kompletny wraz z plikami konfiguracyjnymi (persistence.xml, spring-config-metier-dao.xml) oraz klasą interfejsu graficznego. Przed uruchomieniem projektu należy uruchomić cel SGBD.
5.13. Implementacja warstwy JPA przy użyciu EclipseLink
Interesuje nas następująca architektura, w której warstwa JPA jest teraz implementowana przez EclipseLink:
![]() |
5.13.1. Projekt NetBeans
Nowy projekt NetBeans powstaje poprzez skopiowanie poprzedniego projektu:
![]() |
- na [1]: po kliknięciu prawym przyciskiem myszy na projekt Hibernate należy wybrać Copy
- za pomocą przycisku [2] należy wybrać folder nadrzędny nowego projektu. Nazwa folderu pojawia się w polu [3].
- w polu [4] nadać nazwę nowemu projektowi
- w polu [5] wprowadź nazwę folderu projektu
![]() |
- w [1] nowy projekt został utworzony. Ma on taką samą nazwę jak oryginał,
- w [2] i [3] zmieniamy jego nazwę na [mv-pam-spring-eclipselink].
Aby dostosować projekt do nowej warstwy JPA / EclipseLink, należy wprowadzić dwie zmiany:
- w warstwie [4] należy zmodyfikować pliki konfiguracyjne Springa. Znajduje się w nich bowiem konfiguracja warstwy JPA.
- w warstwie [5] należy zmodyfikować biblioteki projektu: biblioteki Hibernate należy zastąpić bibliotekami z warstwy EclipseLink.
Zacznijmy od tej ostatniej kwestii. Plik [pom.xml] dla nowego projektu będzie wyglądał następująco:
<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-pam-spring-eclipselink</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-pam-spring-eclipselink</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>swing-layout</id>
<layout>default</layout>
<name>Repository for library Library[swing-layout]</name>
</repository>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.1.1.RELEASE</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swing-layout</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
</project>
- wiersze 73–82: zależności dla implementacji JPA EclipseLink,
- wiersze 19–24: repozytorium Maven dla EclipseLink.
Pliki konfiguracyjne Springa należy zmodyfikować, aby uwzględnić zmianę implementacji JPA. W obu plikach zmienia się jedynie sekcja konfigurująca warstwę JPA. Na przykład w pliku [spring-config-metier-dao.xml] mamy:
Wiersze 19–36 konfigurują warstwę JPA. Wykorzystywana implementacja JPA to Hibernate (wiersz 22). Ponadto docelową bazą danych jest [dbpam_hibernate] (wiersz 41).
Aby przejść na implementację JPA / EclipseLink, powyższe wiersze 19–35 należy zastąpić poniższymi wierszami:
- wiersz 5: stosowana implementacja JPA to EclipseLink
- wiersz 9: właściwość databasePlatform określa docelową implementację SGBD, w tym przypadku MySQL
- wiersz 11: służy do generowania tabel bazy danych podczas instancjonowania warstwy JPA. W tym przypadku właściwość jest skomentowana.
- wiersz 7: służy do wyświetlania na konsoli poleceń SQL wysyłanych przez warstwę JPA. Tutaj właściwość jest skomentowana.
Ponadto docelową bazą danych staje się [dbpam_eclipselink] (wiersz 4 poniżej):
5.13.2. Przeprowadzenie testów
Przed przetestowaniem całej aplikacji warto sprawdzić, czy testy JUnit przebiegają pomyślnie z nową implementacją JPA. Przed ich uruchomieniem należy najpierw usunąć tabele z bazy danych. W tym celu w zakładce [Runtime] programu NetBeans, w razie potrzeby, należy utworzyć połączenie z bazą dbpam_eclipselink / MySQL5. Po nawiązaniu połączenia z bazą danych dbpam_eclipselink / MySQL5 można przystąpić do usuwania tabel, jak pokazano poniżej:
- [1]: przed usunięciem
- [2]: po usunięciu
![]() |
Po wykonaniu tej czynności można przeprowadzić pierwszy test na warstwie [DAO]: InitDB, która wypełnia bazę danych. Aby aplikacja odtworzyła wcześniej usunięte tabele, należy upewnić się, że w konfiguracji JPA / EclipseLink w Springu znajduje się wiersz:
istnieje i nie jest skomentowana.
Kompilujemy projekt (Build), a następnie uruchamiamy test [JUnitInitDB] :
![]() |
- w [1] uruchamiany jest test InitDB.
- W przypadku testu [2] test kończy się niepowodzeniem. Wyjątek jest generowany przez Spring, a nie przez test, który zakończył się niepowodzeniem.
Przyczyna: org.springframework.beans.factory.BeanCreationException: Błąd podczas tworzenia obiektu typu bean o nazwie „entityManagerFactory” zdefiniowanego w zasobie ścieżki klas [spring-config-DAO.xml]: Nie udało się wywołać metody init; wyjątkiem zagnieżdżonym jest java.lang.IllegalStateException: Aby korzystać z InstrumentationLoadTimeWeaver, należy uruchomić agenta Java. Zobacz dokumentację Spring.
Spring wskazuje, że występuje problem z konfiguracją. Komunikat nie jest jasny. Przyczyna wyjątku została wyjaśniona w paragrafie 3.1.9 dokumentu [ref1]. Aby konfiguracja Spring / EclipseLink działała, JVM, który uruchamia aplikację, musi zostać uruchomiony z określonym parametrem – agentem Java. Parametr ten ma następującą postać:
[spring-agent.jar] to agent Java, którego potrzebuje JVM do obsługi konfiguracji Spring / EclipseLink.
Podczas uruchamiania projektu można przekazać argumenty do agenta JVM:
![]() |
- w [1] uzyskuje się dostęp do właściwości projektu
- w [2] wyświetlane są właściwości Run
- w [3] przekazujemy parametr -javaagent do JVM
5.13.3. InitDB
Teraz jesteśmy gotowi do ponownego przetestowania [InitDB]. Tym razem uzyskano następujące wyniki:
![]() |
- w [1] test zakończył się powodzeniem
- w przypadku [2], w zakładce [Services] odświeżamy połączenie NetBeans z bazą danych [dbpam_eclipselink]
- w [3] utworzono cztery tabele
![]() |
- w [5] wyświetla się zawartość tabeli [employes]
- w [6] – wynik.
5.13.4. JUnitDao
Wykonanie klasy testowej [JUnitDao] może zakończyć się niepowodzeniem, nawet jeśli w przypadku implementacji JPA / Hibernate zakończyło się sukcesem. Aby zrozumieć dlaczego, przeanalizujmy przykład.
Testowaną metodą jest następująca metoda IndemniteDao.create:
- wiersze 15–22: testowana metoda
Metoda testowa wygląda następująco:
package dao;
...
public class JUnitDao {
// warstwy DAO
static private IEmployeDao employeDao;
static private IIndemniteDao indemniteDao;
static private ICotisationDao cotisationDao;
@BeforeClass
public static void init() {
// dziennik
log("init");
// konfiguracja aplikacji
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
// warstwy DAO
employeDao = (IEmployeDao) ctx.getBean("employeDao");
indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
}
@Before()
public void clean() {
// opróżnianie bazy
for (Employe employe : employeDao.findAll()) {
employeDao.destroy(employe);
}
for (Cotisation cotisation : cotisationDao.findAll()) {
cotisationDao.destroy(cotisation);
}
for (Indemnite indemnite : indemniteDao.findAll()) {
indemniteDao.destroy(indemnite);
}
}
// logi
private static void log(String message) {
System.out.println("----------- " + message);
}
// testy
….
@Test
public void test05() {
log("test05");
// tworzymy dwa świadczenia z tym samym indeksem
// narusza ograniczenie unikalności wskaźnika
boolean erreur = true;
Indemnite indemnite1 = null;
Indemnite indemnite2 = null;
Throwable th = null;
try {
indemnite1 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
indemnite2 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
erreur = false;
} catch (PamException ex) {
th = ex;
// weryfikacje
Assert.assertEquals(31, ex.getCode());
} catch (Throwable th1) {
th = th1;
}
// weryfikacje
Assert.assertTrue(erreur);
// łańcuch wyjątków
System.out.println("Chaîne des exceptions --------------------------------------");
System.out.println(th.getClass().getName());
while (th.getCause() != null) {
th = th.getCause();
System.out.println(th.getClass().getName());
}
// pierwsze odszkodowanie musiało zostać zapisane
Indemnite indemnite = indemniteDao.find(indemnite1.getId());
// weryfikacja
Assert.assertNotNull(indemnite);
Assert.assertEquals(1, indemnite.getIndice());
Assert.assertEquals(1.93, indemnite.getBaseHeure(), 1e-6);
Assert.assertEquals(2, indemnite.getEntretienJour(), 1e-6);
Assert.assertEquals(3, indemnite.getRepasJour(), 1e-6);
Assert.assertEquals(12, indemnite.getIndemnitesCP(), 1e-6);
// druga rekompensata nie powinna była zostać zapisana
List<Indemnite> indemnites = indemniteDao.findAll();
int nbIndemnites = indemnites.size();
Assert.assertEquals(nbIndemnites, 1);
}
...
}
Pytanie: wyjaśnij, na czym polega test test05 i podaj oczekiwane wyniki.
Wyniki uzyskane przy użyciu warstwy JPA / Hibernate są następujące:
Test zakończył się powodzeniem, c.a.d. Asercje zostały zweryfikowane i nie wystąpił żaden wyjątek w metodzie testowej.
Pytanie: proszę wyjaśnić, co się stało.
Wyniki uzyskane przy użyciu warstwy JPA / EclipseLink są następujące:
Podobnie jak wcześniej w przypadku Hibernate, test zakończył się powodzeniem (c.a.d). Sprawdzono asercje i nie wystąpił żaden wyjątek w metodzie testowej.
Pytanie: wyjaśnij, co się stało.
Pytanie: jakie wnioski można wyciągnąć z tych dwóch przykładów na temat zamienności implementacji JPA? Czy jest ona w tym przypadku całkowita?
5.13.5. Pozostałe testy
Po przetestowaniu warstwy [DAO] i uznaniu jej za poprawną można przejść do testowania warstwy [metier] oraz samego projektu w wersji konsolowej lub graficznej. Zmiana implementacji JPA nie ma żadnego wpływu na warstwy [metier] i [ui], a zatem jeśli warstwy te działały z Hibernate, będą działać z EclipseLink z kilkoma wyjątkami: poprzedni przykład pokazuje bowiem, że wyjątki rzucane przez warstwy [DAO] mogą się różnić. Tak więc w przypadku wykorzystania testu, Spring / JPA / Hibernate zgłasza wyjątek typu [PamException], czyli wyjątek specyficzny dla aplikacji [pam], podczas gdy Spring / JPA / EclipseLink generuje wyjątek typu [TransactionSystemException], czyli wyjątek właściwy dla frameworka Spring. Jeśli w przypadku testowym warstwa [ui] oczekuje wyjątku typu [PamException], ponieważ została zbudowana przy użyciu Hibernate, przestanie działać po przejściu na EclipseLink.
5.13.6. Zadanie do wykonania
Zadanie praktyczne: ponowne przeprowadzenie testów aplikacji console i swing z wykorzystaniem różnych instancji SGBD: MySQL5, Oracle XE, SQL Server.





















































































