5. Версія 1: Архітектура Spring / JPA
Пропонується написати консольну програму, а також графічний додаток, що дозволяють формувати розрахункові листки для виховательок, які працюють у «Дому раннього дитинства» однієї з громад. Цей додаток матиме таку архітектуру:
![]() |
5.1. BD База даних
Статичні дані, необхідні для формування розрахункового листка, будуть розміщені в базі даних, яку надалі позначатимемо як dbpam. Ця база даних може містити такі таблиці:
Структура:
первинний ключ | |
номер версії — збільшується при кожній зміні рядка | |
номер соціального страхування працівника — унікальний | |
ім'я працівника | |
його ім'я | |
його адреса | |
його місто | |
його поштовий індекс | |
зовнішній ключ у полі [ID] таблиці [INDEMNITES] |
Його вміст може бути таким:

Структура:
первинний ключ | |
номер версії — збільшується при кожній зміні рядка | |
відсоток: загальний соціальний внесок + внесок на погашення соціального боргу | |
відсоток: загальний соціальний внесок, що підлягає вирахуванню | |
відсоток: соціальне страхування, пенсія у зв’язку зі смертю чоловіка/дружини, пенсія за віком | |
відсоток: додаткова пенсія + страхування на випадок безробіття |
Його зміст може бути таким:
![]()
Ставки соціальних внесків не залежать від працівника. Попередня таблиця містить лише один рядок.
первинний ключ | ||
номер версії — збільшується при кожній зміні рядка | ||
індекс обробки — унікальний | ||
чиста вартість однієї години чергування в євро | ||
добові в євро за день перебування | ||
добові на харчування в євро за день догляду | ||
допомога за оплачувану відпустку. Це відсоток, який застосовується до базової заробітної плати. | ||
Його зміст може бути таким:

Слід зазначити, що розмір надбавок може відрізнятися від однієї няні до іншої. Вони, а саме, прив’язані до конкретної няні через її індекс оплати праці. Так, пані Марі Жувенал, яка має індекс оплати праці 2 (таблиця EMPLOYES), отримує погодинну заробітну плату в розмірі 2,1 євро (таблиця INDEMNITES).
5.2. Спосіб розрахунку заробітної плати виховательки
Тепер ми розглянемо спосіб розрахунку місячної заробітної плати виховательки. Він не претендує на те, щоб відповідати реальному порядку розрахунку. Як приклад візьмемо заробітну плату пані Марі Жувенал, яка пропрацювала 150 годин протягом 20 днів у місяці, за який виплачується заробітна плата.
Враховуються такі елементи: | | |
Базова заробітна плата виховательки визначається за такою формулою: | ||
Певна сума соціальних внесків мають стягуватися з цієї базової заробітної плати : | | |
Загальна сума соціальних внесків: | ||
Крім того, вихователька має право на щоденну компенсацію витрат на утримання та харчування за кожен відпрацьований день. З цієї причини вона отримує такі компенсації: | | |
У підсумку чиста заробітна плата, що підлягає виплаті няні, становить: |
5.3. Принцип роботи консольного додатка
Ось приклад запуску консольного додатка у вікні DOS:
Ми напишемо програму, яка отримуватиме таку інформацію:
- номер соціального страхування няні (у прикладі — 254104940426058 — рядок 1)
- загальна кількість відпрацьованих годин (у прикладі — 150 — рядок 1)
- загальна кількість відпрацьованих днів (у прикладі — 20, рядок 1)
Бачимо, що:
- рядки 9–14: відображають інформацію про працівника, номер соціального страхування якого було вказано
- рядки 17–20: відображають ставки різних внесків
- рядки 23–26: відображають надбавки, пов’язані з індексом заробітної плати працівника (у даному випадку індекс 2)
- рядки 29–33: відображають складові заробітної плати, що підлягає виплаті
Програма повідомляє про можливі помилки:
Виклик без параметрів:
dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar
Syntaxe : pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés
Виклик із помилковими даними:
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é
Виклик із неправильним номером соціального страхування:
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. Робота графічного додатка
Графічний додаток дозволяє розраховувати заробітну плату вихователів за допомогою форми Swing:
![]() |
- інформація, що раніше передавалася у вигляді параметрів до консольної програми, тепер вводиться за допомогою полів введення [1, 2, 3].
- Кнопка [4] запускає розрахунок заробітної плати
- форма відображає різні складові заробітної плати аж до чистої суми до виплати [5]
У випадаючому списку [1, 6] відображаються не номери SS співробітників, а їхні прізвища та імена. Тут припускається, що немає двох співробітників з однаковими прізвищами та іменами.
5.5. Створення бази даних
Запускаємо WampServer і використовуємо інструмент PhpMyAdmin [1]:
![]() |
- у [2] вибираємо опцію [Bases de données],
![]() |
- в [3] створюється база даних [dbpam_hibernate],
- у [4] — база створена. Вибираємо її,
![]() |
- у [5], потрібно імпортувати скрипт SQL,
- в [6], використовуємо кнопку [Parcourir] для вибору файлу,
![]() |
- у [7,8] вибираємо скрипт SQL,
- у [9] його запускають,
![]() |
- в [10], таблиці створено. Їхній вміст такий:
таблиця EMPLOYES

таблиця INDEMNITES

