Skip to content

5. Версія 1: Архітектура Spring / JPA

Пропонується написати консольну програму, а також графічний додаток, що дозволяють формувати розрахункові листки для виховательок, які працюють у «Дому раннього дитинства» однієї з громад. Цей додаток матиме таку архітектуру:

5.1. BD База даних

Статичні дані, необхідні для формування розрахункового листка, будуть розміщені в базі даних, яку надалі позначатимемо як dbpam. Ця база даних може містити такі таблиці:

Таблиця EMPLOYES: містить інформацію про різних нянь

Структура:

ID
первинний ключ
VERSION
номер версії — збільшується при кожній зміні рядка
SS
номер соціального страхування працівника — унікальний
NOM
ім'я працівника
prenom
його ім'я
ADRESSE
його адреса
VILLE
його місто
CODEPOSTAL
його поштовий індекс
INDEMNITE_ID
зовнішній ключ у полі [ID] таблиці [INDEMNITES]

Його вміст може бути таким:

Image

Таблиця COTISATIONS: містить відсотки, необхідні для розрахунку соціальних внесків

Структура:

ID
первинний ключ
VERSION
номер версії — збільшується при кожній зміні рядка
CSGRDS
відсоток: загальний соціальний внесок + внесок на погашення соціального боргу
CSGD
відсоток: загальний соціальний внесок, що підлягає вирахуванню
SECU
відсоток: соціальне страхування, пенсія у зв’язку зі смертю чоловіка/дружини, пенсія за віком
RETRAITE
відсоток: додаткова пенсія + страхування на випадок безробіття

Його зміст може бути таким:

Image

Ставки соціальних внесків не залежать від працівника. Попередня таблиця містить лише один рядок.

Таблиця INDEMNITES: містить елементи, необхідні для розрахунку заробітної плати, що підлягає виплаті.
ID
первинний ключ
 
VERSION
номер версії — збільшується при кожній зміні рядка
 
INDICE
індекс обробки — унікальний
 
BASEHEURE
чиста вартість однієї години чергування в євро
 
ENTRETIENJOUR
добові в євро за день перебування
 
REPASJOUR
добові на харчування в євро за день догляду
 
INDEMNITESCP
допомога за оплачувану відпустку. Це відсоток, який застосовується до базової заробітної плати.
 
  

Його зміст може бути таким:

Image

Слід зазначити, що розмір надбавок може відрізнятися від однієї няні до іншої. Вони, а саме, прив’язані до конкретної няні через її індекс оплати праці. Так, пані Марі Жувенал, яка має індекс оплати праці 2 (таблиця EMPLOYES), отримує погодинну заробітну плату в розмірі 2,1 євро (таблиця INDEMNITES).

5.2. Спосіб розрахунку заробітної плати виховательки

Тепер ми розглянемо спосіб розрахунку місячної заробітної плати виховательки. Він не претендує на те, щоб відповідати реальному порядку розрахунку. Як приклад візьмемо заробітну плату пані Марі Жувенал, яка пропрацювала 150 годин протягом 20 днів у місяці, за який виплачується заробітна плата.

Враховуються такі елементи:

[TOTALHEURES]: total des heures
travaillées dans le mois

[TOTALJOURS]: total des jours travaillés
dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
Базова заробітна плата виховательки
визначається за такою формулою:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+
[INDEMNITESCP]/100)
[SALAIREBASE]=
(150*[2.1])*(1+0.15)= 362,25
Певна сума соціальних внесків
мають стягуватися з цієї базової заробітної плати
:

Contribution sociale généralisée et
contribution au remboursement de la
dette sociale :
 [SALAIREBASE]*[CSGRDS/100]

Contribution sociale généralisée déductible :
 [SALAIREBASE]*[CSGD/100]

Sécurité sociale, veuvage, vieillesse :
 [SALAIREBASE]*[SECU/100]

Retraite Complémentaire + AGPF +
Assurance Chômage :
 [SALAIREBASE]*[RETRAITE/100]