таблиця COTISATIONS
![]()
5.6. Реалізація JPA
5.6.1. Шар JPA / Hibernate
Ми налаштуємо шар JPA у такому середовищі:
![]() |
Консольна програма працюватиме з базою даних. Для цього необхідно:
- мати базу даних,
- мати драйвер JDBC для SGBD, у даному випадку MySQL,
- реалізувати шар JPA за допомогою Hibernate,
- написати консольну програму.
Створюємо проект Maven [mv-pam-jpa-hibernate] [1]:
![]() |
В архітектурі нашого додатка нам потрібні такі елементи:
- база даних,
- драйвер JDBC для SGBD MySQL,
- шар JPA / Hibernate (ентітети та конфігурація),
- тестова консольна програма.
5.6.1.1. База даних
Спочатку створимо порожню базу даних. Запускаємо WampServer і використовуємо інструмент PhpMyAdmin [1]:
![]() |
- у [2] вибираємо опцію [Bases de données],
![]() |
- в [3] створюється база даних [dbpam_hibernate],
- у [4] — база створена.
5.6.1.2. Налаштування шару JPA
Зв’язок між шаром JDBC та базою даних здійснюється у файлі [persistence.xml], який налаштовує шар JPA. Цей файл можна створити за допомогою NetBeans:
![]() |
- у вкладці [services] [1] здійснюється підключення до бази даних за допомогою драйвера JDBC з MySQL [2],
- у [3] вказується назва бази даних, до якої потрібно підключитися.
- у [4] — URL JDBC бази даних,
- в [5] підключаємося як root без пароля,
- у [6] можна перевірити з’єднання,
- у [7] з’єднання встановлено успішно.
![]() |
- з'являється з'єднання в [8] та в [9],
- у [10] додається новий елемент до проєкту,
![]() |
- у [11] вибирається категорія [Persistence], а в [12] — елемент [Persistence Unit],
- у [13] присвоюємо ім’я цій одиниці збереження,
- у [14] вибираємо реалізацію Hibernate,
- у [15] вказується щойно створене з’єднання з базою даних MySQL,
- у [16] вказуємо, що під час інстанціювання шару JPA він повинен створити (create) таблиці, що відповідають сутностям JPA проекту.
По завершенні роботи майстра створюється файл [persistence.xml]:
![]() |
- файл з’являється в новій гілці проєкту, у папці [META-INF] [1],
- яка відповідає папці [src/main/resources] у проєкті [2,3].
Його вміст такий:
<?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>
- рядок 3: назва модуля збереження даних та тип транзакцій. RESOURCE_LOCAL вказує, що проект самостійно керує транзакціями. У цьому випадку це має робити консольна програма,
- рядок 4: використовується реалізація JPA — Hibernate,
- рядки 6–9: параметри JDBC підключення до бази даних,
- рядок 11: запит на створення таблиць, що відповідають сутностям JPA. Насправді NetBeans тут генерує неправильну конфігурацію. Конфігурація має бути такою:
<property name="hibernate.hbm2ddl.auto" value="create"/>
За допомогою опції create Hibernate під час інстанціювання шару JPA спочатку видаляє, а потім створює таблиці, що відповідають сутностям JPA. Опція create-drop діє так само, але після завершення життєвого циклу шару JPA вона видаляє всі таблиці. Існує ще одна опція:
<property name="hibernate.hbm2ddl.auto" value="update"/>
Ця опція створює таблиці, якщо їх ще немає, але не видаляє їх, якщо вони вже існують.
Ми додамо ще три властивості до конфігурації Hibernate:
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
Вони вимагають від Hibernate відображати команди SQL, які він надсилає до бази даних. Отже, повний файл виглядає так:
<?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. Залежності
Повернемося до архітектури проєкту:
![]() |
Ми налаштували рівень JPA за допомогою файлу [persistence.xml]. В якості реалізації було обрано Hibernate. Це призвело до появи залежностей у проєкті:
![]() |
Ці залежності зумовлені включенням Hibernate до проекту. Нам потрібно додати ще одну залежність — драйвер JDBC від MySQL, який реалізує рівень JDBC архітектури. Ми модифікуємо файл [pom.xml] наступним чином:
<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>
У рядках 8–12 додано залежність драйвера JDBC від MySQL.
5.6.1.4. Елементи JPA
![]() |
Завдання: Дотримуючись підходу, наведеного у прикладі в розділі 4.4, створіть об’єкти [Cotisation, Indemnite, Employe].
Примітки:
- елементи будуть частиною пакета з назвою [jpa],
- кожна суть матиме номер версії,
- якщо дві сутності пов’язані відношенням, буде створено лише головне відношення @ManyToOne. Зворотне відношення @OneToMany не створюватиметься.
5.6.1.5. Код головного класу
Ми включаємо в проєкт раніше розроблені сутності JPA та [1]:
![]() |
потім додаємо [2], наступний клас [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) {
// для побудови шару достатньо створити Entity Manager JPA
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
EntityManager em=emf.createEntityManager();
// звільнення ресурсів
em.close();
emf.close();
}
}
- рядок 10: створюємо EntityManagerFactory з одиниці збереження під назвою [mv-pam-jpa-hibernatePU]. Ця назва походить із файлу [persistence.xml]:
<persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
...
</persistence-unit>
- рядок 12: створюється EntityManager. Це створення формує шар JPA. Файл [persistence.xml] буде використано, отже, будуть створені таблиці бази даних,
- рядки 14–15: звільняються ресурси.
5.6.1.6. Tests
Повернемося до архітектури нашого проєкту:
![]() |
Усі шари реалізовано. Запускаємо проект [2].
![]() |
Результати у консолі такі:
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 | |
У консолі містяться лише журнали Hibernate, оскільки запущена програма не виконує ніяких дій, окрім створення екземпляра шару JPA. Зверніть увагу на наступні моменти:
- рядок 43: Hibernate намагається видалити зовнішній ключ із таблиці [EMPLOYES],
- рядки 51–55: видалення трьох таблиць,
- рядок 57: створення таблиці [COTISATIONS],
- рядок 67: створення таблиці [EMPLOYES],
- рядок 80: створення таблиці [INDEMNITES],
- рядок 91: створення зовнішнього ключа таблиці [EMPLOYES].
У NetBeans можна переглянути таблиці у раніше створеному з’єднанні:
![]() |
Створені таблиці залежать як від реалізації використовуваного шару JPA, так і від використовуваного SGBD. Таким чином, реалізація JPA / EclipseLink з тією самою базою даних може генерувати різні таблиці. Саме це ми й розглянемо зараз.
5.6.2. Шар JPA / EclipseLink
Ми створимо новий проект Maven у такому середовищі:
![]() |
Ми будемо дотримуватися підходу, описаного в попередньому параграфі:
- створимо базу даних MySQL [dbpam_eclipselink]. Для її генерації використаємо скрипт [dbpam_eclipselink.sql],
- створимо файл [persistence.xml] для проекту. Візьмемо реалізацію JPA 2.0 EclipseLink,
- додайте до згенерованих залежностей драйвер JDBC від MySQL,
- додати сутності JPA та консольну програму,
- провести тестування.
Файл [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="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>
- властивості 9–13 були згенеровані майстром NetBeans,
- рядок 14: ця властивість дозволяє нам встановити рівень журналу для EclipseLink. Рівень FINE дозволяє нам дізнатися, які команди SQL буде відправляти EclipseLink до бази даних,
- рядок 15: під час інстанціювання шару JPA / EclipseLink таблиці сутностей JPA будуть спочатку видалені, а потім створені.
Отримані результати консолі такі:
- рядки 26–30: підключення до бази даних MySQL,
- рядки 31–34: підтвердження успішного підключення,
- рядок 36: видалення зовнішнього ключа з таблиці [EMPLOYES],
- рядок 37: видалення таблиці [COTISATIONS],
- рядок 38: створення таблиці [COTISATIONS]. Цікаво відзначити, що первинний ключ ID не має атрибутів MySQL та auto_increment. Це означає, що значення первинного ключа генерує не MySQL,
- рядок 39: видалення таблиці [EMPLOYES],
- рядок 40: створення таблиці [EMPLOYES]. Її первинний ключ ID не містить атрибутів MySQL та auto_increment,
- рядок 41: видалення таблиці [INDEMNITES],
- рядок 42: створення таблиці [INDEMNITES]. Її первинний ключ ID не має атрибутів MySQL та auto_increment,
- рядок 43: створення зовнішнього ключа з таблиці [EMPLOYES] до таблиці [INDEMNITES],
- рядок 44: створення таблиці [SEQUENCE]. Вона буде використовуватися для генерації первинних ключів трьох попередніх таблиць,
- рядок 47: виникло виключення, оскільки ця таблиця вже існувала,
- рядки 51–53: ініціалізація таблиці [SEQUENCE].
Наявність згенерованих таблиць можна перевірити в NetBeans [1]:
![]() |
Отже, на основі одних і тих самих сутностей JPA реалізації JPA, Hibernate та EclipseLink не генерують однакові таблиці. У подальшому тексті документа, коли використовується реалізація JPA:
- Hibernate, буде використовуватися база даних [dbpam_hibernate],
- EclipseLink — буде використовуватися база даних [dbpam_eclipselink].
5.6.3. Завдання
Дотримуючись того самого підходу, що й раніше,
- створіть і протестуйте проект [mv-pam-jpa-hibernate-oracle], використовуючи реалізацію JPA для Hibernate та SGBD для Oracle,
- створіть та протестуйте проект [mv-pam-jpa-hibernate-mssql], використовуючи реалізацію JPA на базі Hibernate та сервер SGBD SQL,
- створити та протестувати проект [mv-pam-jpa-eclipselink-oracle] з використанням реалізації JPA EclipseLink та сервера Oracle SGBD,
- створити та протестувати проект [mv-pam-jpa-eclipselink-mssql] з використанням реалізації JPA, EclipseLink та сервера SGBD, SQL,
5.6.4. Lazy чи Eager?
Повернемося до можливого визначення сутності [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;
...
}
У рядках 27–29 визначено зовнішній ключ таблиці [EMPLOYES] щодо таблиці [INDEMNITES]. Атрибут fetch у рядку 27 визначає стратегію пошуку поля indemnite у рядку 29. Існує два режими:
- FetchType.LAZY: під час пошуку співробітника відповідна йому компенсація не повертається. Вона буде повернута, коли поле [Employe].indemnite буде згадано вперше.
- FetchType.EAGER: під час пошуку співробітника відповідна йому надбавка відображається. Це режим за замовчуванням, якщо інший режим не вказано.
Щоб зрозуміти користь опції FetchType.LAZY, можна розглянути такий приклад. Список співробітників без виплат відображається на веб-сторінці з посиланням [Details]. Клік на це посилання відкриває інформацію про виплати обраного співробітника. Можна побачити, що:
- для відображення першої сторінки не потрібні дані про співробітників та їхні надбавки. У цьому випадку підходить режим FetchType.LAZY,
- а для відображення другої сторінки з деталями потрібно виконати додатковий запит до бази даних, щоб отримати надбавки вибраного співробітника.
Режим FetchType.LAZY дозволяє уникнути вивантаження надмірного обсягу даних, які програмі не потрібні відразу. Розглянемо приклад.
Проект [mv-pam-jpa-hibernate] дублюється:
![]() |
- у [1] — копіюється проект,
- у [2] вказуємо папку для копії, а в [3] — її ім’я,
- у [4] новий проект має таку саму назву, як і старий. Ми змінюємо це:
![]() |
- у [1] — перейменовуємо проєкт,
- на [2], перейменовуємо проект, а його — на artifactId,
- на [3] — новий проект.
Ми змінюємо програму [Main.java] наступним чином:
package main;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;
public class Main {
// запит JPQL, наведений нижче, повертає співробітника
// чужий ключ [Employe].indemnite знаходиться в FetchType.LAZY
public static void main(String[] args) {
// достатньо створити Entity Manager, щоб побудувати шар JPA
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
// перша спроба
EntityManager em = emf.createEntityManager();
Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
em.close();
// відображається співробітник
try {
System.out.println(employe);
} catch (Exception ex) {
System.out.println(ex);
}
// друга спроба
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();
// звільнення ресурсів
em.close();
// відображення співробітника
try {
System.out.println(employe);
} catch (Exception ex) {
System.out.println(ex);
}
// звільнення ресурсів
emf.close();
}
}
- рядок 15: створюємо EntityManagerFactory на основі шару JPA,
- рядок 17: отримуємо EntityManager, що дозволяє нам взаємодіяти з шаром JPA,
- рядок 18: запитується співробітник з іменем Jouveinal,
- рядок 19: закриваємо EntityManager. Це призводить до закриття контексту персистентності.
- рядок 22: виводимо отриманого співробітника.
Клас [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;
/**
* 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()
+"]";
}
...
}
- рядок 27: поле indemnite повертається в режим LAZY,
- рядок 47: використовується поле indemnite. Якщо метод toString викликається, коли поле indemnite ще не було скинуто, воно буде скинуто в цей момент. За винятком випадків, коли контекст збереження даних було закрито, як у наведеному прикладі.
Повернемося до коду методу [Main]:
- рядки 21–25: тут має виникнути виняток. Адже буде викликано метод toString. Він використовуватиме поле indemnite. Це поле буде шукатися. Оскільки контекст збереження даних було закрито, повернута суть [Employe] більше не існує, звідси й виняток.
- рядок 27: створюється новий об’єкт EntityManager,
- рядок 28: запитується співробітник Jouveinal із явним зазначенням у запиті JPQL відповідної надбавки. Цей явний запит необхідний, оскільки режим пошуку цієї надбавки — LAZY,
- рядок 30: закриваємо EntityManager,
- рядки 32–36: знову відображається працівник. Винятків бути не повинно.
Для виконання проекту потрібна заповнена база даних. Її буде створено відповідно до процедури, описаної в розділі 5.5. Крім того, файл [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>
- було видалено опцію, яка створювала таблиці. База даних тут уже існує і заповнена,
- ми видалили параметри, через які Hibernate записував у журнал команди SQL, які він надсилав до бази даних.
Під час виконання проекту в консолі з’являються такі два повідомлення:
- рядок 1: виняток, що виник під час пошуку відсутньої компенсації, коли сесія була закрита. Бачимо, що компенсація не була повернута через режим LAZY,
- рядок 2: співробітник із його надбавкою, отриманою за допомогою запиту, який обійшов режим LAZY.
5.6.5. Завдання
Використовуючи підхід, аналогічний тому, що був щойно застосований, створіть проект [mv-pam-pa-eclipselink-lazy], який демонструє поведінку EclipseLink у порівнянні з режимом LAZY.
Отримуємо такі результати:
У режимі LAZY обидва запити повернули інформацію про компенсацію разом із даними про працівника. Якщо пошукати в Інтернеті інформацію про цю аномалію, можна дізнатися, що анотація [FetchType.LAZY] (рядок 1):
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
не є наказом, а лише рекомендацією. Реалізатор JPA не зобов’язаний її дотримуватися. Отже, бачимо, що код іноді залежить від використовуваної реалізації JPA. Можна через налаштування задати EclipseLink поведінку, очікувану для режиму LAZY.
5.6.6. Далі
Архітектура додатка, що створюється, є такою:
![]() |
У подальшій частині документа ми скопіюємо проект Maven [mv-pam-jpa-hibernate] у проект [mv-pam-spring-hibernate] [1, 2, 3]:
![]() |
- потім перейменуємо новий проєкт на [4, 5, 6].
Змінимо залежності нового проєкту. Файл [pom.xml] набуває такого вигляду:
<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>
- рядки 25–31: залежність для тестів JUnit,
- рядки 32–41: залежності для пулу з’єднань Apache DBCP,
- рядки 42–65: залежності для фреймворку Spring,
- рядки 67–71: залежності для реалізації JPA / Hibernate,
- рядки 72–76: залежність драйвера JDBC від MySQL,
- рядки 77–81: залежність від інтерфейсу Swing. Вона автоматично додається NetBeans під час додавання інтерфейсу Swing до проєкту.
Крім того, будуть згенеровані дві бази даних MySQL:
- [dbpam_hibernate] на основі скрипта [dbpam_hibernate.sql],
- [dbpam_eclipselink] — на основі скрипта [dbpam_eclipselink.sql],
5.7. Інтерфейси для шарів [metier] та [DAO]
Повернемося до архітектури додатка:
![]() |
У наведеній вище архітектурі, який інтерфейс повинен надавати рівень [DAO] рівню [metier], а який інтерфейс повинен надавати рівень [metier] рівню [ui]? Першим підходом до визначення інтерфейсів різних рівнів є аналіз різних сценаріїв використання (use cases) додатка. У даному випадку ми маємо два сценарії, залежно від обраного інтерфейсу користувача: консоль або графічний інтерфейс.
Розглянемо спосіб використання консольного додатка:
Додаток отримує від користувача три відомості (див. рядок 1 вище)
- номер соціального страхування няні
- кількість відпрацьованих годин за місяць
- кількість відпрацьованих днів у місяці
На основі цих даних та інших, записаних у файлах конфігурації, програма відображає таку інформацію:
- рядки 4–6: введені значення
- рядки 8–10: інформація про працівника, номер соціального страхування якого було вказано
- рядки 12–14: ставки різних соціальних внесків
- рядки 16–17: різні виплати, що нараховуються няні
- рядки 19–24: елементи розрахункового листка няні
Рівень [metier] повинен надавати рівень [ui] певну інформацію:
- інформація щодо виховательки, ідентифікованої за її номером соціального страхування. Ця інформація міститься в таблиці [EMPLOYES]. Це дозволяє відобразити рядки 6–8.
- суми різних ставок соціальних внесків, що підлягають утриманню з валової заробітної плати. Ця інформація міститься в таблиці [COTISATIONS]. Це дозволяє відобразити рядки 10–12.
- суми різних надбавок, пов’язаних з роботою няні. Ця інформація міститься в таблиці [INDEMNITES]. Це дозволяє відобразити рядки 14–15.
- складові заробітної плати, що відображаються у рядках 18–22.
На основі цього можна визначити першу операцію запису з інтерфейсу [IMetier], представленого шаром [metier], до шару [ui]:
- рядок 1: елементи шару [metier] поміщаються в пакет [metier]
- рядок 5: метод [ calculerFeuilleSalaire ] приймає як параметри три значення, отримані шаром [ui], і повертає об’єкт типу [FeuilleSalaire], що містить інформацію, яку шар [ui] відобразить у консолі. Клас [FeuilleSalaire] може мати такий вигляд:
- рядок 9: працівник, якого стосується платіжна відомість — інформація № 1, що відображається шаром [ui]
- рядок 10: різні ставки внесків — інформація № 2, що відображається шаром [ui]
- рядок 11: різні надбавки, пов’язані з індексом працівника — інформація № 3, що відображається шаром [ui]
- рядок 12: складові його заробітної плати — інформація № 4, що відображається шаром [ui]
Другий приклад використання шару [métier] пов’язаний із графічним інтерфейсом:
![]() |
Як бачимо вище, у випадаючому списку [1, 2] відображаються всі співробітники. Цей список потрібно отримати з шару [métier]. Інтерфейс , пов’язаний із цим шаром, змінюється таким чином:
- рядок [10]: метод, який дозволить шару [ui] запросити список усіх співробітників у шару [métier].
Рівень [metier] може ініціалізувати поля [Employe, Cotisation, Indemnite] вищезазначеного об’єкта [FeuilleSalaire] лише шляхом запиту до рівня [DAO], оскільки ця інформація міститься в таблицях бази даних. Те саме стосується отримання списку всіх співробітників. Можна створити єдиний інтерфейс [DAO], який керуватиме доступом до трьох сутностей [Employe, Cotisation, Indemnite]. Однак у даному випадку ми вирішили створити окремий інтерфейс [DAO] для кожної сутності.
Інтерфейс [DAO] для доступу до сутностей [Cotisation] з таблиці [COTISATIONS] матиме такий вигляд:
- у рядку 6 інтерфейс [ICotisationDao] керує доступом до сутності [Cotisation] і, отже, до таблиці [COTISATIONS] у базі даних. Нашому додатку потрібен лише метод [findAll] із рядка 16, який дозволяє отримати весь вміст таблиці [COTISATIONS]. Тут ми хотіли розглянути більш загальний випадок, коли всі операції CRUD (Create, Read, Update, Delete) виконуються над сутністю.
- рядок 8: метод [create] створює нову суть [Cotisation]
- рядок 10: метод [edit] змінює існуючий об’єкт [Cotisation]
- рядок 12: метод [destroy] видаляє існуючий об’єкт [Cotisation]
- рядок 14: метод [find] дозволяє знайти існуючий об’єкт [Cotisation] за його ідентифікатором id
- рядок 16: метод [findAll] повертає список усіх існуючих об’єктів [Cotisation]
Звернемо увагу на сигнатуру методу [create]:
Метод create має параметр cotisation типу Cotisation. Параметр cotisation має бути збережений, c.a.d. який тут заноситься до таблиці [COTISATIONS]. До цього збереження параметр cotisation має ідентифікатор id без значення. Після збереження поле id має значення, яке є первинним ключем запису, доданого до таблиці [COTISATIONS]. Отже, параметр cotisation є параметром входу/виходу методу create. Не здається необхідним, щоб метод create додатково повертав параметр cotisation як результат. Оскільки метод, що викликає, має посилання на об’єкт [Cotisation cotisation], то у разі його зміни він матиме доступ до зміненого об’єкта, оскільки має посилання на нього. Отже, вона може дізнатися значення, яке метод create присвоїв полю id об’єкта [Cotisation cotisation]. Тому сигнатура методу могла б бути простішою:
Під час написання інтерфейсу варто пам’ятати, що його можна використовувати у двох різних контекстах: local та distant. У контексті local викликаючий метод і викликаний метод виконуються в одному й тому ж JVM:
![]() |
Якщо рівень [metier] викликає метод create з рівня [DAO], він дійсно має посилання на параметр [Cotisation cotisation], який передає методу.
У контексті distant метод, що викликає, та метод, що викликається, виконуються в різних JVM:
![]() |
У наведеному вище прикладі шар [metier] виконується в JVM 1, а шар [DAO] — у JVM 2 на двох різних машинах. Ці два рівні не взаємодіють безпосередньо. Між ними розташований проміжний рівень, який ми назвемо рівнем зв’язку [1]. Він складається з рівня передачі [2] та рівня прийому [3]. Зазвичай розробнику не доводиться писати ці комунікаційні шари. Вони генеруються автоматично за допомогою програмних інструментів. Рівень [metier] написано так, ніби він виконується в тому самому JVM, що й рівень [DAO]. Отже, жодних змін у коді не відбувається.
Механізм взаємодії між шаром [metier] та шаром [DAO] є таким:
- шар [metier] викликає метод create шару [DAO], передаючи йому параметр [Cotisation cotisation1]
- цей параметр фактично передається шару відправлення [2]. Цей шар передасть у мережу значення параметра cotisation1, а не його посилання. Точний вигляд цього значення залежить від використовуваного протоколу зв’язку.
- Рівень прийому [3] отримає це значення і на його основі відтворить об’єкт [Cotisation cotisation2], який є відображенням початкового параметра, надісланого рівнем [metier]. Тепер ми маємо два ідентичні (за змістом) об’єкти у двох різних JVM: cotisation1 та cotisation2.
- Приймаючий рівень передасть об’єкт cotisation2 методу create рівня [DAO], який збереже його в базі даних. Після цієї операції поле id об’єкта cotisation2 було ініціалізовано первинним ключем запису, доданого до таблиці [COTISATIONS]. Це не стосується об’єкта cotisation1, на який посилається шар [metier]. Якщо потрібно, щоб шар [metier] мав посилання на об’єкт cotisation2, його потрібно надіслати йому. Тому доводиться змінити сигнатуру методу create шару [DAO]:
- З цим новим підписом метод create поверне в якості результату збережений об’єкт cotisation2. Цей результат передається до приймального шару [3], який викликав шар [DAO]. Останній поверне значення (а не посилання) на cotisation2 шару відправлення [2].
- Рівень відправлення [2] отримає це значення і на його основі відтворить об’єкт [Cotisation cotisation3] — відображення результату, повернутого методом create рівня [DAO].
- Об’єкт [Cotisation cotisation3] передається методу шару [metier], виклик якого до методу create шару [DAO] і започаткував весь цей механізм. Отже, рівень [metier] може знати значення первинного ключа, присвоєного об’єкту [Cotisation cotisation1], для якого він запитував збереження: це значення поля id об’єкта cotisation3.
Попередня архітектура не є найпоширенішою. Частіше зустрічаються шари [metier] та [DAO] у тому самому JVM:
![]() |
У цій архітектурі результати повинні повертати методи шару [metier], а не методи шару [DAO]. Проте наступний підпис методу create шару [DAO]:
дозволяє нам не робити припущень щодо фактично реалізованої архітектури. Використання сигнатур, які працюватимуть незалежно від обраної архітектури — локальної чи віддаленої, — означає, що у випадку, коли викликаний метод змінює деякі зі своїх параметрів:
- вони також повинні бути частиною результату викликаного методу
- метод, що викликає, повинен використовувати результат викликаного методу, а не посилання на змінені параметри, які він передав викликаному методу.
Таким чином ми залишаємо собі можливість перейти з архітектури locale на архітектуру distante без зміни коду. Давайте ще раз розглянемо в цьому контексті інтерфейс [ICotisationDao]:
- рядок 8: оброблено випадок методу create
- рядок 10: метод edit використовує свій параметр [Cotisation cotisation1] для оновлення запису в таблиці [COTISATIONS], що має той самий первинний ключ, що й об’єкт cotisation. Вона повертає як результат об’єкт cotisation2 — образ зміненого запису. Параметр cotisation1 при цьому не змінюється. Метод повинен повертати cotisation2 у якості результату незалежно від того, чи знаходиться він у контексті архітектури distante чи locale.
- рядок 12: метод destroy видаляє запис із таблиці [COTISATIONS], який має той самий первинний ключ, що й об’єкт cotisation, переданий як параметр. Цей об’єкт не змінюється. Отже, його не потрібно повертати.
- рядок 14: параметр id методу find не змінюється цим методом. Він не повинен входити до складу результату.
- рядок 16: метод findAll не має параметрів. Тому його не потрібно розглядати.
У підсумку, лише сигнатура методу create повинна бути адаптована для використання в рамках архітектури distante. Викладені вище міркування будуть дійсними й для інших інтерфейсів [DAO]. Ми не будемо їх повторювати, а безпосередньо використовуватимемо сигнатури, придатні як для архітектури distante, так і для locale.
Інтерфейс [DAO] для доступу до об’єктів [Indemnite] таблиці [INDEMNITES] матиме такий вигляд:
- у рядку 6 інтерфейс [IIndemniteDao] керує доступом до об’єкта [Indemnite], а отже, і до таблиці [INDEMNITES] у базі даних. Нашому додатку потрібен лише метод [findAll] із рядка 16, який дозволяє отримати весь вміст таблиці [INDEMNITES]. Тут ми хотіли розглянути більш загальний випадок, коли всі операції CRUD (Create, Read, Update, Delete) виконуються над сутністю.
- рядок 8: метод [create] створює нову суть [Indemnite]
- рядок 10: метод [edit] змінює існуючий об’єкт [Indemnite]
- рядок 12: метод [destroy] видаляє існуючий об’єкт [Indemnite]
- рядок 14: метод [find] дозволяє знайти існуючий об’єкт [Indemnite] за його ідентифікатором id
- рядок 16: метод [findAll] повертає список усіх існуючих об’єктів [Indemnite]
Інтерфейс [DAO] для доступу до об’єктів [Employe] з таблиці [EMPLOYES] матиме такий вигляд:
- у рядку 6 інтерфейс [IEmployeDao] керує доступом до сутності [Employe], а отже, і до таблиці [EMPLOYES] у базі даних. Нашому додатку потрібен лише метод [findAll] із рядка 16, який дозволяє отримати весь вміст таблиці [EMPLOYES]. Тут ми хотіли розглянути більш загальний випадок, коли всі операції CRUD (Create, Read, Update, Delete) виконуються над сутністю.
- рядок 8: метод [create] створює нову суть [Employe]
- рядок 10: метод [edit] змінює існуючий об’єкт [Employe]
- рядок 12: метод [destroy] видаляє існуючий об’єкт [Employe]
- рядок 14: метод [find] дозволяє знайти існуючий об’єкт [Employe] за його ідентифікатором id
- рядок 16: метод [find(String SS)] дозволяє знайти існуючий об’єкт [Employe] за його номером SS. Ми бачили, що цей метод був необхідний для консольного додатка.
- рядок 18: метод [findAll] повертає список усіх існуючих об’єктів [Employe]. Ми бачили, що цей метод був необхідний для графічного додатка.
5.8. Клас [PamException]
Шар [DAO] працюватиме з API та JDBC на Java. Цей API генерує контрольовані винятки типу [SQLException], які мають два недоліки:
- вони ускладнюють код, який обов’язково має обробляти ці винятки за допомогою блоків try/catch;
- їх необхідно оголошувати у сигнатурі методів інтерфейсу [IDao] за допомогою «throws SQLException». Це унеможливлює реалізацію цього інтерфейсу класами, які б генерували контрольоване виключення типу, відмінного від [SQLException].
Щоб вирішити цю проблему, рівень [DAO] «передаватиме» лише неконтрольовані винятки типу [PamException].
![]() |
- шар [JDBC] генерує винятки типу [SQLException]
- рівень [JPA] генерує винятки, властиві використовуваній реалізації JPA
- рівень [DAO] генерує неконтрольовані винятки типу [PamException]
Це має два наслідки:
- шар [metier] не буде зобов’язаний обробляти винятки шару [DAO] за допомогою блоків try/catch. Він зможе просто дозволити їм просуватися вгору до шару [ui].
- методи інтерфейсу [IDao] не повинні вказувати у своїх сигнатурах тип винятку [PamException], що залишає можливість реалізувати цей інтерфейс за допомогою класів, які генерують інший тип неконтрольованого винятку.
Клас [PamException] буде розміщено в пакеті [exception] проєкту NetBeans:
![]() |
Його код такий:
- рядок 4: [PamException] походить від [RuntimeException]. Отже, це тип винятків, які компілятор не вимагає обробляти за допомогою блоку try/catch або вказувати в сигнатурі методів. Саме з цієї причини [PamException] відсутній у сигнатурі методів інтерфейсу [IDao]. Це дозволяє реалізувати цей інтерфейс класом, який генерує винятки іншого типу, за умови, що цей клас також походить від [RuntimeException].
- Щоб розрізнити помилки, які можуть виникнути, використовується код помилки з рядка 7. Три конструктори в рядках 14, 19 та 24 — це конструктори батьківського класу [RuntimeException], до яких додано один параметр: код помилки, який потрібно присвоїти винятковій ситуації.
Функціонування додатка з точки зору винятків буде таким:
- рівень [DAO] інкапсулює будь-яке виявлене виключення у виключення типу [PamException] і перекине його на рівень [métier].
- Рівень [métier] пропускатиме винятки, запущені рівнем [DAO]. Вона інкапсулює будь-яке виключення, що виникає на рівні [métier], у виключення типу [PamException] і перенаправить це виключення на рівень [ui].
- Рівень [ui] перехоплює всі винятки, що надходять з рівнів [métier] та [DAO]. Він обмежиться відображенням винятку на консолі або в графічному інтерфейсі.
Тепер по черзі розглянемо реалізацію шарів [DAO] та [metier].
5.9. Рівень [DAO] додатка [PAM]
Ми розглядаємо наступну архітектуру:
![]() |
5.9.1. Реалізація
Рекомендована література: параграф 3.1.3 з [ref1]
Завдання: Використовуючи інтеграцію Spring / JPA, напишіть класи [CotisationDao, IndemniteDao, EmployeDao] для реалізації інтерфейсів [ICotisationDao, IIndemniteDao, IEmployeDao]. Кожен метод класу повинен перехоплювати можливе виключення та інкапсулювати його у виключення типу [PamException] із кодом помилки, що відповідає перехопленому виключенню.
Класи реалізації увійдуть до пакету [dao]:
![]() |
5.9.2. Конфігурація
Рекомендована література: розділ 3.1.5 документа [ref1]
Інтеграція DAO / JPA налаштовується за допомогою файлу Spring [spring-config-dao.xml] та файлів JPA і [persistence.xml]:
![]() |
Завдання: записати вміст цих двох файлів. Припустимо, що використовується база даних MySQL5 [dbpam_hibernate], створена за допомогою скрипта SQL [dbpam_hibernate.sql]. Файл Spring визначатиме такі три біни: employeDao типу EmployeDao, indemniteDao типу IndemniteDao, cotisationDao типу CotisationDao. Крім того, реалізацією JPA, що використовується, буде Hibernate.
5.9.3. Тестування
Рекомендовані матеріали: параграфи 3.1.6 та 3.1.7 з [ref1]
Тепер, коли рівень [DAO] написано та налаштовано, ми можемо його протестувати. Архітектура тестів буде такою:
![]() |
5.9.4. InitDB
Ми створимо дві програми для тестування шару [DAO]. Вони будуть розміщені в пакеті [dao] [2] гілки [Test Packages] [1] проекту NetBeans. Ця гілка не входить до складу проекту, згенерованого за допомогою опції [Build project], що гарантує, що тестові програми, які ми туди розміщуємо, не будуть включені до кінцевого .jar-файлу проекту.
![]() |
Класи, розміщені у гілці [Test Packages], мають доступ до класів, що містяться у гілці [Source Packages], а також до бібліотек класів проекту. Якщо для тестів потрібні бібліотеки, відмінні від тих, що належать до проекту, їх слід оголосити у гілці [Test Libraries] [2].
Тестові класи використовують інструмент для модульних тестів JUnit:
- [JUnitInitDB] не виконує жодних тестів. Він заповнює базу даних кількома записами, а потім виводить їх на консоль.
- [JUnitDao] виконує серію тестів і перевіряє їхні результати.
Структура класу [JUnitInitDB] така:
- Метод [init] виконується перед початком серії тестів (анотація @BeforeClass). Він створює екземпляр шару [DAO].
- Метод [clean] виконується перед кожним тестом (анотація @Before). Він очищає базу даних.
- Метод [initDB] є тестом (анотація @Test). Це єдиний тест. Тест повинен містити інструкції перевірки Assert.assertCondition. У цьому випадку їх не буде. Отже, цей метод є фіктивним тестом. Його завдання — заповнити базу даних кількома рядками, а потім вивести вміст бази на консоль. Тут використовуються методи create та findAll з рівнів [DAO].
Завдання: доповнити код класу [JUnitInitDB]. Скористайтеся прикладом із параграфа 3.1.6 класу [ref1]. Код має згенерувати вміст, наведений у параграфі 5.1.
5.9.5. Впровадженн та тестування
Тепер ми готові до виконання [InitDB]. Опишемо процедуру на прикладі SGBD та MySQL5:
![]() |
- класи [1], файли конфігурації [2] та класи тестування рівня [DAO] [3] налаштовані,
![]() |
- проект зібрано [4]
- клас [JUnitInitDB] виконується [5]. SGBD MySQL5 запускається з існуючою базою [dbpam_hibernate],
- у вікні [Test Results] та [6] вказано, що тести пройшли успішно. Це повідомлення тут не має значення, оскільки програма [JUnitInitDB] не містить жодної інструкції перевірки Assert.assertCondition, яка могла б спричинити провал тесту. Проте це свідчить про те, що під час виконання тесту не виникло жодних винятків.
Вікно [Output] містить журнали виконання: журнали Spring та самого тесту. Клас [JUnitInitDB] виводить такі дані:
Таблиці [EMPLOYES, INDEMNITES, COTISATIONS] заповнені. Це можна перевірити за допомогою підключення NetBeans до бази даних [dbpam_hibernate].
![]() |
- у [1], на вкладці [services] відображаються дані таблиці [employes] з підключення [dbpam_hibernate] [2],
- а в [3] — результат.
5.9.6. JUnitDao
Тепер розглянемо другий клас тестів [JUnitDao]:
![]() |
Структура класу буде такою:
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 | |
У попередньому класі тестів база даних очищується перед кожним тестом.
Завдання: написати такі методи:
1 — test02: слід орієнтуватися на test01
2 — test03: у співробітника є поле типу Indemnite. Тому потрібно створити сутності Indemnite та Employe
3 — test04.
Діючи так само, як і для класу тестів [JUnitInitDB], отримуємо такі результати:
![]() |
- у [1] виконується клас тестів
- у [2] — результати тестів у вікні [Test Results]
Спровокуємо помилку, щоб побачити, як вона відображається на сторінці результатів:
У рядку 13 перевірка спричинить помилку, оскільки значення Csgrds дорівнює 3,49 (рядок 8). Виконання класу тестів дає такі результати:
![]() |
- сторінка результатів [1] тепер показує, що деякі тести не пройшли успішно.
- У [2] наведено короткий опис винятку, через який тест завершився невдало. Там вказано номер рядка коду Java, у якому стався виняток.
5.10. Рівень [metier] додатка [PAM]
Тепер, коли рівень [DAO] написано, перейдемо до вивчення бізнес-рівня [2]:
![]() |
5.10.1. Інтерфейс Java [IMetier]
Він був описаний у розділі 5.7. Наведемо його ще раз нижче:
Реалізація шару [metier] буде здійснена в пакеті [metier]:
![]() |
Пакет [metier] міститиме, окрім інтерфейсу [IMetier] та його реалізації [Metier], ще два класи: [FeuilleSalaire] та [ElementsSalaire]. Клас [FeuilleSalaire] було коротко представлено в розділі 5.7. Тепер ми повернемося до нього.
5.10.2. Клас [FeuilleSalaire]
Метод [calculerFeuilleSalaire] інтерфейсу [IMetier] повертає об’єкт типу [FeuilleSalaire], який представляє різні елементи відомості про заробітну плату. Його визначення таке:
- рядок 7: клас реалізує інтерфейс Serializable, оскільки його екземпляри можуть передаватися по мережі.
- рядок 9: працівник, якого стосується платіжна відомість
- рядок 10: різні ставки внесків
- рядок 11: різні надбавки, пов’язані з індексом працівника
- рядок 12: складові його заробітної плати
- рядки 14–22: два конструктори класу
- рядки 25–27: метод [toString], що ідентифікує конкретний об’єкт [FeuilleSalaire]
- рядки 29 і далі: публічні аксесори до приватних полів класу
Клас [ElementsSalaire], на який є посилання у рядку 11 класу [FeuilleSalaire] вище, об’єднує елементи, що складають розрахунковий лист. Його визначення таке:
- рядок 3: клас реалізує інтерфейс Serializable, оскільки є компонентом класу FeuilleSalaire, який має бути серіалізованим.
- рядок 6: базова заробітна плата
- рядок 7: соціальні внески, сплачені з цієї базової заробітної плати
- рядок 8: щоденні виплати на утримання дитини
- рядок 9: щоденні виплати на харчування дитини
- рядок 10: чиста заробітна плата, що підлягає виплаті няні
- рядки 12–24: конструктори класу
- рядки 27–31: метод [toString], що ідентифікує конкретний об’єкт [ElementsSalaire]
- рядки 34 і далі: публічні методи доступу до приватних полів класу
5.10.3. Клас реалізації [Metier] шару [metier]
Клас реалізації [Metier] шару [metier] може мати такий вигляд:
- рядок 5: анотація Spring @Transactional гарантує, що кожен метод класу виконуватиметься в рамках транзакції.
- рядки 9–10: посилання на шари [DAO] з сутностей [Cotisation, Employe, Indemnite]
- рядки 14–17: метод [calculerFeuilleSalaire]
- рядки 20–22: метод [findAllEmployes]
- рядок 24 і далі: публічні методи доступу до приватних полів класу
Завдання: напишіть код методу [findAllEmployes].
Завдання: написати код методу [calculerFeuilleSalaire].
Слід звернути увагу на наступні моменти:
- спосіб розрахунку заробітної плати було пояснено в розділі 5.2.
- якщо параметр [SS] не відповідає жодному працівнику (шар [DAO] повернув покажчик null), метод викличе виняток типу [PamException] із відповідним кодом помилки.
5.10.4. Тестування шару [metier]
Ми створюємо дві тестові програми:
![]() |
Тестові класи [3] створюються в пакеті [metier] [2] гілки [Test Packages] [1] проекту.
Клас [JUnitMetier_1] може виглядати так:
У класі немає твердження Assert.assertCondition. Ми просто намагаємося обчислити кілька заробітних плат, щоб потім перевірити їх вручну. Екранне зображення, отримане в результаті виконання попереднього класу, виглядає так:
- рядок 4: розрахунковий лист Жюстіни Лаверті
- рядок 5: розрахунковий лист Марі Жувенал
- рядок 6: виняток, пов'язаний з тим, що працівник із номером SS «xx» не існує.
Запитання: у рядку 17 [JUnitMetier_1] використовується Spring-бін із назвою metier. Наведіть визначення цього біна у файлі [spring-config-metier-dao.xml].
Клас [JUnitMetier_2] може мати такий вигляд:
Клас [JUnitMetier_2] є копією класу [JUnitMetier_1], але цього разу в метод test01 додано перевірки.
Завдання: написати метод test01.
Під час виконання класу [JUnitMetier_2], якщо все пройде успішно, отримаємо такі результати:

5.11. Рівень [ui] додатка [PAM] – версія консоль
Тепер, коли шар [metier] написано, нам залишається написати шари [ui] та [1]:
![]() |
Ми створимо дві різні реалізації шару [ui]: версію console та графічну версію swing:
![]() |
5.11.1. Клас [ui.console.Main]
Спочатку розглянемо консольну програму, реалізовану вищезазначеним класом [ui.console.Main]. Її функціонування було описано в розділі 5.3. Скелет класу [Main] може виглядати наступним чином:
Завдання: доповніть наведений вище код.
5.11.2. Виконання
Щоб виконати клас [ui.console.Main], слід виконати такі дії:
![]() |
- у [1], вибрати властивості проекту,
- у [2] вибрати властивість [Run] проекту,
- за допомогою кнопки [3] вкажіть клас (так званий головний клас), який потрібно виконати,
- виберіть клас [4],
- клас з’явиться у полі [5]. Для його виконання потрібні три аргументи (№ SS, кількість відпрацьованих годин, кількість відпрацьованих днів). Ці аргументи вводяться у поле [6],
- після чого можна запустити проект [7]. За попередньою конфігурацією буде виконано саме клас [ui.console.Main].
Результати виконання відображаються у вікні [output]:
![]() | ![]() |
5.12. Шар [ui] додатка [PAM] — графічна версія
Тепер реалізуємо шар [ui] із графічним інтерфейсом:
![]() |
![]() |
- у [1], клас [PamJFrame] графічного інтерфейсу
- в [2]: графічний інтерфейс
5.12.1. Короткий посібник
Щоб створити графічний інтерфейс, можна вчинити наступним чином:
![]() |
- [1]: створюємо новий файл за допомогою кнопки [1] [New File...]
- [2]: вибираємо категорію файлу [Swing GUI Forms], c.a.d. графічні форми
- [3]: вибираємо тип [JFrame Form] — тип порожньої форми
![]() |
- [5]: присвоюємо формі ім’я, яке також буде класом
- [6]: розміщуємо форму в пакеті
- [8]: форма додається до дерева структури проекту
- [9]: до форми можна отримати доступ з двох перспектив: [Design] та [9], які дозволяють малювати різні компоненти форми, а також [Source] та [10 ci-dessous], які надають доступ до Java-коду форми. Зрештою, форма — це звичайний клас Java, як і будь-який інший. Перспектива [Design] спрощує процес малювання форми. При кожному додаванні компонента в режимі [Design] у перспективі [Source] додається код Java, щоб врахувати цей компонент.
![]() |
- [11]: список компонентів Swing, доступних для форми, міститься у вікні [Palette].
- [12]: у вікні [Inspector] відображається дерево компонентів форми. Компоненти, що мають візуальне представлення, знаходяться у гілці [JFrame], інші — у гілці [Other Components].
![]() |
- у [13] ми вибираємо компонент [JLabel] простим клацанням
- у [14] ми розміщуємо його у формі у режимі [Design]
- у [15] ми визначаємо властивості JLabel (текст, шрифт).
![]() |
- у [16] — отриманий результат.
- у [17] запитуємо попередній перегляд форми
- у [18] — результат
- у [19] мітка [JLabel1] була додана до дерева компонентів у вікні [Inspector]
![]() |
- у [20] та [21]: у перспективі [Source] форми було додано Java-код для управління доданим JLabel.
Посібник зі створення форм за допомогою NetBeans доступний за посиланням [http://www.netbeans.org/kb/trails/matisse.html].
5.12.2. Графічний інтерфейс [PamJFrame]
Ми створимо такий графічний інтерфейс:
![]() |
- у [1], графічний інтерфейс
- у [2] — дерево його компонентів: один JLabel та шість контейнерів JPanel
JLabel1
![]() |
JPanel1
![]() | ![]() |
JPanel2
![]() | ![]() |
JPanel3
![]() | ![]() |
JPanel4
![]() | ![]() |
JPanel5
![]() | ![]() |
Практичне завдання: створити наведений вище графічний інтерфейс, скориставшись посібником [http://www.netbeans.org/kb/trails/matisse.html].
5.12.3. Події графічного інтерфейсу
Рекомендована література: розділ [Interfaces graphiques] з [ref2].
Ми будемо обробляти натискання кнопки [jButtonSalaire]. Щоб створити метод обробки цієї події, можна вчинити наступним чином:
![]() |
Створено обробник кліка на кнопку [JButtonSalaire]:
Також генерується Java-код, який пов’язує попередній метод із кліком на кнопку [JButtonSalaire]:
Саме рядки 2–5 вказують, що клік (подія типу ActionPerformed) на кнопці [jButtonSalaire] (рядок 2) має оброблятися методом [jButtonSalaireActionPerformed] (рядок 4).
Ми також оброблятимемо подію [caretUpdate] (переміщення курсору введення) у полі введення [jTextFieldHT]. Щоб створити обробник цієї події, ми діємо так само, як і раніше:
![]() |
Обробник події [caretUpdate] для поля введення [jTextFieldHT] створено:
Також згенеровано Java-код, який пов’язує попередній метод із подією [caretUpdate] у полі введення [jTextFieldHT]:
Рядки 1–4 вказують, що подія [caretUpdate] (рядок 2) для кнопки [jTextFieldHT] (рядок 1) має оброблятися методом [ jTextFieldHTCaretUpdate] (рядок 3).
5.12.4. Ініціалізація графічного інтерфейсу
Повернемося до архітектури нашого додатка:
![]() |
Рівень [ui] потребує посилання на рівень [metier]. Нагадаємо, як це посилання було отримано в додатку console:
У графічному додатку використовується той самий метод. Необхідно, щоб під час його ініціалізації також ініціалізувалося посилання [IMetier metier] із рядка 3 вище. Наразі код, згенерований для графічного інтерфейсу, має такий вигляд:
- рядки 29–35: статичний метод [main], який запускає додаток
- рядок 32: створюється та стає видимим екземпляр графічного інтерфейсу [PamJFrame].
- рядки 7–9: конструктор графічного інтерфейсу.
- рядок 8: виклик методу [initComponents], визначеного в рядку 17. Цей метод автоматично згенеровано на основі роботи, виконаної в режимі [Design]. Його не слід змінювати.
- рядок 21: метод, який керуватиме переміщенням курсора введення в полі [jTextFieldHT]
- рядок 25: метод, який керуватиме натисканням кнопки [jButtonSalaire]
Щоб додати до попереднього коду наші власні ініціалізації, можна вчинити так:
- рядок 4: викликається власний метод для виконання наших власних ініціалізацій. Вони визначені в коді рядків 10–42
Завдання: скориставшись коментарями, доповніть код процедури [doMyInit].
5.12.5. Обробники подій
Завдання: напишіть метод [jTextFieldHTCaretUpdate]. Цей метод повинен забезпечити, щоб у разі, якщо дані в полі [jTextFieldHT] не є дійсним числом >=0, кнопка [jButtonSalaire] була неактивною.
Завдання: написати метод [jButtonSalaireActionPerformed], який повинен відображати розрахунковий лист вибраного працівника з поля [jComboBoxEmployes].
5.12.6. Запуск графічного інтерфейсу
Щоб запустити графічний інтерфейс, потрібно змінити конфігурацію [Run] проекту:
![]() |
- на [1], вказати клас графічного інтерфейсу
Проєкт має бути повним, включаючи конфігураційні файли (persistence.xml, spring-config-metier-dao.xml) та клас графічного інтерфейсу. Перед запуском проєкту слід запустити цільовий файл SGBD.
5.13. Реалізація шару JPA за допомогою EclipseLink
Нас цікавить така архітектура, де шар JPA тепер реалізується за допомогою EclipseLink:
![]() |
5.13.1. Проєкт NetBeans
Новий проект NetBeans створюється шляхом копіювання попереднього проекту:
![]() |
- у [1]: після клацання правою кнопкою миші на проекті Hibernate виберіть Copy
- за допомогою кнопки [2] виберіть батьківську папку для нового проекту. Назва папки з’явиться у полі [3].
- у [4] вкажіть назву нового проєкту
- у [5] — назву папки проекту
![]() |
- у [1] новий проєкт створено. Він має таку саму назву, як і оригінальний,
- у [2] та [3] його перейменовують на [mv-pam-spring-eclipselink].
Проект потрібно змінити у двох місцях, щоб адаптувати його до нового шару JPA / EclipseLink:
- у [4] необхідно змінити конфігураційні файли Spring. У них, а саме, міститься конфігурація шару JPA.
- у [5] потрібно змінити бібліотеки проєкту: бібліотеки Hibernate слід замінити на бібліотеки з EclipseLink.
Почнемо з останнього пункту. Файл [pom.xml] для нового проєкту матиме такий вигляд:
<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>
- рядки 73–82: залежності для реалізації JPA EclipseLink,
- рядки 19–24: репозиторій Maven для EclipseLink.
Конфігураційні файли Spring потрібно змінити, щоб вказати, що реалізація JPA змінилася. В обох файлах змінюється лише розділ, що налаштовує рівень JPA. Наприклад, у [spring-config-metier-dao.xml] маємо:
Рядки 19–36 налаштовують шар JPA. Використовувана реалізація JPA — це Hibernate (рядок 22). Крім того, цільовою базою даних є [dbpam_hibernate] (рядок 41).
Щоб перейти на реалізацію JPA / EclipseLink, рядки 19–35 вище замінюються на наведені нижче:
- рядок 5: використовувана реалізація JPA — це EclipseLink
- рядок 9: властивість databasePlatform встановлює цільовий SGBD, у даному випадку MySQL
- рядок 11: для створення таблиць бази даних під час інстанціювання шару JPA. Тут властивість винесено в коментарі.
- рядок 7: для відображення на консолі команд SQL, що генеруються шаром JPA. Тут властивість винесено в коментарі.
Крім того, цільовою базою даних стає [dbpam_eclipselink] (рядок 4 нижче):
5.13.2. Виконання тестів
Перш ніж тестувати весь додаток, варто перевірити, чи проходять тести JUnit із новою реалізацією JPA. Перед цим слід спочатку видалити таблиці з бази даних. Для цього у вкладці [Runtime] у NetBeans, за необхідності, слід створити з’єднання з базою даних dbpam_eclipselink / MySQL5. Після підключення до бази даних dbpam_eclipselink / MySQL5 можна приступити до видалення таблиць, як показано нижче:
- [1]: до видалення
- [2]: після видалення
![]() |
Після цього можна виконати перший тест на шарі [DAO]: InitDB, який заповнює базу даних. Щоб раніше видалені таблиці були відновлені додатком, потрібно переконатися, що в конфігурації Spring для JPA / EclipseLink рядок:
існувала та не була закомментована.
Ми збираємо проект (Build), а потім виконуємо тест [JUnitInitDB] :
![]() |
- у [1] виконується тест InitDB.
- У тесті [2] він завершується з помилкою. Виняток генерується Spring, а не тестом, який міг би завершитися з помилкою.
Причина: org.springframework.beans.factory.BeanCreationException: Помилка під час створення об’єкта з іменем «entityManagerFactory», визначеного в ресурсі шляху до класів [spring-config-DAO.xml]: Виклик методу init завершився невдало; вкладене виключення — java.lang.IllegalStateException: Для використання InstrumentationLoadTimeWeaver необхідно запустити Java-агент. Див. документацію Spring.
Spring вказує на наявність проблеми з конфігурацією. Повідомлення є нечітким. Причина виникнення винятку пояснена в розділі 3.1.9 [ref1]. Щоб конфігурація Spring / EclipseLink працювала, JVM, який виконує додаток, має бути запущений із певним параметром — Java-агентом. Формат цього параметра такий:
[spring-agent.jar] — це Java-агент, необхідний JVM для управління конфігурацією Spring / EclipseLink.
Під час виконання проєкту можна передавати аргументи агенту JVM:
![]() |
- у [1] можна отримати доступ до властивостей проекту
- у [2] — до властивостей Run
- у [3] передається параметр -javaagent до JVM
5.13.3. InitDB
Тепер ми готові знову протестувати [InitDB]. Цього разу отримано такі результати:
![]() |
- у [1] тест пройшов успішно
- у [2], у вкладці [Services] оновлюємо з’єднання NetBeans із базою даних [dbpam_eclipselink]
- у [3] було створено чотири таблиці
![]() |
- у [5] переглядається вміст таблиці [employes]
- у [6] — результат.
5.13.4. JUnitDao
Виконання класу тестів [JUnitDao] може завершитися невдачею, навіть якщо з реалізацією JPA / Hibernate воно пройшло успішно. Щоб зрозуміти, чому, проаналізуємо приклад.
Тестованим методом є наступний метод IndemniteDao.create:
- рядки 15–22: метод, що тестується
Метод тестування такий:
package dao;
...
public class JUnitDao {
// шари DAO
static private IEmployeDao employeDao;
static private IIndemniteDao indemniteDao;
static private ICotisationDao cotisationDao;
@BeforeClass
public static void init() {
// журнал
log("init");
// конфігурація додатка
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
// шари DAO
employeDao = (IEmployeDao) ctx.getBean("employeDao");
indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
}
@Before()
public void clean() {
// очищення бази даних
for (Employe employe : employeDao.findAll()) {
employeDao.destroy(employe);
}
for (Cotisation cotisation : cotisationDao.findAll()) {
cotisationDao.destroy(cotisation);
}
for (Indemnite indemnite : indemniteDao.findAll()) {
indemniteDao.destroy(indemnite);
}
}
// журнали
private static void log(String message) {
System.out.println("----------- " + message);
}
// тестування
….
@Test
public void test05() {
log("test05");
// створено дві компенсації з однаковим індексом
// порушується обмеження унікальності індексу
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;
// перевірки
Assert.assertEquals(31, ex.getCode());
} catch (Throwable th1) {
th = th1;
}
// перевірки
Assert.assertTrue(erreur);
// ланцюжок винятків
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());
}
// першу компенсацію слід було зберегти
Indemnite indemnite = indemniteDao.find(indemnite1.getId());
// перевірка
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);
// друга компенсація не мала бути збережена
List<Indemnite> indemnites = indemniteDao.findAll();
int nbIndemnites = indemnites.size();
Assert.assertEquals(nbIndemnites, 1);
}
...
}
Питання: поясніть, що робить тест test05, та вкажіть очікувані результати.
Результати, отримані з використанням шару JPA / Hibernate, такі:
Тест пройшов успішно, c.a.d. Перевірено всі твердження, і з тестового методу не виходить жодного винятку.
Запитання: поясніть, що сталося.
Результати, отримані з використанням шару JPA / EclipseLink, такі:
Як і раніше з Hibernate, тест пройшов успішно, c.a.d. Перевірено всі твердження, і з тестового методу не виходить жодного винятку.
Питання: поясніть, що сталося.
Питання: що можна зробити висновок щодо взаємозамінності реалізацій JPA на основі цих двох прикладів? Чи є вона тут повною?
5.13.5. Інші тести
Після того як рівень [DAO] буде протестовано та визнано правильним, можна перейти до тестування рівня [metier] та самого проєкту в його консольній або графічній версії. Зміна реалізації JPA жодним чином не впливає на шари [metier] та [ui], а отже, якщо ці шари працювали з Hibernate, вони працюватимуть і з EclipseLink, за деякими винятками: попередній приклад показує, що винятки, які генерують шари [DAO], можуть відрізнятися. Так, у разі використання тесту Spring / JPA / Hibernate генерує виняток типу [PamException] — виняток, властивий самому додатку [pam], тоді як Spring / JPA / EclipseLink — виняток типу [TransactionSystemException], який є винятком фреймворку Spring. Якщо в тестовому сценарії рівень [ui] очікує виняток типу [PamException], оскільки він був створений з використанням Hibernate, він перестане працювати після переходу на EclipseLink.
5.13.6. Завдання
Практичне завдання: переробити тести для додатків console та swing з використанням різних SGBD: MySQL5, Oracle XE, SQL Server.





















































