CSGRDS : 12,64
CSGD : 22,28
Sécurité sociale : 34,02
Retraite : 28,55
Загальна сума соціальних внесків:
[COTISATIONSSOCIALES]=
[SALAIREBASE]*(CSGRDS+CSGD
+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Крім того, вихователька має право на щоденну компенсацію витрат на утримання та харчування за кожен відпрацьований день. З цієї причини вона отримує такі компенсації:

[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
У підсумку чиста заробітна плата, що підлягає виплаті няні, становить:
[SALAIREBASE]-[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

5.3. Принцип роботи консольного додатка

Ось приклад запуску консольного додатка у вікні DOS:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des Oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations :
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités :
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

Ми напишемо програму, яка отримуватиме таку інформацію:

  1. номер соціального страхування няні (у прикладі — 254104940426058 — рядок 1)
  2. загальна кількість відпрацьованих годин (у прикладі — 150 — рядок 1)
  3. загальна кількість відпрацьованих днів (у прикладі — 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

Image

таблиця INDEMNITES

Image

таблиця COTISATIONS

Image

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].

Результати у консолі такі:

------------------------------------------------------------------------
Building mv-pam-jpa-hibernate 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
juin 21, 2012 4:22:47 PM org.hibernate.annotations.common.Version <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
juin 21, 2012 4:22:47 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.1.2}
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000402: Using Hibernate built-in connection pool (not for production use!)
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000115: Hibernate connection pool size: 20
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000006: Autocommit mode: true
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000401: using driver [com.mysql.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/dbpam_hibernate]
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000046: Connection properties: {user=root, autocommit=true, release_mode=auto}
juin 21, 2012 4:22:48 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
juin 21, 2012 4:22:48 PM org.hibernate.engine.jdbc.internal.LobCreatorBuilder useContextualLobCreation
INFO: HHH000423: Disabling contextual LOB creation as JDBC driver reported JDBC version [3] less than 4
juin 21, 2012 4:22:48 PM org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService
INFO: HHH000268: Transaction strategy: org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
juin 21, 2012 4:22:48 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000227: Running hbm2ddl schema export
Hibernate: 
    alter table EMPLOYES 
        drop 
        foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: HHH000389: Unsuccessful: alter table EMPLOYES drop foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: Table 'dbpam_hibernate.employes' doesn't exist
Hibernate: 
    drop table if exists COTISATIONS
Hibernate: 
    drop table if exists EMPLOYES
Hibernate: 
    drop table if exists INDEMNITES
Hibernate: 
    create table COTISATIONS (
        id bigint not null auto_increment,
        CSGD double precision not null,
        CSGRDS double precision not null,
        RETRAITE double precision not null,
        SECU double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    create table EMPLOYES (
        id bigint not null auto_increment,
        SS varchar(15) not null unique,
        ADRESSE varchar(50) not null,
        CP varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(20) not null,
        VERSION integer not null,
        VILLE varchar(30) not null,
        INDEMNITE_ID bigint not null,
        primary key (id)
    )
Hibernate: 
    create table INDEMNITES (
        id bigint not null auto_increment,
        BASE_HEURE double precision not null,
        ENTRETIEN_JOUR double precision not null,
        INDEMNITES_CP double precision not null,
        INDICE integer not null unique,
        REPAS_JOUR double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    alter table EMPLOYES 
        add index FK75C8D6BC73F24A67 (INDEMNITE_ID), 
        add constraint FK75C8D6BC73F24A67 
        foreign key (INDEMNITE_ID) 
        references INDEMNITES (id)
juin 21, 2012 4:22:49 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
juin 21, 2012 4:22:49 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:mysql://localhost:3306/dbpam_hibernate]
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 2.637s
Finished at: Thu Jun 21 16:22:49 CEST 2012
Final Memory: 8M/153M

У консолі містяться лише журнали Hibernate, оскільки запущена програма не виконує ніяких дій, окрім створення екземпляра шару JPA. Зверніть увагу на наступні моменти:

  • рядок 43: Hibernate намагається видалити зовнішній ключ із таблиці [EMPLOYES],
  • рядки 51–55: видалення трьох таблиць,
  • рядок 57: створення таблиці [COTISATIONS],
  • рядок 67: створення таблиці [EMPLOYES],
  • рядок 80: створення таблиці [INDEMNITES],
  • рядок 91: створення зовнішнього ключа таблиці [EMPLOYES].

У NetBeans можна переглянути таблиці у раніше створеному з’єднанні:

Створені таблиці залежать як від реалізації використовуваного шару JPA, так і від використовуваного SGBD. Таким чином, реалізація JPA / EclipseLink з тією самою базою даних може генерувати різні таблиці. Саме це ми й розглянемо зараз.

Ми створимо новий проект Maven у такому середовищі:

Ми будемо дотримуватися підходу, описаного в попередньому параграфі:

  1. створимо базу даних MySQL [dbpam_eclipselink]. Для її генерації використаємо скрипт [dbpam_eclipselink.sql],
  2. створимо файл [persistence.xml] для проекту. Візьмемо реалізацію JPA 2.0 EclipseLink,
  3. додайте до згенерованих залежностей драйвер JDBC від MySQL,
  4. додати сутності JPA та консольну програму,
  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="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 будуть спочатку видалені, а потім створені.

Отримані результати консолі такі:

------------------------------------------------------------------------
Building mv-pam-jpa-eclipselink 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
[EL Config]: 2012-06-22 14:35:01.852--ServerSession(730572764)--Потік(Thread[main,5,main])--Тип доступу для постійного класу [class jpa.Cotisation] встановлено як [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Потік(Thread[main,5,main])--Тип доступу для постійного класу [class jpa.Employe] встановлено як [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Потік(Thread[main,5,main])--Клас цільового об’єкта (посилання) для елемента зіставлення «багато до одного» [field indemnite] за замовчуванням встановлено як: клас jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Потік (Thread[main,5,main])--Тип доступу для постійного класу [class jpa.Indemnite] встановлено як [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Потік(Thread[main,5,main])--Псевдонім для класу сутності [class jpa.Cotisation] встановлюється за замовчуванням як: Cotisation.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Потік(Thread[main,5,main])--Назва стовпця для елемента [id] встановлюється за замовчуванням як: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--Псевдонім для класу сутності [class jpa.Employe] встановлюється за замовчуванням як: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--Назва стовпця для елемента [id] встановлюється за замовчуванням як: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--Псевдонім для класу сутності [class jpa.Indemnite] встановлюється за замовчуванням як: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Потік(Thread[main,5,main])--Назва стовпця для елемента [id] встановлюється за замовчуванням як: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Thread(Thread[main,5,main])--Ім'я стовпця первинного ключа для елемента зіставлення [field indemnite] встановлюється за замовчуванням як: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Потік(Thread[main,5,main])--EclipseLink, версія: Eclipse Persistence Services — 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--З'єднання(1543921451)--Потік(Thread[main,5,main])--підключення(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> "root"
    datasource URL=> "jdbc:mysql://localhost:3306/dbpam_eclipselink"
))
[EL Config]: 2012-06-22 14:35:02.738--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--Підключено: jdbc:mysql://localhost:3306/dbpam_eclipselink
    User: root@localhost
    Database: MySQL  Version: 5.5.20-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
[EL Info]: 2012-06-22 14:35:02.798--ServerSession(730572764)--Потік (Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU Увійшли успішно
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--ALTER TABLE EMPLOYES DROP FOREIGN KEY FK_EMPLOYES_INDEMNITE_ID
[EL Fine]: 2012-06-22 14:35:03.088--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--CREATE TABLE COTISATIONS (ID BIGINT NOT NULL, CSGD DOUBLE NOT NULL, CSGRDS DOUBLE NOT NULL, RETRAITE DOUBLE NOT NULL, SECU DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.198--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--CREATE TABLE EMPLOYES (ID BIGINT NOT NULL, SS VARCHAR(15) NOT NULL UNIQUE, ADRESSE VARCHAR(50) NOT NULL, CP VARCHAR(5) NOT NULL, NOM VARCHAR(30) NOT NULL, PRENOM VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(30) NOT NULL, INDEMNITE_ID BIGINT NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.318--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--CREATE TABLE INDEMNITES (ID BIGINT NOT NULL, BASE_HEURE DOUBLE NOT NULL, ENTRETIEN_JOUR DOUBLE NOT NULL, INDEMNITES_CP DOUBLE NOT NULL, INDICE INTEGER NOT NULL UNIQUE, REPAS_JOUR DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.418--ServerSession(730572764)--З’єднання(1296716340)--Потік(Thread[main,5,main])--ALTER TABLE EMPLOYES ADD CONSTRAINT FK_EMPLOYES_INDEMNITE_ID FOREIGN KEY (INDEMNITE_ID) REFERENCES INDEMNITES (ID)
[EL Fine]: 2012-06-22 14:35:03.568--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Тема(Thread[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Потік(Thread[main,5,main])--Виняток [EclipseLink-4002] (Eclipse Persistence Services — 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery(sql="CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))")
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--З'єднання(1296716340)--Потік(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) значення (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--З'єднання (1296716340)--Потік (Thread[main,5,main])--відключення
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Потік(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU Вихід із системи успішно завершено
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--З'єднання(1543921451)--Потік(Thread[main,5,main])--відключення
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • рядки 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. Завдання

Дотримуючись того самого підходу, що й раніше,

  1. створіть і протестуйте проект [mv-pam-jpa-hibernate-oracle], використовуючи реалізацію JPA для Hibernate та SGBD для Oracle,
  2. створіть та протестуйте проект [mv-pam-jpa-hibernate-mssql], використовуючи реалізацію JPA на базі Hibernate та сервер SGBD SQL,
  3. створити та протестувати проект [mv-pam-jpa-eclipselink-oracle] з використанням реалізації JPA EclipseLink та сервера Oracle SGBD,
  4. створити та протестувати проект [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, які він надсилав до бази даних.

Під час виконання проекту в консолі з’являються такі два повідомлення:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
jpa.Employe[id=31,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
  • рядок 1: виняток, що виник під час пошуку відсутньої компенсації, коли сесія була закрита. Бачимо, що компенсація не була повернута через режим LAZY,
  • рядок 2: співробітник із його надбавкою, отриманою за допомогою запиту, який обійшов режим LAZY.

5.6.5. Завдання

Використовуючи підхід, аналогічний тому, що був щойно застосований, створіть проект [mv-pam-pa-eclipselink-lazy], який демонструє поведінку EclipseLink у порівнянні з режимом LAZY.

Отримуємо такі результати:

jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]

У режимі 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) додатка. У даному випадку ми маємо два сценарії, залежно від обраного інтерфейсу користувача: консоль або графічний інтерфейс.

Розглянемо спосіб використання консольного додатка:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
...

Informations Cotisations :
CSGRDS : 3.49 %
...

Informations Indemnités :
...

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

Додаток отримує від користувача три відомості (див. рядок 1 вище)

  • номер соціального страхування няні
  • кількість відпрацьованих годин за місяць
  • кількість відпрацьованих днів у місяці

На основі цих даних та інших, записаних у файлах конфігурації, програма відображає таку інформацію:

  • рядки 4–6: введені значення
  • рядки 8–10: інформація про працівника, номер соціального страхування якого було вказано
  • рядки 12–14: ставки різних соціальних внесків
  • рядки 16–17: різні виплати, що нараховуються няні
  • рядки 19–24: елементи розрахункового листка няні

Рівень [metier] повинен надавати рівень [ui] певну інформацію:

  1. інформація щодо виховательки, ідентифікованої за її номером соціального страхування. Ця інформація міститься в таблиці [EMPLOYES]. Це дозволяє відобразити рядки 6–8.
  2. суми різних ставок соціальних внесків, що підлягають утриманню з валової заробітної плати. Ця інформація міститься в таблиці [COTISATIONS]. Це дозволяє відобразити рядки 10–12.
  3. суми різних надбавок, пов’язаних з роботою няні. Ця інформація міститься в таблиці [INDEMNITES]. Це дозволяє відобразити рядки 14–15.
  4. складові заробітної плати, що відображаються у рядках 18–22.

На основі цього можна визначити першу операцію запису з інтерфейсу [IMetier], представленого шаром [metier], до шару [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // отримати розрахунковий лист
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • рядок 1: елементи шару [metier] поміщаються в пакет [metier]
  • рядок 5: метод [ calculerFeuilleSalaire ] приймає як параметри три значення, отримані шаром [ui], і повертає об’єкт типу [FeuilleSalaire], що містить інформацію, яку шар [ui] відобразить у консолі. Клас [FeuilleSalaire] може мати такий вигляд:
package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire {
   // приватні поля
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  ...
}
  • рядок 9: працівник, якого стосується платіжна відомість — інформація № 1, що відображається шаром [ui]
  • рядок 10: різні ставки внесків — інформація № 2, що відображається шаром [ui]
  • рядок 11: різні надбавки, пов’язані з індексом працівника — інформація № 3, що відображається шаром [ui]
  • рядок 12: складові його заробітної плати — інформація № 4, що відображається шаром [ui]

Другий приклад використання шару [métier] пов’язаний із графічним інтерфейсом:

Як бачимо вище, у випадаючому списку [1, 2] відображаються всі співробітники. Цей список потрібно отримати з шару [métier]. Інтерфейс , пов’язаний із цим шаром, змінюється таким чином:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // отримати розрахунковий лист
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // список співробітників
  public List<Employe> findAllEmployes();
}
  • рядок [10]: метод, який дозволить шару [ui] запросити список усіх співробітників у шару [métier].

Рівень [metier] може ініціалізувати поля [Employe, Cotisation, Indemnite] вищезазначеного об’єкта [FeuilleSalaire] лише шляхом запиту до рівня [DAO], оскільки ця інформація міститься в таблицях бази даних. Те саме стосується отримання списку всіх співробітників. Можна створити єдиний інтерфейс [DAO], який керуватиме доступом до трьох сутностей [Employe, Cotisation, Indemnite]. Однак у даному випадку ми вирішили створити окремий інтерфейс [DAO] для кожної сутності.

Інтерфейс [DAO] для доступу до сутностей [Cotisation] з таблиці [COTISATIONS] матиме такий вигляд:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // створити новий внесок
  public Cotisation create(Cotisation cotisation);
       // змінити існуючий внесок
  public Cotisation edit(Cotisation cotisation);
       // видалити існуючий внесок
  public void destroy(Cotisation cotisation);
       // пошук конкретного внеску
  public Cotisation find(Long id);
       // отримати всі об’єкти «Внесок»
  public List<Cotisation> findAll();

}
  • у рядку 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]:

       // створити новий внесок
Cotisation create(Cotisation cotisation);

Метод create має параметр cotisation типу Cotisation. Параметр cotisation має бути збережений, c.a.d. який тут заноситься до таблиці [COTISATIONS]. До цього збереження параметр cotisation має ідентифікатор id без значення. Після збереження поле id має значення, яке є первинним ключем запису, доданого до таблиці [COTISATIONS]. Отже, параметр cotisation є параметром входу/виходу методу create. Не здається необхідним, щоб метод create додатково повертав параметр cotisation як результат. Оскільки метод, що викликає, має посилання на об’єкт [Cotisation cotisation], то у разі його зміни він матиме доступ до зміненого об’єкта, оскільки має посилання на нього. Отже, вона може дізнатися значення, яке метод create присвоїв полю id об’єкта [Cotisation cotisation]. Тому сигнатура методу могла б бути простішою:

       // створити новий внесок
void create(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]:
       // створити новий внесок
Cotisation create(Cotisation cotisation);
  • З цим новим підписом метод 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]:

       // створити новий внесок
Cotisation create(Cotisation cotisation);

дозволяє нам не робити припущень щодо фактично реалізованої архітектури. Використання сигнатур, які працюватимуть незалежно від обраної архітектури — локальної чи віддаленої, — означає, що у випадку, коли викликаний метод змінює деякі зі своїх параметрів:

  • вони також повинні бути частиною результату викликаного методу
  • метод, що викликає, повинен використовувати результат викликаного методу, а не посилання на змінені параметри, які він передав викликаному методу.

Таким чином ми залишаємо собі можливість перейти з архітектури locale на архітектуру distante без зміни коду. Давайте ще раз розглянемо в цьому контексті інтерфейс [ICotisationDao]:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // створити новий внесок
  public Cotisation create(Cotisation cotisation);
       // змінити існуючий внесок
  public Cotisation edit(Cotisation cotisation);
       // видалити існуючий внесок
  public void destroy(Cotisation cotisation);
       // пошук конкретного внеску
  public Cotisation find(Long id);
       // отримати всі об’єкти «Внесок»
  public List<Cotisation> findAll();

}
  • рядок 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] матиме такий вигляд:

package dao;

import java.util.List;
import jpa.Indemnite;

public interface IIndemniteDao {
     // створити об’єкт «Компенсація»
  public Indemnite create(Indemnite indemnite);
     // змінити об’єкт «Компенсація»
  public Indemnite edit(Indemnite indemnite);
     // видалити об’єкт «Indemnite»
  public void destroy(Indemnite indemnite);
     // пошук об’єкта «Indemnite» за його ідентифікатором
  public Indemnite find(Long id);
     // отримати всі об’єкти «Indemnite»
  public List<Indemnite> findAll();

}
  • у рядку 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] матиме такий вигляд:

package dao;

import java.util.List;
import jpa.Employe;

public interface IEmployeDao {
     // створити новий об’єкт «Співробітник»
  public Employe create(Employe employe);
     // змінити існуючий об’єкт «Співробітник»
  public Employe edit(Employe employe);
     // видалити об’єкт «Співробітник»
  public void destroy(Employe employe);
     // пошук об’єкта «Співробітник» за його ідентифікатором id
  public Employe find(Long id);
     // пошук об’єкта «Співробітник» за номером SS
  public Employe find(String SS);
     // отримати всі об’єкти «Співробітник»
  public List<Employe> findAll();
}
  • у рядку 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:

Його код такий:

package exception;

@SuppressWarnings("serial")
public class PamException extends RuntimeException {

   // код помилки
  private int code;

  public PamException(int code) {
    super();
    this.code = code;
  }

  public PamException(String message, int code) {
    super(message);
    this.code = code;
  }

  public PamException(Throwable cause, int code) {
    super(cause);
    this.code = code;
  }

  public PamException(String message, Throwable cause, int code) {
    super(message, cause);
    this.code = code;
  }

   // геттер і сеттер

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }

}
  • рядок 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] така:

package dao;

...

public class JUnitInitDB {

  private IEmployeDao employeDao = null;
  private ICotisationDao cotisationDao = null;
  private IIndemniteDao indemniteDao = null;

  @BeforeClass
  public void init(){
     // конфігурація додатка
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // шари DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
  }

  @Test
  public void initDB(){
     // заповнення бази даних
...
     // виведення вмісту бази даних
...
  }

  @Before()
  public void clean(){
     // очищення бази даних
...
  }
}
  • Метод [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] виводить такі дані:

------------- Standard Output ---------------
Employés ----------------------
jpa.Employe[id=5,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=6,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1]
Indemnités ----------------------
jpa.Indemnite[id=5,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0]
jpa.Indemnite[id=6,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0]
Cotisations ----------------------
jpa.Cotisation[id=3,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88]
------------- ---------------- ---------------

Таблиці [EMPLOYES, INDEMNITES, COTISATIONS] заповнені. Це можна перевірити за допомогою підключення NetBeans до бази даних [dbpam_hibernate].

  • у [1], на вкладці [services] відображаються дані таблиці [employes] з підключення [dbpam_hibernate] [2],
  • а в [3] — результат.

5.9.6. JUnitDao

Тепер розглянемо другий клас тестів [JUnitDao]:

Структура класу буде такою:

package dao;

import exception.PamException;
...

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");
  }

  @AfterClass
  public static void terminate() {
  }

  @Before()
  public void clean() {
...
  }

   // журнали
  private static void log(String message) {
    System.out.println("----------- " + message);
  }

   // тести
  @Test
  public void test01() {
    log("test01");
     // перелік внесків
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // додавання внеску
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // за запитом
    cotisation = cotisationDao.find(cotisation.getId());
     // перевірка
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(3.49, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // вносимо зміни
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // перевірки
    Assert.assertEquals(cotisation.getVersion() + 1, cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation2.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getSecu(), 1e-6);
     // запит на змінений елемент
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // перевірки
    Assert.assertEquals(cotisation3.getVersion(), cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation3.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getSecu(), 1e-6);
     // видалення елемента
    cotisationDao.destroy(cotisation3);
     // перевірки
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // запит на перелік компенсацій
...
     // додається виплата «Indemnite»
..
     // запитуємо виплату з бази даних – отримуємо виплату1
..
     // перевіряємо, чи виплата1 = виплата
...
     // змінюємо отриману компенсацію та зберігаємо зміну в BD. Отримуємо компенсацію2
 ...
     // перевіряємо версію компенсації2
    ...
     // витягуємо indemnite2 з бази даних — отримуємо indemnite3
    ...
     // перевіряємо, чи indemnite3 = indemnite2
    ...
     // видаляємо з бази зображення indemnite3
    ...
     // шукаємо «indemnite3» у базі даних
    ...
     // перевіряємо, чи отримано нульове посилання
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // повторюємо тест, аналогічний попереднім, для «Співробітника»
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // тестуємо метод [IEmployeDao].find(String SS)
     // спочатку з існуючим співробітником
     // потім із неіснуючим працівником
...
  }

  @Test
  public void test05(){
    log("test05");
     // створюємо дві виплати з однаковим індексом
     // порушує обмеження щодо унікальності індексу
     // перевіряється, чи виникає виняток типу PamException
     // і що вона має очікуваний номер помилки
...
  }

  @Test
  public void test06(){
    log("test06");
     // створюються два співробітники з однаковим номером SS
     //, що порушує обмеження унікальності номера SS
     // перевіряємо, чи виникає виняток типу PamException
     // і що вона має очікуваний номер помилки
...

  }

  @Test
  public void test07(){
    log("test07");
     // створюємо двох співробітників з однаковим номером SS: першого за допомогою команди create, другого — за допомогою команди edit
     // порушує обмеження унікальності номера SS
     // перевіряється, чи виникає виняток типу PamException
     // і що вона має очікуваний номер помилки
...
  }

  @Test
  public void test08(){
    log("test08");
     // видалення неіснуючого співробітника не викликає винятку
     // його додають, а потім видаляють — це перевіряється
...
  }

  @Test
  public void test09(){
    log("test09");
     // зміна даних співробітника без наявності правильної версії повинна викликати виняток
     // це перевіряється
...
  }

  @Test
  public void test10(){
    log("test10");
     // Видалення співробітника без наявності правильної версії має викликати виняток
     // перевіряємо
...

  }

   // методи getter та setter
  ...
}

У попередньому класі тестів база даних очищується перед кожним тестом.


Завдання: написати такі методи:

1 — test02: слід орієнтуватися на test01

2 — test03: у співробітника є поле типу Indemnite. Тому потрібно створити сутності Indemnite та Employe

3 — test04.


Діючи так само, як і для класу тестів [JUnitInitDB], отримуємо такі результати:

  • у [1] виконується клас тестів
  • у [2] — результати тестів у вікні [Test Results]

Спровокуємо помилку, щоб побачити, як вона відображається на сторінці результатів:

  @Test
  public void test01() {
    log("test01");
     // список внесків
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // додаємо внесок
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // запит
    cotisation = cotisationDao.find(cotisation.getId());
     // перевірка
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(0, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // її зміна
....
}

У рядку 13 перевірка спричинить помилку, оскільки значення Csgrds дорівнює 3,49 (рядок 8). Виконання класу тестів дає такі результати:

  • сторінка результатів [1] тепер показує, що деякі тести не пройшли успішно.
  • У [2] наведено короткий опис винятку, через який тест завершився невдало. Там вказано номер рядка коду Java, у якому стався виняток.

5.10. Рівень [metier] додатка [PAM]

Тепер, коли рівень [DAO] написано, перейдемо до вивчення бізнес-рівня [2]:

5.10.1. Інтерфейс Java [IMetier]

Він був описаний у розділі 5.7. Наведемо його ще раз нижче:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // отримати розрахунковий лист
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // список співробітників
  public List<Employe> findAllEmployes();
}

Реалізація шару [metier] буде здійснена в пакеті [metier]:

 

Пакет [metier] міститиме, окрім інтерфейсу [IMetier] та його реалізації [Metier], ще два класи: [FeuilleSalaire] та [ElementsSalaire]. Клас [FeuilleSalaire] було коротко представлено в розділі 5.7. Тепер ми повернемося до нього.

5.10.2. Клас [FeuilleSalaire]

Метод [calculerFeuilleSalaire] інтерфейсу [IMetier] повертає об’єкт типу [FeuilleSalaire], який представляє різні елементи відомості про заробітну плату. Його визначення таке:

package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire implements Serializable{
   // приватні поля
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

   // конструктори
  public FeuilleSalaire() {

  }

  public FeuilleSalaire(Employe employe, Cotisation cotisation, ElementsSalaire elementsSalaire) {
    setEmploye(employe);
    setCotisation(cotisation);
    setElementsSalaire(elementsSalaire);
  }

   // toString
  public String toString() {
    return "[" + employe + "," + cotisation + "," + elementsSalaire + "]";
  }

   // аксесори
...  
}
  • рядок 7: клас реалізує інтерфейс Serializable, оскільки його екземпляри можуть передаватися по мережі.
  • рядок 9: працівник, якого стосується платіжна відомість
  • рядок 10: різні ставки внесків
  • рядок 11: різні надбавки, пов’язані з індексом працівника
  • рядок 12: складові його заробітної плати
  • рядки 14–22: два конструктори класу
  • рядки 25–27: метод [toString], що ідентифікує конкретний об’єкт [FeuilleSalaire]
  • рядки 29 і далі: публічні аксесори до приватних полів класу

Клас [ElementsSalaire], на який є посилання у рядку 11 класу [FeuilleSalaire] вище, об’єднує елементи, що складають розрахунковий лист. Його визначення таке:

package metier;

public class ElementsSalaire implements Serializable{

   // приватні поля
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

   // виробники
  public ElementsSalaire() {

  }

  public ElementsSalaire(double salaireBase, double cotisationsSociales,
    double indemnitesEntretien, double indemnitesRepas,
    double salaireNet) {
    setSalaireBase(salaireBase);
    setCotisationsSociales(cotisationsSociales);
    setIndemnitesEntretien(indemnitesEntretien);
    setIndemnitesRepas(indemnitesRepas);
  }

   // toString
  public String toString() {
    return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
      + indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
      + salaireNet + "]";
  }

   // публічні аксесори
...  
}
  • рядок 3: клас реалізує інтерфейс Serializable, оскільки є компонентом класу FeuilleSalaire, який має бути серіалізованим.
  • рядок 6: базова заробітна плата
  • рядок 7: соціальні внески, сплачені з цієї базової заробітної плати
  • рядок 8: щоденні виплати на утримання дитини
  • рядок 9: щоденні виплати на харчування дитини
  • рядок 10: чиста заробітна плата, що підлягає виплаті няні
  • рядки 12–24: конструктори класу
  • рядки 27–31: метод [toString], що ідентифікує конкретний об’єкт [ElementsSalaire]
  • рядки 34 і далі: публічні методи доступу до приватних полів класу

5.10.3. Клас реалізації [Metier] шару [metier]

Клас реалізації [Metier] шару [metier] може мати такий вигляд:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // посилання на шар [DAO]
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // отримати розрахунковий лист
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // список співробітників
   public List<Employe> findAllEmployes() {
     ...
  }

   // методи getter та setter
...
 }
  • рядок 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] може виглядати так:

package metier;

...

public class JUnitMetier_1 {

// бізнес-шар
  private IMetier metier;

  @BeforeClass
  public void init(){
     // журнал
    log("init");
     // конфігурація додатка
     // інстанціювання шару [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // шари DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // очищення бази даних
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // заповнення бази
    Indemnite indemnite1=indemniteDao.create(new Indemnite(1,1.93,2,3,12));
    Indemnite indemnite2=indemniteDao.create(new Indemnite(2,2.1,2.1,3.1,15));
    Employe employe2=employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    Employe employe1=employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    Cotisation cotisation1=cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }

   // журнали
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // тест
  @Test
  public void test01(){
     // розрахунок платіжних відомостей
    System.out.println(metier.calculerFeuilleSalaire("260124402111742",30, 5));
    System.out.println(metier.calculerFeuilleSalaire("254104940426058",150, 20));
    try {
      System.out.println(metier.calculerFeuilleSalaire("xx", 150, 20));
    } catch (PamException ex) {
      System.err.println(String.format("PamException[Code=%d, message=%s]",ex.getCode(), ex.getMessage()));
    }
  }
}

У класі немає твердження Assert.assertCondition. Ми просто намагаємося обчислити кілька заробітних плат, щоб потім перевірити їх вручну. Екранне зображення, отримане в результаті виконання попереднього класу, виглядає так:

1
2
3
4
5
6
7
Testsuite: metier.JUnitMetier_1
----------- init
....
[jpa.Employe[id=22,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=29,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0],[salaire base=64.85,cotisations sociales=17.45,indemnités d'entretien=10.0,indemnités de repas=15.0,salaire net=72.4]]
[jpa.Employe[id=21,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=30,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0],[salaire base=362.25,cotisations sociales=97.48,indemnités d'entretien=42.0,indemnités de repas=62.0,salaire net=368.77]]
PamException[Code=101, message=L'employé de n°[xx] est introuvable]
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3,234 sec
  • рядок 4: розрахунковий лист Жюстіни Лаверті
  • рядок 5: розрахунковий лист Марі Жувенал
  • рядок 6: виняток, пов'язаний з тим, що працівник із номером SS «xx» не існує.

Запитання: у рядку 17 [JUnitMetier_1] використовується Spring-бін із назвою metier. Наведіть визначення цього біна у файлі [spring-config-metier-dao.xml].


Клас [JUnitMetier_2] може мати такий вигляд:

package metier;

...
public class JUnitMetier_2 {

// бізнес-шар
  private IMetier metier;

  @BeforeClass
  public void init(){
...
  }

   // журнали
  private void log(String message) {
    System.out.println("----------- " + message);
  }

   // тест
  @Test
  public void test01(){
...
  }
}

Клас [JUnitMetier_2] є копією класу [JUnitMetier_1], але цього разу в метод test01 додано перевірки.


Завдання: написати метод test01.


Під час виконання класу [JUnitMetier_2], якщо все пройде успішно, отримаємо такі результати:

Image

5.11. Рівень [ui] додатка [PAM] – версія консоль

Тепер, коли шар [metier] написано, нам залишається написати шари [ui] та [1]:

Ми створимо дві різні реалізації шару [ui]: версію console та графічну версію swing:

5.11.1. Клас [ui.console.Main]

Спочатку розглянемо консольну програму, реалізовану вищезазначеним класом [ui.console.Main]. Її функціонування було описано в розділі 5.3. Скелет класу [Main] може виглядати наступним чином:

package ui.console;

import exception.PamException;
import metier.FeuilleSalaire;
import metier.IMetier;

import java.util.ArrayList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

  /**
   * @param args
   */
  public static void main(String[] args) {
     // локальні дані
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // перевірка кількості параметрів
...
     // список помилок
    ArrayList erreurs = new ArrayList();
     // другий параметр має бути дійсним числом >0
...
     // помилка?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // третій параметр повинен бути цілим числом, більшим за 0
...
     // помилка?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // помилки?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // все гаразд — можна запросити розрахунковий лист
    FeuilleSalaire feuilleSalaire = null;
    try {
       // інстанціювання шару [metier]
      ...
       // розрахунок відомості про заробітну плату
      ...
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.toString());
      return;
    }

     // детальний перегляд
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
    output += ajouteInfo("Nombre d'heures travaillées", args[1]);
    output += ajouteInfo("Nombre de jours travaillés", args[2]);
    output += ajouteInfo("\nInformations Employé", "");
    output += ajouteInfo("Nom", feuilleSalaire.getEmploye().getNom());
    output += ajouteInfo("Prénom", feuilleSalaire.getEmploye().getPrenom());
    output += ajouteInfo("Adresse", feuilleSalaire.getEmploye().getAdresse());
    output += ajouteInfo("Ville", feuilleSalaire.getEmploye().getVille());
    output += ajouteInfo("Code Postal", feuilleSalaire.getEmploye().getCodePostal());
    output += ajouteInfo("Indice", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndice());
    output += ajouteInfo("\nInformations Cotisations", "");
    output += ajouteInfo("CSGRDS", ""+ feuilleSalaire.getCotisation().getCsgrds() + " %");
    output += ajouteInfo("CSGD", ""+ feuilleSalaire.getCotisation().getCsgd() + " %");
    output += ajouteInfo("Retraite", ""+ feuilleSalaire.getCotisation().getRetraite() + " %");
    output += ajouteInfo("Sécurité sociale", ""+ feuilleSalaire.getCotisation().getSecu() + " %");
    output += ajouteInfo("\nInformations Indemnités", "");
    output += ajouteInfo("Salaire horaire", ""+ feuilleSalaire.getEmploye().getIndemnite().getBaseHeure() + " euro");
    output += ajouteInfo("Entretien/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getEntretienJour() + " euro");
    output += ajouteInfo("Repas/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getRepasJour() + " euro");
    output += ajouteInfo("Congés Payés", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndemnitesCP()+ " %");
    output += ajouteInfo("\nInformations Salaire", "");
    output += ajouteInfo("Salaire de base", ""+ feuilleSalaire.getElementsSalaire().getSalaireBase()+ " euro");
    output += ajouteInfo("Cotisations sociales", ""+ feuilleSalaire.getElementsSalaire().getCotisationsSociales()+ " euro");
    output += ajouteInfo("Indemnités d'entretien", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesEntretien()+ " euro");
    output += ajouteInfo("Indemnités de repas", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesRepas()+ " euro");
    output += ajouteInfo("Salaire net", ""+ feuilleSalaire.getElementsSalaire().getSalaireNet() + " euro");

    System.out.println(output);
  }

  static String ajouteInfo(String message, String valeur) {
    return message + " : " + valeur + "\n";
  }
}

Завдання: доповніть наведений вище код.


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]:

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
       // TODO додайте тут свій код обробки:
}

Також генерується Java-код, який пов’язує попередній метод із кліком на кнопку [JButtonSalaire]:

1
2
3
4
5
6
    jButtonSalaire.setText("Salaire");
    jButtonSalaire.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButtonSalaireActionPerformed(evt);
      }
});

Саме рядки 2–5 вказують, що клік (подія типу ActionPerformed) на кнопці [jButtonSalaire] (рядок 2) має оброблятися методом [jButtonSalaireActionPerformed] (рядок 4).

Ми також оброблятимемо подію [caretUpdate] (переміщення курсору введення) у полі введення [jTextFieldHT]. Щоб створити обробник цієї події, ми діємо так само, як і раніше:

Обробник події [caretUpdate] для поля введення [jTextFieldHT] створено:

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }

Також згенеровано Java-код, який пов’язує попередній метод із подією [caretUpdate] у полі введення [jTextFieldHT]:

1
2
3
4
5
    jTextFieldHT.addCaretListener(new javax.swing.event.CaretListener() {
      public void caretUpdate(javax.swing.event.CaretEvent evt) {
        jTextFieldHTCaretUpdate(evt);
      }
});

Рядки 1–4 вказують, що подія [caretUpdate] (рядок 2) для кнопки [jTextFieldHT] (рядок 1) має оброблятися методом [ jTextFieldHTCaretUpdate] (рядок 3).

5.12.4. Ініціалізація графічного інтерфейсу

Повернемося до архітектури нашого додатка:

Рівень [ui] потребує посилання на рівень [metier]. Нагадаємо, як це посилання було отримано в додатку console:

1
2
3
    // інстанціювання шару [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
IMetier metier = (IMetier) ctx.getBean("metier");

У графічному додатку використовується той самий метод. Необхідно, щоб під час його ініціалізації також ініціалізувалося посилання [IMetier metier] із рядка 3 вище. Наразі код, згенерований для графічного інтерфейсу, має такий вигляд:

package ui.swing;

...
public class PamJFrame extends javax.swing.JFrame {

   /** Створює нову форму PamJFrame */
  public PamJFrame() {
    initComponents();
  }

   /** Цей метод викликається зсередини конструктора для
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the Form Editor.
   */
   // <editor-fold defaultstate="collapsed" desc=" Згенерований код ">
  private void initComponents() {
...
  }// </editor-fold>

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }                                        

  private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {                                               
...
  }                                              

  public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new PamJFrame().setVisible(true);
      }
    });
  }

   // Оголошення змінних — не змінювати
  private javax.swing.JButton jButtonSalaire;
...
   // Кінець оголошення змінних

}
  • рядки 29–35: статичний метод [main], який запускає додаток
  • рядок 32: створюється та стає видимим екземпляр графічного інтерфейсу [PamJFrame].
  • рядки 7–9: конструктор графічного інтерфейсу.
  • рядок 8: виклик методу [initComponents], визначеного в рядку 17. Цей метод автоматично згенеровано на основі роботи, виконаної в режимі [Design]. Його не слід змінювати.
  • рядок 21: метод, який керуватиме переміщенням курсора введення в полі [jTextFieldHT]
  • рядок 25: метод, який керуватиме натисканням кнопки [jButtonSalaire]

Щоб додати до попереднього коду наші власні ініціалізації, можна вчинити так:

  /** Створює нову форму PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

   // змінні екземпляру
  private IMetier metier=null;
  private List<Employe> employes=null;
  private String[] employesCombo=null;
  private double heuresTravaillées=0;

   // Власні ініціалізації
  public void doMyInit(){
     // Ініціалізація контексту
    try{
       // інстанціювання рівня [metier]
...
     // список співробітників
...
    }catch (PamException ex){
     // повідомлення про виняток розміщується в [jTextAreaStatus]
...
     // повернення
      return;
    }
     // кнопка «Заробітна плата» заблокована
...
     // jScrollPane1 приховано
...
     // спиннер «відпрацьовані дні»
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // комбінований список співробітників
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • рядок 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.

Нас цікавить така архітектура, де шар JPA тепер реалізується за допомогою EclipseLink:

5.13.1. Проєкт NetBeans

Новий проект NetBeans створюється шляхом копіювання попереднього проекту:

  • у [1]: після клацання правою кнопкою миші на проекті Hibernate виберіть Copy
  • за допомогою кнопки [2] виберіть батьківську папку для нового проекту. Назва папки з’явиться у полі [3].
  • у [4] вкажіть назву нового проєкту
  • у [5] — назву папки проекту
  • у [1] новий проєкт створено. Він має таку саму назву, як і оригінальний,
  • у [2] та [3] його перейменовують на [mv-pam-spring-eclipselink].

Проект потрібно змінити у двох місцях, щоб адаптувати його до нового шару JPA / EclipseLink:

  1. у [4] необхідно змінити конфігураційні файли Spring. У них, а саме, міститься конфігурація шару JPA.
  2. у [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] маємо:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

   <!-- прикладні шари -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- бізнес-процеси -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

   <!-- конфігурація JPA -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
    -->
        <property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
        <property name="generateDdl" value="true" />
   <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
  </bean>

   <!-- джерело даних DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/dbpam_hibernate" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>
....  
</beans>

Рядки 19–36 налаштовують шар JPA. Використовувана реалізація JPA — це Hibernate (рядок 22). Крім того, цільовою базою даних є [dbpam_hibernate] (рядок 41).

Щоб перейти на реалізацію JPA / EclipseLink, рядки 19–35 вище замінюються на наведені нижче:

   <!-- конфігурація JPA -->
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
  -->
        <property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform" />
        <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
</bean>
  • рядок 5: використовувана реалізація JPA — це EclipseLink
  • рядок 9: властивість databasePlatform встановлює цільовий SGBD, у даному випадку MySQL
  • рядок 11: для створення таблиць бази даних під час інстанціювання шару JPA. Тут властивість винесено в коментарі.
  • рядок 7: для відображення на консолі команд SQL, що генеруються шаром JPA. Тут властивість винесено в коментарі.

Крім того, цільовою базою даних стає [dbpam_eclipselink] (рядок 4 нижче):

1
2
3
4
5
6
7
8
9
<!-- джерело даних DBCP -->
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>

5.13.2. Виконання тестів

Перш ніж тестувати весь додаток, варто перевірити, чи проходять тести JUnit із новою реалізацією JPA. Перед цим слід спочатку видалити таблиці з бази даних. Для цього у вкладці [Runtime] у NetBeans, за необхідності, слід створити з’єднання з базою даних dbpam_eclipselink / MySQL5. Після підключення до бази даних dbpam_eclipselink / MySQL5 можна приступити до видалення таблиць, як показано нижче:

  • [1]: до видалення
  • [2]: після видалення

Після цього можна виконати перший тест на шарі [DAO]: InitDB, який заповнює базу даних. Щоб раніше видалені таблиці були відновлені додатком, потрібно переконатися, що в конфігурації Spring для JPA / EclipseLink рядок:

        <property name="generateDdl" value="true" />

існувала та не була закомментована.

Ми збираємо проект (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-агентом. Формат цього параметра такий:

-javaagent:C:\...\spring-agent.jar

[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:

package dao;

...
@Transactional(propagation=Propagation.REQUIRED)
public class IndemniteDao implements IIndemniteDao{

  @PersistenceContext
  private EntityManager em;

   // виробник
  public IndemniteDao() {
  }

   // створити компенсацію
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • рядки 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, такі:

----------- test05
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
ATTENTION: SQL Error: 1062, SQLState: 23000
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
GRAVE: Duplicate entry '1' for key 2
Chaîne des exceptions --------------------------------------
exception.PamException
javax.persistence.EntityExistsException
org.hibernate.exception.ConstraintViolationException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

Тест пройшов успішно, c.a.d. Перевірено всі твердження, і з тестового методу не виходить жодного винятку.


Запитання: поясніть, що сталося.


Результати, отримані з використанням шару JPA / EclipseLink, такі:

----------- test05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Виняток [EclipseLink-4002] (Eclipse Persistence Services — 2.0.0.v20091127-r5931): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 2
Error Code: 1062
Call: INSERT INTO INDEMNITES (ID, ENTRETIEN_JOUR, REPAS_JOUR, INDICE, INDEMNITES_CP, BASE_HEURE, VERSION) VALUES (?, ?, ?, ?, ?, ?, ?)
        bind => [108, 2.0, 3.0, 1, 12.0, 1.93, 1]
Query: InsertObjectQuery(jpa.Indemnite[id=108,version=1,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0])
Chaîne des exceptions --------------------------------------
org.springframework.transaction.TransactionSystemException
javax.persistence.RollbackException
org.eclipse.persistence.exceptions.DatabaseException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

Як і раніше з 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.