Skip to content

4. Короткий огляд JPA « »

Ми пропонуємо ознайомитися з JPA (Java Persistence API) на прикладі кількох прикладів. JPA розглядається в курсі:

  • «Практичний підхід до Java Persistence 5: [http://tahe.developpez.com/java/jpa]» — надає інструменти для побудови рівня доступу до даних за допомогою JPA

4.1. Місце JPA у багатошаровій архітектурі

Читачеві пропонується ще раз ознайомитися з початком цього документа (параграф 2), де пояснюється роль шару JPA у багатошаровій архітектурі. Шар JPA вписується в шари доступу до даних:

Рівень [DAO] взаємодіє зі специфікацією JPA. Незалежно від того, який продукт її реалізує, інтерфейс шару JPA, що надається шару [DAO], залишається незмінним. Далі ми наведемо кілька прикладів із [ref1], які допоможуть нам створити власний рівень JPA.

4.2. JPA — приклади

4.2.1. Приклад 1 — Об’єктне представлення єдиної таблиці

4.2.1.1. Таблиця [personne]

Розглянемо базу даних, що містить єдину таблицю [personne], призначенням якої є зберігання певної інформації про осіб:

 
ID
первинний ключ таблиці
VERSION
версія запису в таблиці. Кожного разу, коли інформація про особу змінюється, її номер версії збільшується на 1.
NOM
ім’я особи
PRENOM
її ім’я
DATENAISSANCE
дата народження
MARIE
ціле число 0 (неодружений) або 1 (одружений)
NBENFANTS
кількість дітей цієї особи

4.2.1.2. Елемент [Personne]

Ми знаходимося в такому середовищі виконання:

Рівень JPA [5] повинен слугувати мостом між реляційним світом бази даних [7] та об’єктним світом [4], з яким працюють програми Java [3]. Цей міст створюється шляхом конфігурації, і для цього існує два способи:

  1. за допомогою файлів XML. Це був практично єдиний спосіб до появи JDK 1.5
  2. за допомогою Java-анотацій, починаючи з версії JDK 1.5

У цьому документі ми використовуватимемо виключно другий метод.

Об’єкт [Personne], що відображає таблицю [personne], представлену раніше, може виглядати так:


...

@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

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

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }

    // toString
    public String toString() {
...
    }

    // гетери та сеттери
...
}

Налаштування здійснюється за допомогою Java-анотацій @Annotation. Анотації Java обробляються або компілятором, або спеціалізованими інструментами під час виконання. За винятком анотації в рядку 3, призначеної для компілятора, усі інші анотації тут призначені для використовуваної реалізації JPA — Hibernate або Toplink. Отже, вони будуть оброблятися під час виконання. За відсутності інструментів, здатних їх інтерпретувати, ці анотації ігноруються. Таким чином, наведений вище клас [Personne] може використовуватися в контексті поза JPA.

Слід розрізняти два випадки використання анотацій JPA у класі C, пов’язаному з таблицею T:

  1. таблиця T вже існує: у цьому випадку анотації JPA повинні відтворювати існуючі дані (назви та визначення стовпців, обмеження цілісності, зовнішні ключі, первинні ключі тощо)
  2. таблиця T не існує і буде створена на основі анотацій, знайдених у класі C.

Випадок 2 є найпростішим у обробці. За допомогою анотацій JPA ми вказуємо бажану структуру таблиці T. Випадок 1 часто є складнішим. Таблиця T могла бути створена давно, поза будь-яким контекстом JPA. Тоді її структура може бути недостатньо пристосованою до реляційно-об’єктного мосту JPA. Для спрощення розглянемо випадок 2, де таблиця T, пов’язана з класом C, буде створена на основі анотацій JPA класу C.

Прокоментуємо анотації JPA класу [Personne]:

  • рядок 4: анотація @Entity є першою обов’язковою анотацією. Вона розміщується перед рядком, що оголошує клас, і вказує, що даний клас має управлятися рівнем персистентності JPA. За відсутності цієї анотації всі інші анотації JPA ігноруватимуться.
  • рядок 5: анотація @Table позначає таблицю бази даних, яку представляє даний клас. Її головним аргументом є name, що вказує на ім’я таблиці. За відсутності цього аргументу таблиця матиме ім’я класу, у даному випадку [Personne]. Отже, у нашому прикладі анотація @Table є зайвою.
  • рядок 8: анотація @Id слугує для позначення поля в класі, яке є відображенням первинного ключа таблиці. Ця анотація є обов’язковою. Вона вказує, що поле id у рядку 11 є відображенням первинного ключа таблиці.
  • рядок 9: анотація @Column слугує для встановлення зв’язку між полем класу та стовпцем таблиці, якому це поле відповідає. Атрибут name вказує назву стовпця в таблиці. За відсутності цього атрибута стовпець має таку саму назву, як і поле. Отже, у нашому прикладі аргумент name не був обов’язковим. Аргумент nullable=false вказує, що стовпець, пов’язаний із полем, не може мати значення NULL, а отже, поле обов’язково має мати значення.
  • рядок 10: анотація @GeneratedValue вказує, як формується первинний ключ, коли він генерується автоматично за допомогою SGBD. Так буде у всіх наших прикладах. Це не є обов’язковим. Так, наша особа може мати номер студента, який слугуватиме первинним ключем і який не генеруватиметься функцією SGBD, а встановлюватиметься додатком. У цьому випадку анотація @GeneratedValue буде відсутня. Аргумент strategy вказує, як генерується первинний ключ, коли його генерує SGBD. Не всі SGBD використовують однакову техніку генерації значень первинного ключа. Наприклад:
Firebird
використовує генератор значень, який викликається перед кожним вставленням
SQL server
поле первинного ключа визначено як тип Identity. Результат схожий на генератор значень Firebird, за винятком того, що значення ключа стає відомим лише після вставки рядка.
Oracle
використовує об’єкт із назвою SEQUENCE, який тут також виконує роль генератора значень

Рівень JPA повинен генерувати різні команди SQL залежно від SGBD для створення генератора значень. Через конфігурацію їй вказується тип об’єкта SGBD, яким вона має керувати. Таким чином, вона може визначити, яка стратегія зазвичай використовується для генерації значень первинного ключа цього об’єкта SGBD. Аргумент strategy = GenerationType.AUTO вказує шару JPA, що він повинен використовувати цю звичайну стратегію. Ця техніка працювала у всіх прикладах цього документа для семи використаних SGBD.

  • рядок 14: анотація @Version позначає поле, яке використовується для управління паралельним доступом до одного й того самого рядка таблиці.

Щоб зрозуміти цю проблему одночасного доступу до одного й того самого рядка таблиці [personne], припустимо, що веб-додаток дозволяє оновлювати дані про особу, і розглянемо такий випадок:

У момент часу T1 користувач U1 переходить до редагування особи P. На цей момент кількість дітей дорівнює 0. Він змінює цю кількість на 1, але до того, як він підтвердить свої зміни, користувач U2 переходить до редагування тієї самої особи P. Оскільки U1 ще не підтвердив свої зміни, U2 бачить на своєму екрані, що кількість дітей дорівнює 0. U2 переводить ім’я особи P у великі літери. Потім U1 і U2 підтверджують свої зміни в такому порядку. Переможе зміна, внесена U2: у базі даних ім’я буде переведено у великі літери, а кількість дітей залишиться рівною нулю, хоча U1 вважає, що змінив її на 1.

Поняття «версії особи» допомагає нам вирішити цю проблему. Розглянемо той самий приклад:

У момент часу T1 користувач U1 переходить до редагування особи P. На цей момент кількість дітей дорівнює 0, а версія — V1. Він змінює кількість дітей на 1, але перш ніж він підтвердить свою зміну, користувач U2 переходить до редагування тієї самої особи P. Оскільки U1 ще не підтвердив свою зміну, U2 бачить, що кількість дітей дорівнює 0, а версія — V1. U2 змінює ім’я особи P на великі літери. Потім U1 та U2 підтверджують свої зміни в цьому порядку. Перед підтвердженням зміни перевіряється, чи користувач, який вносить зміни до особи P, має ту саму версію, що й особа P, яка наразі зареєстрована. Це стосується користувача U1. Отже, його зміна приймається, і версія зміненої особи змінюється з V1 на V2, щоб відзначити, що особа зазнала змін. Під час підтвердження зміни U2 стане відомо, що U2 містить версію V1 особи P, тоді як на даний момент її версія — V2. Тоді ми зможемо повідомити користувачу U2, що хтось встиг зробити це раніше за нього і що йому слід почати з нової версії особи P. Він це зробить, отримає особу P у версії V2, яка тепер має дитину, переведе ім’я у великі літери та підтвердить зміни. Його зміна буде прийнята, якщо зареєстрована особа P все ще має версію V2. У підсумку зміни, внесені U1 та U2, будуть враховані, тоді як у випадку використання без версій одна зі змін була б втрачена.

Рівень [DAO] клієнтського додатка може самостійно керувати версією класу [Personne]. Щоразу, коли відбуватиметься зміна об’єкта P, версія цього об’єкта в таблиці збільшуватиметься на 1. Анотація @Version дозволяє передати це управління на рівень JPA. Відповідне поле зовсім не обов’язково має називатися version, як у прикладі. Воно може мати будь-яку назву.

Поля, що відповідають анотаціям @Id та @Version, існують через необхідність збереження даних. Вони не були б потрібні, якби клас [Personne] не потребував збереження. Отже, бачимо, що об’єкт має різне представлення залежно від того, чи потрібно його зберігати.

  • рядок 17: знову анотація @Column, що надає інформацію про стовпець таблиці [personne], пов’язаний із полем nom класу Personne. Тут ми бачимо два нових аргументи:
    • unique=true вказує, що ім’я особи має бути унікальним. У базі даних це призведе до додавання обмеження унікальності для стовпця NOM таблиці [personne].
    • length=30 встановлює кількість символів у стовпці NOM на 30. Це означає, що тип цього стовпця буде VARCHAR(30).
  • рядок 24: анотація @Temporal використовується для вказівки типу SQL для стовпця/поля типу «дата/час». Тип TemporalType.DATE позначає лише дату без пов’язаного з нею часу. Інші можливі типи — це TemporalType.TIME для кодування часу та TemporalType.TIMESTAMP для кодування дати з часом.

Тепер прокоментуємо решту коду класу [Personne]:

  • рядок 6: клас реалізує інтерфейс Serializable. sérialisation об’єкта полягає в його перетворенні на послідовність бітів. désérialisation — це зворотна операція. Серіалізація/десеріалізація, зокрема, використовується в клієнт-серверних додатках, де об’єкти обмінюються через мережу. Клієнтські або серверні додатки не знають про цю операцію, яка виконується прозоро за допомогою JVM. Однак для її здійснення необхідно, щоб класи обмінюваних об’єктів були «позначені» ключовим словом Serializable.
  • рядок 37: конструктор класу. Зверніть увагу, що поля id та version не входять до складу параметрів. Дійсно, ці два поля управляються рівнем JPA, а не додатком.
  • рядки 51 і далі: методи get та set для кожного з полів класу. Слід зазначити, що анотації JPA можна розміщувати на методах get полів замість того, щоб розміщувати їх на самих полях. Місце розташування анотацій визначає режим, який повинен використовувати JPA для доступу до полів:
    • якщо анотації розміщені на рівні полів, JPA отримуватиме прямий доступ до полів для їхнього читання або запису
    • якщо анотації розміщені на рівні get, JPA отримуватиме доступ до полів через методи get / set для їх читання або запису

Саме положення анотації @Id визначає положення анотацій JPA у класі. Розміщена на рівні поля, вона вказує на прямий доступ до полів, а розміщена на рівні get — на доступ до полів через get та set. Інші анотації повинні бути розміщені так само, як і анотація @Id.

4.2.2. Налаштування шару JPA

Тестування шару JPA можна проводити за допомогою такої архітектури:

  • у [7]: база даних, яка буде згенерована на основі анотацій сутності [Personne], а також додаткових налаштувань, зроблених у файлі з назвою [persistence.xml]
  • в [5, 6]: шар JPA, реалізований за допомогою Hibernate
  • у [4]: суть [Personne]
  • у [3]: консольна тестова програма

Конфігурація шару JPA забезпечується файлом [META-INF/persistence.xml]:

Під час виконання файл [META-INF/persistence.xml] шукається у файлі Classpath додатка.

Розглянемо конфігурацію шару JPA, визначену у файлі [persistence.xml] нашого проєкту:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  провайдер -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Персистентні класи -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- журнали SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- підключення JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!-- автоматичне створення схеми -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- діалект -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  властивості DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Щоб зрозуміти цю конфігурацію, нам слід повернутися до архітектури доступу до даних нашого додатка:

  • файл [persistence.xml] налаштовує шари [4, 5, 6]
  • [4]: реалізація JPA за допомогою Hibernate
  • [5]: Hibernate отримує доступ до бази даних через пул з’єднань. Пул з’єднань — це резерв відкритих з’єднань із SGBD. До SGBD звертаються багато користувачів, хоча з міркувань продуктивності кількість одночасно відкритих з’єднань не може перевищувати граничного значення N. Добре написаний код відкриває з’єднання з SGBD на мінімальний проміжок часу: він надсилає команди SQL і закриває з’єднання. Він повторюватиме це щоразу, коли йому потрібно працювати з базою даних. Витрати на відкриття/закриття з’єднання є значними, і саме тут на допомогу приходить пул з’єднань. Під час запуску програми пул відкриває N1 з’єднань із SGBD. Саме до нього програма звертатиметься за відкритим з’єднанням, коли це буде потрібно. Це з’єднання буде повернуто до пулу, щойно програма більше не потребуватиме його, бажано якомога швидше. З’єднання не закривається і залишається доступним для наступного користувача. Отже, пул з’єднань — це система спільного використання відкритих з’єднань.
  • [6]: драйвер JDBC, що використовується SGBD

Тепер давайте розглянемо, як файл [persistence.xml] налаштовує вищезазначені шари [4, 5, 6]:

  • рядок 2: кореневий тег файлу XML — <persistence>.
  • рядок 3: <persistence-unit> слугує для визначення одиниці персистентності. Може бути кілька одиниць персистентності. Кожна з них має ім’я (атрибут name) та тип транзакцій (атрибут transaction-type). Додаток матиме доступ до одиниці персистентності через її ім’я, у даному випадку jpa. Тип транзакції RESOURCE_LOCAL вказує, що додаток самостійно керує транзакціями з SGBD. Саме так буде в даному випадку. Коли додаток виконується в контейнері EJB3, він може використовувати його службу транзакцій. У цьому випадку слід вказати transaction-type=JTA (Java Transaction API). JTA є значенням за замовчуванням, коли атрибут transaction-type відсутній.
  • рядок 5: тег <provider> використовується для визначення класу, що реалізує інтерфейс [javax.persistence.spi.PersistenceProvider], який дозволяє додатку ініціалізувати рівень збереження даних. Оскільки ми використовуємо реалізацію JPA / Hibernate, клас, що використовується тут, є класом Hibernate.
  • рядок 6: тег <properties> вводить властивості, характерні для конкретного обраного provider. Отже, залежно від того, чи обрано Hibernate, Toplink, Kodo тощо, ми матимемо різні властивості. Наведені нижче властивості характерні саме для Hibernate.
  • рядок 8: запит до Hibernate про сканування classpath проекту з метою пошуку класів з анотацією @Entity для їхнього управління. Класи @Entity також можна оголошувати за допомогою тегів <class>nom_de_la_classe</class>, безпосередньо під тегом <persistence-unit>. Саме це ми зробимо з provider JPA / Toplink.
  • Рядки 10–12, які тут винесені в коментарі, налаштовують консольні журнали Hibernate:
    • рядок 10: визначає, чи відображати команди, що видаються Hibernate на SQL. Це дуже корисно на етапі навчання. Через реляційно-об’єктний міст додаток працює з об’єктами, що зберігаються у базі даних, до яких він застосовує операції типу [persist, merge, remove]. Дуже цікаво дізнатися, які саме команди SQL насправді генеруються під час цих операцій. Вивчаючи їх, поступово можна вгадати команди SQL, які Hibernate згенерує під час виконання певної операції над об’єктами, що зберігаються, і реляційно-об’єктний міст починає набувати чітких обрисів у свідомості.
    • рядок 11: команди SQL, що відображаються на консолі, можна гарно відформатувати, щоб полегшити їхнє читання
    • рядок 12: команди SQL, що виводяться, також будуть супроводжуватися коментарями
  • рядки 15–19 визначають шар JDBC (шар [6] в архітектурі):
    • рядок 15: клас драйвера JDBC для SGBD, тут — MySQL5
    • рядок 16: URL-адреса використовуваної бази даних
    • рядки 17, 18: ім’я користувача для підключення та його пароль
  • рядок 22: Hibernate повинен знати, з яким саме SGBD він має справу. Справа в тому, що всі SGBD мають власні розширення SQL — це особливий спосіб управління автоматичним генеруванням значень первинного ключа, ... через що Hibernate повинен знати SGBD, з яким він працює, щоб надсилати йому команди SQL, які той зрозуміє. [MySQL5InnoDBDialect] позначає SGBD MySQL5 із таблицями типу InnoDB, що підтримують транзакції.
  • рядки 24–28 налаштовують пул з’єднань c3p0 (рівень [5] в архітектурі):
    • рядки 24, 25: мінімальна (за замовчуванням 3) та максимальна кількість з’єднань (за замовчуванням 15) у пулі. Початкова кількість з’єднань за замовчуванням становить 3.
    • рядок 26: максимальний час очікування запиту на з’єднання від клієнта в мілісекундах. Після закінчення цього часу c3p0 поверне клієнту виняток.
    • рядок 27: для доступу до BD Hibernate використовує підготовлені запити SQL (PreparedStatement), які c3p0 може зберігати у кеші. Це означає, що якщо додаток вдруге запитує підготовлену команду SQL, яка вже знаходиться в кеші, її не потрібно буде готувати заново (підготовка команди SQL пов’язана з витратами), і буде використана та, що знаходиться в кеші. Тут вказується максимальна кількість підготовлених команд SQL, які може містити кеш для всіх з’єднань разом (одна підготовлена команда SQL належить до одного з’єднання).
    • рядок 28: частота перевірки дійсності з’єднань у мілісекундах. З’єднання з пулу може стати недійсним з різних причин (драйвер JDBC визнає з’єднання недійсним через надто тривалий час, драйвер JDBC має «баги» тощо).
  • рядок 20: тут задається, щоб під час ініціалізації модуля персистентності було згенеровано базу даних-образ об’єктів @Entity. Hibernate тепер має всі інструменти для видачі команд SQL щодо генерації таблиць бази даних:
    • конфігурація об’єктів @Entity дозволяє йому визначити, які таблиці потрібно згенерувати
    • рядки 15–18 та 24–28 дозволяють йому встановити з’єднання з SGBD
    • рядок 22 дозволяє визначити, який діалект SQL використовувати для генерації таблиць

Таким чином, файл [persistence.xml], який використовується тут, створює нову базу даних при кожному новому запуску програми. Таблиці створюються заново (create table) після їх видалення (drop table), якщо вони раніше існували. Слід зауважити, що, звісно, це не слід робити з базою даних у виробничому середовищі...

4.2.3. Приклад 2: відношення «один до багатьох»

4.2.3.1. -схема бази даних

 
1
2

    alter table jpa06_article 
        drop 
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article 
        add index FKFFBDD9D8ECCE8750 (categorie_id), 
        add constraint FKFFBDD9D8ECCE8750 
        foreign key (categorie_id) 
references jpa06_categorie (id);
  • у [1] — база даних, а в [2] — її DDL (MySQL5)

Стаття A(id, версія, назва) належить саме до однієї категорії C(id, версія, назва). Категорія C може містити 0, 1 або кілька статей. Маємо відношення «один до багатьох» (Категорія -> Стаття) та зворотне відношення «багато до одного» (Стаття -> Категорія). Цей зв’язок реалізується за допомогою зовнішнього ключа, який таблиця [article] має до таблиці [categorie] (рядки 24–28 таблиці DDL).

4.2.3.2. Об’єкти @Entity, що представляють базу даних

Стаття представлена наступним @Entity [Article]:


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // поля
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // головна реляція «Стаття» (багато) -> «Категорія» (одна)
    // реалізована за допомогою зовнішнього ключа (categorie_id) у «Статті»
    // 1 «Стаття» обов’язково має 1 «Категорію» (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

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

    // методи getter та setter
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }

}
  • рядки 9–11: первинний ключ @Entity
  • рядки 13–15: її номер версії
  • рядки 17–18: назва статті
  • рядки 20–25: відношення «багато до одного», яке пов’язує @Entity Article з @Entity Categorie:
    • рядок 23: анотація ManyToOne. «Many» (багато) відноситься до @Entity Article, в якій ми перебуваємо, а «One» (один) — до @Entity Categorie (рядок 25). Одна категорія (One) може мати кілька статей (Many).
    • рядок 24: анотація ManyToOne визначає стовпець із зовнішнім ключем у таблиці [article]. Вона матиме назву (name) categorie_id, і кожен рядок повинен мати значення в цьому стовпці (nullable=false).
    • рядок 25: категорія, до якої належить стаття. Коли стаття буде поміщена в контекст персистентності, вимагається, щоб її категорія не вносилася туди одразу (fetch=FetchType.LAZY, рядок 23). Невідомо, чи має цей запит сенс. Побачимо.

Категорія представлена наступною @Entity [Categorie]:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // поля
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // зворотний зв'язок «Категорія (один)» -> «Стаття (багато)» у зв'язку «Стаття (багато)» -> «Категорія (один)»
    // каскадне вставлення «Категорія» -> вставлення «Статті»
    // каскадне оновлення «Категорія» -> оновлення «Статті»
    // каскадне видалення «Категорія» → видалення «Статті»
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

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

    // методи getter та setter
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // двостороннє зв'язування «Категорія» <--> «Стаття»
    public void addArticle(Article article) {
        // стаття додається до колекції статей категорії
        articles.add(article);
        // стаття змінює категорію
        article.setCategorie(this);
    }
}
  • рядки 8–11: первинний ключ @Entity
  • рядки 12–14: її версія
  • рядки 16–17: назва категорії
  • рядки 19–24: набір (set) статей категорії
    • рядок 23: анотація @OneToMany позначає відношення «один до багатьох». «One» позначає @Entity [Categorie], в якій ми перебуваємо, а «Many» — тип [Article] із рядка 24: одна (One) категорія має кілька (Many) статей.
    • рядок 23: анотація є оберненою (mappedBy) до анотації ManyToOne, розміщеної на полі categorie сутності @Entity Article: mappedBy=categorie. Відношення ManyToOne, розміщене на полі categorie сутності @Entity Article, є головним відношенням. Воно є обов’язковим. Вона реалізує відношення зовнішнього ключа, яке пов’язує @Entity Article з @Entity Categorie. Відношення OneToMany, встановлене на полі articles @Entity Categorie, є зворотним відношенням. Воно не є обов’язковим. Це зручний спосіб отримання статей певної категорії. Без цього зручного способу ці статті отримувалися б за допомогою запиту JPQL.
    • рядок 23: cascadeType.ALL вимагає, щоб операції (persist, merge, remove), виконані над @Entity Categorie, поширювалися на його статті.
    • рядок 24: статті категорії будуть розміщені в об’єкті типу Set<Article>. Тип Set не допускає дублікатів. Отже, не можна двічі додати одну й ту саму статтю до об’єкта Set<Article>. Що означає «одна й та сама стаття»? Щоб сказати, що товар a є тим самим, що й товар b, Java використовує вираз a.equals(b). У класі Object, базовому для всіх класів, a.equals(b) є істинним, якщо a==b, c.a.d. якщо об’єкти a та b мають однакову пам’ятну адресу. Можна було б сказати, що елементи a та b є однаковими, якщо вони мають однакову назву. У цьому випадку розробник повинен перевизначити два методи у класі [Article]:
  • equals: повинен повертати «true», якщо обидва елементи мають однакову назву
  • hashCode: повинен повертати однакове ціле значення для двох об’єктів [Article], які метод equals вважає рівними. Отже, у цьому випадку значення буде побудовано на основі назви статті. Значення, яке повертає hashCode, може бути будь-яким цілим числом. Воно використовується в різних контейнерах об’єктів, зокрема в словниках (Hashtable).

Відношення OneToMany може використовувати інші типи, крім Set, для зберігання «Many», наприклад, об’єкти типу List. У цьому документі ми не розглядатимемо ці випадки. Читач знайде їх у [ref1].

  • рядок 38: метод [addArticle] дозволяє додати статтю до категорії. Метод оновлює обидва кінці відношення OneToMany, яке пов’язує [Categorie] із [Article].

4.3. API на рівні JPA

Розглянемо середовище виконання клієнта JPA:

Ми знаємо, що шар JPA [2] створює міст між об’єктом [3] та реляційною базою даних [4]. «Контекст персистентності» — це сукупність об’єктів, що управляються шаром JPA у рамках цього об’єктно-реляційного мосту. Щоб отримати доступ до даних контексту персистентності, клієнт JPA [1] повинен звернутися до шару JPA [2]:

  1. він може створити об’єкт і звернутися до шару JPA із запитом на його збереження. Після цього об’єкт стає частиною контексту збереження.
  2. він може звернутися до шару [JPA] із запитом на посилання на існуючий персистентний об’єкт.
  3. він може змінити об’єкт, отриманий з шару JPA.
  4. він може звернутися до шару JPA із запитом на видалення об’єкта з контексту збереження.

Рівень JPA надає клієнту інтерфейс під назвою [EntityManager], який, як випливає з назви, дозволяє керувати об’єктами @Entity у контексті персистентності. Нижче наведено основні методи цього інтерфейсу:

void persist(Object entity)
додає entity до контексту персистентності
void remove(Object entity)
видаляє entity з контексту персистентності
<T> T merge(T entity)
об'єднує об'єкт entity клієнта, який не керується контекстом збереження
з об’єктом entity з контексту збереження, що має той самий первинний ключ.
У результаті отримується об’єкт entity з контексту збереження.
<T> T find(Class<T> entityClass,
Object primaryKey)
додає до контексту збереження об’єкт, знайдений у базі даних
за його первинним ключем. Тип T об’єкта дозволяє шару JPA
визначити, до якої таблиці слід звернутися. Створений таким чином об’єкт, що зберігається, повертається клієнту.
Query createQuery(String queryText)
створює об’єкт Query на основі запиту JPQL (Java Persistence
Query Language). Запит JPQL аналогічний запиту SQL, якщо
за винятком того, що він запитує об’єкти, а не таблиці.
Query createNativeQuery(String queryText)
метод, аналогічний попередньому, за винятком того, що queryText є
SQL, а не JPQL.
Query createNamedQuery(String name)
Метод ідентичний createQuery, за винятком того, що порядок JPQL queryText
був винесений у файл конфігурації та пов’язаний з іменем.
Саме ця назва є параметром методу.

Об’єкт EntityManager має життєвий цикл, який не обов’язково збігається з життєвим циклом додатка. Він має початок і кінець. Таким чином, клієнт JPA може послідовно працювати з різними об’єктами EntityManager. Контекст збереження, пов'язаний з об'єктом EntityManager, має такий самий життєвий цикл, як і сам об'єкт. Вони нерозривно пов'язані між собою. Коли об'єкт EntityManager закривається, його контекст збереження, за необхідності, синхронізується з базою даних, після чого перестає існувати. Щоб знову отримати контекст збереження, потрібно створити новий EntityManager.

Клієнт JPA може створити EntityManager, а отже, і контекст збереження, за допомогою такої інструкції:


EntityManagerFactory emf = Persistence.createEntityManagerFactory("nom d'une unité de persistance");
  • javax.persistence.Persistence — це статичний клас, що дозволяє отримати фабрику (factory) об’єктів EntityManager. Ця фабрика пов’язана з конкретною одиницею персистентності. Нагадаємо, що конфігураційний файл [META-INF/persistence.xml] дозволяє визначати одиниці збереження, і що кожна з них має ім’я:

    <persistence-unit name="elections-dao-jpa-mysql-01PU" transaction-type="RESOURCE_LOCAL">

У наведеному вище прикладі одиниця збереження має назву elections-dao-jpa-mysql-01PU. З нею пов’язана ціла конфігурація, зокрема SGBD, з якою вона працює. Інструкція [Persistence.createEntityManagerFactory("elections-dao-jpa-mysql-01PU")] створює фабрику об’єктів типу EntityManagerFactory, здатну надавати об’єкти EntityManager, призначені для управління контекстами персистентності, пов’язаними з одиницею персистентності з назвою elections-dao-jpa-mysql-01PU. Отримання об’єкта EntityManager, а отже, і контексту персистентності, здійснюється на основі об’єкта EntityManagerFactory таким чином:

        EntityManager em = emf.createEntityManager();

Наступні методи інтерфейсу [EntityManager] дозволяють керувати життєвим циклом контексту збереження:

void close()
контекст збереження закривається. Примусово синхронізує контекст збереження з базою даних:
  • якщо об’єкт контексту відсутній у базі даних, він додається туди за допомогою операції SQL INSERT)
  • якщо об’єкт контексту присутній у базі даних і був змінений з моменту його зчитування, виконується операція SQL UPDATE для збереження зміни
  • якщо об’єкт контексту було позначено як «видалений» після виконання над ним операції remove, виконується операція SQL DELETE для його видалення з бази даних.
void clear()
Контекст збереження очищено від усіх об’єктів, але не закрито.
void flush()
контекст збереження синхронізується з базою даних у спосіб, описаний для close()

Клієнт JPA може примусово ініціювати синхронізацію контексту збереження з базою даних за допомогою методу [EntityManager].flush, описаного вище. Синхронізація може бути явною або неявною. У першому випадку клієнт повинен виконувати операції flush, коли він хоче здійснити синхронізацію; в іншому випадку синхронізація відбувається в певні моменти, які ми уточнимо. Режим синхронізації керується наступними методами інтерфейсу [EntityManager]:

void setFlushMode(FlushModeType
flushMode)
Для flushmode можливі два значення:
FlushModeType.AUTO (за замовчуванням): синхронізація відбувається перед
кожного запиту SELECT, що надходить до бази даних.
FlushModeType.COMMIT: синхронізація відбувається лише після
завершення транзакцій у базі даних.
FlushModeType getFlushMode()
показує поточний режим синхронізації

Підсумуємо. У режимі FlushModeType.AUTO, який є режимом за замовчуванням, контекст збереження даних синхронізується з базою даних у такі моменти:

  1. перед кожною операцією SELECT у базі даних
  2. після завершення транзакції в базі даних
  3. після операції flush або close над контекстом збереження

У режимі FlushModeType.COMMIT все відбувається так само, за винятком операції 1, яка не виконується. Звичайний режим взаємодії з шаром JPA — це транзакційний режим. Клієнт виконує різні операції з контекстом збереження даних у межах транзакції. У цьому випадку моменти синхронізації контексту збереження даних із базою даних відповідають випадкам 1 і 2, описаним вище для режиму AUTO, а для режиму COMMIT — лише випадку 2.

На завершення розглянемо API інтерфейсу Query, який дозволяє надсилати команди JPQL до контексту збереження або команди SQL безпосередньо до бази даних для пошуку даних. Інтерфейс Query має такий вигляд:

  • 1 — метод getResultList виконує SELECT, який повертає кілька об’єктів. Ці об’єкти будуть отримані в об’єкті List. Цей об’єкт є інтерфейсом. Він надає об’єкт Iterator, який дозволяє переглядати елементи списку L у такому вигляді:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
             // використати об’єкт iterator.next(), який представляє поточний елемент списку
...
}

Список L також можна використовувати за допомогою об’єкта for:


        for (Object o : L) {
             // використати об’єкт o
}
  • 2 — метод getSingleResult виконує команду JPQL / SQL SELECT, яка повертає єдиний об’єкт.
  • 3 — метод executeUpdate виконує команду SQL (update або delete) і повертає кількість рядків, на які вплинула операція.
  • 4 - метод setParameter(String, Object) дозволяє присвоїти значення іменованому параметру налаштованої команди JPQL
  • 5 — метод setParameter(int, Object), але параметр позначається не за іменем, а за позицією в команді JPQL.

4.4. Запити JPQL

JPQL (Java Persistence Query Language) — це мова запитів рівня JPA. Мова JPQL споріднена з мовою SQL баз даних. У той час як SQL працює з таблицями, JPQL працює з об’єктами-зображеннями цих таблиць. Ми розглянемо приклад у рамках такої архітектури:

База даних, яку ми назвемо [dbrdvmedecins2] , є базою даних MySQL5 із чотирма таблицями:

  

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

4.4.1. Таблиця [MEDECINS]

Вона містить інформацію про лікарів.

  • ID: номер, що ідентифікує лікаря — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • NOM: прізвище лікаря
  • PRENOM: його ім’я
  • TITRE: його/її титул (пані, пані, пан)

4.4.2. Таблиця [CLIENTS]

Пацієнти різних лікарів заносяться до таблиці [CLIENTS]:

  • ID: номер, що ідентифікує клієнта — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 кожного разу, коли до рядка вносяться зміни.
  • NOM: ім’я клієнта
  • PRENOM: його ім’я
  • TITRE: титул (пані, пані, пан)

4.4.3. Таблиця [CRENEAUX]

У ній перелічено часові проміжки, у яких можливі RV:

  • ID: номер, що ідентифікує часовий проміжок — первинний ключ таблиці (рядок 8)
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • ID_MEDECIN: номер, що ідентифікує лікаря, якому належить цей часовий проміжок — зовнішній ключ у стовпці MEDECINS (ID).
  • HDEBUT: час початку часового проміжку
  • MDEBUT: хвилини початку часового проміжку
  • HFIN: година закінчення часового проміжку
  • MFIN: хвилини закінчення інтервалу

Другий рядок таблиці [CRENEAUX] (див. [1] вище) вказує, наприклад, що слот № 2 починається о 8:20 і закінчується о 8:40 та належить лікарю № 1 (пані Марі PELISSIER).

4.4.4. Таблиця [RV]

У ній наведено перелік RV, призначених кожному лікарю:

  • ID: номер, що однозначно ідентифікує RV — первинний ключ
  • JOUR: день RV
  • ID_CRENEAU: часовий проміжок запису RV — зовнішній ключ до поля [ID] таблиці [CRENEAUX] — визначає як часовий проміжок, так і відповідного лікаря.
  • ID_CLIENT: номер клієнта, для якого зроблено бронювання — зовнішній ключ у полі [ID] таблиці [CLIENTS]

Ця таблиця має обмеження унікальності ( ) для значень з’єднаних стовпців (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Якщо рядок таблиці [RV] має значення (JOUR1, ID_CRENEAU1) для стовпців (JOUR, ID_CRENEAU), це значення не може зустрічатися більше ніде. Інакше це означало б, що одночасно було зареєстровано два записи RV для одного й того самого лікаря. З точки зору програмування на Java драйвер JDBC бази даних запускає SQLException, коли трапляється такий випадок.

Рядок id, що дорівнює 3 (див. [1] вище), означає, що 23.08.2006 було заброньовано RV для слоту № 20 та клієнта № 4. З таблиці [CRENEAUX] випливає, що слот № 20 відповідає часовому проміжку 16:20–16:40 і належить лікарю № 1 (пані Марі PELISSIER). З таблиці [CLIENTS] випливає, що клієнт № 4 — це пані Бріжит BISTROU.

4.4.5. Створення бази даних

Для створення таблиць та їх заповнення можна використати скрипт [dbrdvmedecins2.sql]. За допомогою [WampServer] можна діяти наступним чином:

  • у [1] натисніть на піктограму [WampServer] і виберіть опцію [PhpMyAdmin] [2],
  • у [3] у вікні, що відкрилося, виберіть посилання [Bases de données],
  • на [2], створюємо базу даних, якій дали ім’я [4] та кодування [5],
  • у [7] база даних створена. Клацаємо на її посилання,
  • у [8] імпортуємо файл SQL,
  • який вибираємо у файловій системі за допомогою кнопки [9],
  • у [11] вибираємо скрипт SQL, а в [12] запускаємо його,
  • у [13] було створено чотири таблиці бази даних. Переходимо за одним із посилань,
  • у [14] — вміст таблиці.

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

4.4.6. Рівень [JPA]

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

Тепер ми створюємо проект Maven для шару [JPA].

4.4.7. Проєкт NetBeans

Виглядає так:

  • у [1] створюємо проект Maven типу [Java Application] [2],
  • у [3] надаємо проекту ім’я,
  • у [4] — згенерований проєкт.

4.4.8. Створення шару [JPA]

Повернемося до архітектури, яку ми маємо побудувати:

За допомогою NetBeans можна автоматично згенерувати шар [JPA]. Ці методи автоматичного генерування варто знати, оскільки згенерований код дає цінні підказки щодо написання сутностей JPA.

4.4.9. Створення з’єднання NetBeans із базою даних

  • запустіть SGBD MySQL 5, щоб BD став доступним,
  • створіть з’єднання NetBeans із базою даних [dbrdvmedecins2],
  • у вкладці [Services] [1], у гілці [Databases] [2] вибрати драйвер JDBC MySQL [3],
  • потім вибрати опцію [4] «Connect Using», що дозволяє встановити з’єднання з базою даних MySQL,
  • у [5] введіть необхідну інформацію. У [6] — назву бази даних, у [7] — ім’я користувача бази даних та його пароль,
  • у [8] можна перевірити введені дані,
  • у [9] — очікуване повідомлення, якщо дані правильні,
  • у [10] — з’єднання встановлено. Тут відображаються чотири таблиці підключеної бази даних.

4.4.10. Створення модуля збереження даних

Повернемося до архітектури, що зараз будується:

Ми зараз будуємо рівень [JPA]. Його конфігурація здійснюється у файлі [persistence.xml], у якому визначаються одиниці збереження даних. Кожна з них потребує такої інформації:

  • параметри JDBC доступу до бази даних (URL, ім’я користувача, пароль),
  • класи, які будуть відображати таблиці бази даних,
  • використовувану реалізацію JPA. Справді, JPA — це специфікація, реалізована різними продуктами. У цьому випадку ми використовуватимемо Hibernate.

NetBeans може згенерувати цей файл персистентності за допомогою майстра.

  • клацніть правою кнопкою миші на проекті та виберіть створення одиниці персистентності [1],
  • у [2] створіть модуль збереження даних,
  • у [3] надайте ім’я створюваному модулю збереження даних,
  • в [4] вибрати реалізацію Hibernate JPA (JPA 2.0),
  • у [5] вказати, що таблиці з BD вже створені, а тому їх не створювати. Підтверджуємо дії майстра,
  • у [6] — новий проєкт,
  • у [7] файл [persistence.xml] було згенеровано у папці [META-INF],
  • у [8] до проекту Maven додано нові залежності.

Створений файл [META-INF/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-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>

Він містить інформацію, вказану в майстрі:

  • рядок 3: назва модуля збереження даних,
  • рядок 3: тип транзакцій із базою даних. У даному випадку RESOURCE_LOCAL вказує, що додаток самостійно керуватиме своїми транзакціями,
  • рядки 6–9: властивості JDBC джерела даних.

На вкладці [Design] можна отримати загальний огляд файлу [persistence.xml]:

Щоб отримати журнали Hibernate, доповнюємо файл [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-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>      
    </properties>
  </persistence-unit>
</persistence>
  • рядок 11: запитується перегляд команд SQL, виданих Hibernate,
  • рядок 12: ця властивість дозволяє отримати відформатоване відображення цих наказів.

До проекту додано залежності. Файл [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-rdvmedecins-jpql-hibernate</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-rdvmedecins-jpql-hibernate</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.logging</groupId>
      <artifactId>jboss-logging</artifactId>
      <version>3.1.0.GA</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.spec.javax.transaction</groupId>
      <artifactId>jboss-transaction-api_1.1_spec</artifactId>
      <version>1.0.0.Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.2</version>
    </dependency>
    <dependency>
      <groupId>antlr</groupId>
      <artifactId>antlr</artifactId>
      <version>2.7.7</version>
    </dependency>
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.6.1</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate.javax.persistence</groupId>
      <artifactId>hibernate-jpa-2.0-api</artifactId>
      <version>1.0.1.Final</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.15.0-GA</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate.common</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>4.0.1.Final</version>
    </dependency>
  </dependencies>
</project>

Усі додані залежності стосуються Hibernate ORM. Додамо залежність драйвера JDBC від MySQL:


    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>        

4.4.11. Створення об'єктів JPA

Елементи JPA можна створити за допомогою майстра NetBeans:

  • у [1] створюються сутності JPA на основі бази даних,
  • в [2] вибирається раніше створене з'єднання [dbrdvmedecins2],
  • у [3] вибираються всі таблиці відповідної бази даних,
  • у [4] присвоюємо імена класам Java, пов’язаним із цими чотирма таблицями,
  • а також ім’я пакета [5],
  • у [6], JPA об’єднує рядки таблиць з BD у колекції. Ми обираємо список як колекцію,
  • у [7] — класи Java, створені майстром.

4.4.12. Згенеровані сутності JPA

Ентітет [Medecin] є відображенням таблиці [medecins]. Клас Java переповнений анотаціями, що на перший погляд ускладнюють читання коду. Якщо залишити лише те, що є необхідним для розуміння ролі ентітету, отримаємо такий код:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
  private List<Creneau> creneauList;

// конструктори
....

   // гетери та сеттери
....

  @Override
  public int hashCode() {
  ...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • у рядку 4 анотація @Entity робить клас [Medecin] сутністю JPA, c.a.d. клас, пов’язаний із таблицею BD через API та JPA,
  • у рядку 5 вказано назву таблиці BD, пов’язаної з сутністю JPA. Кожне поле таблиці відповідає полю в класі Java,
  • рядок 6: клас реалізує інтерфейс Serializable. Це необхідно в клієнт-серверних додатках, де сутності серіалізуються між клієнтом і сервером.
  • рядки 10–11: поле id класу [Medecin] відповідає полю [ID] (рядок 10) таблиці [medecins],
  • рядки 13–14: поле «title» класу [Medecin] відповідає полю [TITRE] (рядок 13) таблиці [medecins],
  • рядки 16–17: поле «name» класу [Medecin] відповідає полю [NOM] (рядок 16) таблиці [medecins],
  • рядки 19–20: поле «версія» класу [Medecin] відповідає полю [VERSION] (рядок 19) таблиці [medecins]. У цьому випадку майстер не розпізнає, що цей стовпець насправді є стовпцем версії, значення якого має збільшуватися при кожній зміні рядка, до якого він належить. Щоб надати йому цю роль, потрібно додати анотацію @Version. Ми зробимо це на наступному кроці,
  • рядки 22–23: поле prenom класу [Medecin] відповідає полю [PRENOM] таблиці [medecins],
  • рядки 10–11: поле id відповідає первинному ключу [ID] таблиці. Анотації в рядках 8–9 уточнюють цей момент,
  • рядок 8: анотація @Id вказує, що анотоване поле пов’язане з первинним ключем таблиці,
  • рядок 9: шар [JPA] згенерує первинний ключ для рядків, які він вставит у таблицю [Medecins]. Існує кілька можливих стратегій. У даному випадку стратегія GenerationType.IDENTITY вказує, що шар JPA використовуватиме режим auto_increment таблиці MySQL,
  • рядки 25–26: таблиця [creneaux] має зовнішній ключ до таблиці [medecins]. Один час прийому належить одному лікарю. І навпаки, один лікар має кілька часів прийому, пов’язаних із ним. Отже, ми маємо відношення «один (лікар) до декількох (часових слотів)», яке кваліфікується анотацією @OneToMany через JPA (рядок 25). Поле рядка 26 міститиме всі часові слоти лікаря. Це відбувається без програмування. Щоб повністю зрозуміти рядок 25, нам потрібно представити клас [Creneau].

Він має такий вигляд:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "MDEBUT")
  private int mdebut;

  @Column(name = "HFIN")
  private int hfin;

  @Column(name = "HDEBUT")
  private int hdebut;

  @Column(name = "MFIN")
  private int mfin;

  @Column(name = "VERSION")
  private int version;

  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin idMedecin;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> rvList;

// конструктори
...
// методи getter та setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

Ми коментуємо лише нові примітки:

  • ми вже зазначали, що таблиця [creneaux] має зовнішній ключ до таблиці [medecins]: один час прийому пов’язаний з одним лікарем. З одним лікарем може бути пов’язано кілька часів прийому. Існує зв’язок із таблиці [creneaux] до таблиці [medecins], який кваліфікується як «багато (часових проміжків) до одного (лікаря)». Саме анотація @ManyToOne у рядку 32 використовується для визначення чужого ключа,
  • у рядку 31 з приміткою @JoinColumn вказано зв’язок із зовнішнім ключем: стовпець [ID_MEDECIN] таблиці [creneaux] є зовнішнім ключем для стовпця [ID] таблиці [medecins],
  • рядок 33: посилання на лікаря, якому належить часовий проміжок. Його також можна отримати без програмування.

Отже, зв’язок зовнішнього ключа між сутністю [Creneau] та сутністю [Medecin] реалізується двома анотаціями:

  • в об’єкті [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • в об’єкті [Medecin]:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;

Обидві анотації відображають один і той самий зв’язок: зв’язок зовнішнього ключа таблиці [creneaux] із таблицею [medecins]. Кажуть, що вони є взаємно оберненими. Необхідним є лише зв’язок @ManyToOne. Він однозначно визначає зв’язок як зв’язок із зовнішнім ключем. Зв’язок @OneToMany є необов’язковим. Якщо він присутній, то лише посилається на зв’язок @ManyToOne, з яким він пов’язаний. Саме це означає атрибут mappedBy у рядку 1 сутності [Medecin]. Значенням цього атрибута є назва поля сутності [Creneau], яке має анотацію @ManyToOne, що визначає зовнішній ключ. Також у цьому ж рядку 1 сутності [Medecin] атрибут cascade=CascadeType.ALL визначає поведінку сутності [Medecin] щодо сутності [Creneau]:

  • якщо в базу даних вставляється новий об’єкт [Medecin], то об’єкти [Creneau] з поля у рядку 2 також мають бути вставлені,
  • якщо в базі даних змінюється об’єкт [Medecin], то об’єкти [Creneau] у полі рядка 2 також мають бути змінені,
  • якщо з бази видалити запис [Medecin], то записи [Creneau] у полі рядка 2 також мають бути видалені.

Код двох інших об’єктів наводимо без особливих коментарів, оскільки вони не містять нових позначень.

Елемент [Client]


package rdvmedecins.jpa;

...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
  private List<Rv> rvList;

// конструктори
...
// методи getter та setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • рядки 24–25 відображають зв’язок із зовнішнім ключем між таблицею [rv] та таблицею [clients].

Об’єкт [Rv]:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;

  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau idCreneau;

  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client idClient;

   // конструктори
...

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

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • у рядку 13 вказано тип поля «день» у таблиці Java Date. Зазначено, що в таблиці [rv] стовпець [JOUR] (рядок 12) має тип «дата» (без часу),
  • рядки 16–18: визначають зв’язок із зовнішнім ключем, який таблиця [rv] має з таблицею [creneaux],
  • рядки 20–22: визначають зв’язок із зовнішнім ключем, який має таблиця [rv] до таблиці [clients].

Автоматичне створення сутностей JPA дозволяє нам отримати робочу базу даних. Іноді цього достатньо, іноді — ні. Саме такий випадок маємо тут:

  • необхідно додати анотацію @Version до різних полів version сутностей,
  • необхідно написати методи toString, які будуть більш зрозумілими, ніж згенеровані,
  • об’єкти [Medecin] та [Client] є аналогічними. Ми зробимо їх похідними від класу [Personne],
  • видалимо зворотні зв’язки @OneToMany, що є зворотними до зв’язків @ManyToOne. Вони не є обов’язковими та ускладнюють програмування,
  • видаляємо перевірку @NotNull для первинних ключів. Коли ми зберігаємо суть JPA разом із MySQL, початкова суть має первинний ключ null. Лише після збереження в базі даних первинний ключ збереженого елемента набуває значення.

З урахуванням цих специфікацій класи мають такий вигляд:

Клас «Personne» використовується для представлення лікарів та клієнтів:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Basic(optional = false)
  @Column(name = "TITRE")
  private String titre;

  @Basic(optional = false)
  @Column(name = "NOM")
  private String nom;

  @Basic(optional = false)
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @Column(name = "PRENOM")
  private String prenom;
// конструктори
...

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

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • рядок 6: зверніть увагу, що клас [Personne] сам по собі не є сутністю (@Entity). Він буде батьківським класом для сутностей. Анотація @MappedSuperClass позначає цю ситуацію.

Ентітет [Client] інкапсулює рядки таблиці [clients]. Він походить від попереднього класу [Personne]:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

// конструктори
...

  @Override
  public int hashCode() {
...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • рядок 6: клас [Client] є сутністю JPA,
  • рядок 7: він пов’язаний із таблицею [clients],
  • рядок 8: вона походить від класу [Personne].

Ентітет [Medecin], який інкапсулює рядки таблиці [medecins], побудований за тим самим зразком:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

  // конструктори
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}

Об’єкт [Creneau] інкапсулює рядки таблиці [creneaux]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @Column(name = "MDEBUT")
  private int mdebut;
  
  @Basic(optional = false)
  @Column(name = "HFIN")
  private int hfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HDEBUT")
  private int hdebut;
  
  @Basic(optional = false)
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin medecin;

  // конструктори
  ...

  // методи getter та setter
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Попередження — цей метод не працюватиме, якщо поля id не задані
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • рядки 40–42 моделюють відношення «багато до одного», яке існує між таблицею [creneaux] та таблицею [medecins] у базі даних: один лікар має кілька часових слотів, а один часовий слот належить лише одному лікарю.

Ентітет [Rv] інкапсулює рядки таблиці [rv]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;

@Entity
@Table(name = "rv")
public class Rv implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau creneau;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;

   // конструктори
...

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

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • рядки 27–29 моделюють відношення «багато до одного», що існує між таблицею [rv] та таблицею [clients] (один клієнт може фігурувати в декількох Rv) бази даних, а рядки 23–25 — відношення «багато до одного», яке існує між таблицею [rv] та таблицею [creneaux] (один часовий проміжок може фігурувати в декількох Rv).

4.4.13. Код доступу до даних

Тепер ми додамо до проєкту код доступу до даних через шар JPA:

Клас [MainJpql] має такий вигляд:


package rdvmedecins.console;

import java.util.Scanner;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class MainJpql {

  public static void main(String[] args) {
    // EntityManagerFactory
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-rdvmedecins-jpql-hibernatePU");
    // entityManager
    EntityManager em = emf.createEntityManager();
    // сканер клавіатури
    Scanner clavier = new Scanner(System.in);
    // цикл введення запитів JPQL
    System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
    String requete = clavier.nextLine();
    while (!requete.trim().equals("*")) {
      try {
        // відображення результату запиту
        for (Object o : em.createQuery(requete).getResultList()) {
          System.out.println(o);
        }
      } catch (Exception e) {
        System.out.println("L'exception suivante s'est produite : " + e);
      }
      // очищення контексту збереження даних
      em.clear();
      // новий запит
      System.out.println("---------------------------------------------");
      System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
      requete = clavier.nextLine();
    }
    // закриття ресурсів
    em.close();
    emf.close();
  }
}
  • рядок 12: створення EntityManagerFactory, пов'язаного з одиницею збереження, яку ми створили раніше. Параметром методу createEntityManagerFactory є ім'я цієї одиниці збереження:

  <persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
</persistence-unit>
  • рядок 14: створення EntityManager, що керує рівнем збереження даних,
  • рядок 19: введення запиту JPQL select,
  • рядки 23–28: відображення результату запиту,
  • рядок 20: введення припиняється, коли користувач вводить *.

Запитання: наведіть запити JPQL, що дозволяють отримати таку інформацію:


  • список лікарів у порядку спадання їхніх прізвищ
  • список лікарів, у яких title='Mr'
  • список часових проміжків пані Пеліссьє
  • список призначених зустрічей у порядку зростання днів
  • список клієнтів (прізвища), які записалися на прийом до пані PELISSIER 24.08.2006
  • кількість клієнтів пані PELISSIER 24.08.2006
  • клієнти, які не записалися на прийом
  • лікарі, у яких немає призначених зустрічей

Слід орієнтуватися на приклад із параграфа 2.7 [ref1]. Ось приклад виконання:

Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :
select c from Client c
Hibernate: 
    select
        client0_.ID as ID2_,
        client0_.NOM as NOM2_,
        client0_.PRENOM as PRENOM2_,
        client0_.TITRE as TITRE2_,
        client0_.version as version2_ 
    from
        clients client0_
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
  • рядок 2: запит JPQL,
  • рядки 3–11: відповідний запит SQL,
  • рядки 12–15: результат запиту JPQL.

4.5. Зв’язки між контекстом збереження даних та SGBD

4.5.1. Клас «Особа»

package entites;

...

@Entity
@Table(name = "jpa01_personne")
public class Personne {

  @Id
  @Column(name = "ID", nullable = false)
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  @Column(name = "VERSION", nullable = false)
  @Version
  private int version;

  @Column(name = "NOM", length = 30, nullable = false, unique = true)
  private String nom;

  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

  @Column(name = "DATENAISSANCE", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date datenaissance;

  @Column(name = "MARIE", nullable = false)
  private boolean marie;

  @Column(name = "NBENFANTS", nullable = false)
  private int nbenfants;

   // конструктори

  public Personne() {
  }

  public Personne(String nom, String prenom, Date datenaissance, boolean marie, int nbenfants) {
    setNom(nom);
    setPrenom(prenom);
    setDatenaissance(datenaissance);
    setMarie(marie);
    setNbenfants(nbenfants);
  }

   // toString

  public String toString() {
    return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(), getNom(), getPrenom(), 
                         new SimpleDateFormat("dd/MM/yyyy").format(getDatenaissance()), isMarie(), getNbenfants());
  }

   // гетери та сеттери
...
}

4.5.2. Тестова програма

package tests;

....
import entites.Personne;

@SuppressWarnings("unchecked")
public class Test1 {

   // константи
  private final static String TABLE_NAME = "jpa01_personne";  // Контекст збереження даних
  private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  private static Personne p1;

  public static void main(String[] args) throws Exception {
     // очищення бази даних
    log("clean");
    clean();

     // зливання даних
    log("dump");
    dump();

     // тест1
    log("test1");
    test1();

     // тест2
    log("test2");
    test2();

     // закриття EntityManagerFactory
    emf.close();
  }

   // відображення вмісту таблиці
  private static void dump() {
     // контекст збереження
    EntityManager em = emf.createEntityManager();
     // початок транзакції
    EntityTransaction tx = em.getTransaction();
    tx.begin();
     // відображення осіб
    for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
      System.out.println(p);
    }
     // завершення транзакції
    tx.commit();
     // завершення контексту
    em.close();
  }

   // очищення BD
  private static void clean() {
     // контекст збереження
    EntityManager em = emf.createEntityManager();
     // початок транзакції
    EntityTransaction tx = em.getTransaction();
    tx.begin();
     // видалити елементи з таблиці PERSONNES
    em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
     // завершення транзакції
    tx.commit();
     // завершення контексту
    em.close();
  }

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

   // управління об’єктами, що зберігаються
  public static void test1() throws ParseException {
     // контекст збереження
    EntityManager em = emf.createEntityManager();
     // створення осіб
    p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
    Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
     // початок транзакції
    EntityTransaction tx = em.getTransaction();
    System.out.println("début transaction");
    tx.begin();
     // збереження осіб
     // журнали показують, що операція SQL INSERT генерується одразу після операції збереження
     // ймовірно, для отримання первинного ключа
    System.out.println(String.format("Personne p1 %s non persistée", p1));
    System.out.println("em.persist(p1)");
    em.persist(p1);
    System.out.println(String.format("Personne p1 %s persistée", p1));
     // особа p2
     // INSERT генерується одразу після операції persist
    System.out.println(String.format("Personne p2 %s non persistée", p2));
    System.out.println("em.persist(p2)");
    em.persist(p2);
    System.out.println(String.format("Personne p2 %s persistée", p2));
    p2.setMarie(true);
    System.out.println(String.format("Personne p2 %s modifiée", p2));
     // операція DELETE, пов’язана з операцією видалення, виконується лише наприкінці транзакції
    System.out.println("em.remove(p2)");
    em.remove(p2);
    System.out.println(String.format("Personne p2 %s supprimée", p2));
     // зміна p1
    p1.setNom("P1");
     // завершення транзакції
    System.out.println("fin transaction");
    tx.commit();
     // кінець контексту
    em.close();
     // таблиця відображається
    dump();
  }

   // управління збереженими об’єктами
  public static void test2() throws ParseException {
     // контекст збереження
    EntityManager em = emf.createEntityManager();
     // початок транзакції
    EntityTransaction tx = em.getTransaction();
    System.out.println("début transaction");
    tx.begin();
     // змінюємо особу p1, яка наразі від'єднана
    System.out.println(String.format("Personne p1 %s actuelle non persistée", p1));
    p1.setMarie(false);
    System.out.println(String.format("Personne p1 %s nouvelle non persistée", p1));
     // особу знову прив’язують P1
    System.out.println("em.merge(p1)");
    Personne p1b = em.merge(p1);
    System.out.println(String.format("Personne p1b %s attachée", p1b));
     // завершення транзакції
    System.out.println("fin transaction");
    tx.commit();
       // кінець контексту
    em.close();
   // відображається таблиця
    dump();
  }
}

4.5.3. Конфігурація Hibernate

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
  <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
         <!- - провайдер -->
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
             <!-- Постійні класи -->
      <property name="hibernate.archive.autodetection" value="class, hbm" />
      <property name="hibernate.show_sql" value="true"/>
....
             <! -- автоматичне створення схеми -->
      <property name="hibernate.hbm2ddl.auto" value="create" />
....
    </properties>
  </persistence-unit>
</persistence>

4.5.4. Налаштування log4j.properties

# Направляти повідомлення журналу безпосередньо у stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

# Параметр кореневого логера
log4j.rootLogger=ERROR, stdout

# Параметри журналювання Hibernate (INFO відображає лише повідомлення про запуск)
#log4j.logger.org.hibernate=DEBUG

# Реєструвати аргументи виконання зв'язувальних параметрів JDBC
log4j.logger.org.hibernate.type=DEBUG

4.5.5. Результати

init:
deps-jar:
Compiling 1 source file to C:\data\travail\2008-2009\netbeans\jpa\hibernate-personnes-entites\build\classes
compile-single:
run-single:
main : ----------- очищення
Hibernate: delete from jpa01_personne
main : ----------- злиття
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
main : ----------- test1
début transaction
Personne p1 [null,0,Martin,Paul,31/01/2000,true,2] non persistée
em.persist(p1)
Hibernate: insert into jpa01_personne (DATENAISSANCE, MARIE, NBENFANTS, NOM, PRENOM, VERSION) values (?, ?, ?, ?, ?, ?)
17:57:26,312 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,312 DEBUG BooleanType:133 - binding 'true' to parameter: 2
17:57:26,312 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,312 DEBUG StringType:133 - binding 'Martin' to parameter: 4
17:57:26,312 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,312 DEBUG IntegerType:133 - binding '0' to parameter: 6
Personne p1 [1,0,Martin,Paul,31/01/2000,true,2] persistée
Personne p2 [null,0,Durant,Sylvie,05/07/2001,false,0] non persistée
em.persist(p2)
Hibernate: insert into jpa01_personne (DATENAISSANCE, MARIE, NBENFANTS, NOM, PRENOM, VERSION) values (?, ?, ?, ?, ?, ?)
17:57:26,328 DEBUG DateType:133 - binding '05 juillet 2001' to parameter: 1
17:57:26,328 DEBUG BooleanType:133 - binding 'false' to parameter: 2
17:57:26,328 DEBUG IntegerType:133 - binding '0' to parameter: 3
17:57:26,328 DEBUG StringType:133 - binding 'Durant' to parameter: 4
17:57:26,328 DEBUG StringType:133 - binding 'Sylvie' to parameter: 5
17:57:26,328 DEBUG IntegerType:133 - binding '0' to parameter: 6
Personne p2 [2,0,Durant,Sylvie,05/07/2001,false,0] persistée
Personne p2 [2,0,Durant,Sylvie,05/07/2001,true,0] modifiée
em.remove(p2)
Personne p2 [2,0,Durant,Sylvie,05/07/2001,true,0] supprimée
fin transaction
Hibernate: update jpa01_personne set DATENAISSANCE=?, MARIE=?, NBENFANTS=?, NOM=?, PRENOM=?, VERSION=? where ID=? and VERSION=?
17:57:26,343 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,343 DEBUG BooleanType:133 - binding 'true' to parameter: 2
17:57:26,343 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,343 DEBUG StringType:133 - binding 'P1' to parameter: 4
17:57:26,359 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,359 DEBUG IntegerType:133 - binding '1' to parameter: 6
17:57:26,359 DEBUG IntegerType:133 - binding '1' to parameter: 7
17:57:26,359 DEBUG IntegerType:133 - binding '0' to parameter: 8
Hibernate: delete from jpa01_personne where ID=? and VERSION=?
17:57:26,359 DEBUG IntegerType:133 - binding '2' to parameter: 1
17:57:26,359 DEBUG IntegerType:133 - binding '0' to parameter: 2
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
17:57:26,375 DEBUG IntegerType:172 - returning '1' as column: ID0_
17:57:26,390 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_
17:57:26,390 DEBUG BooleanType:172 - returning 'true' as column: MARIE0_
17:57:26,390 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_
17:57:26,390 DEBUG StringType:172 - returning 'P1' as column: NOM0_
17:57:26,390 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_
17:57:26,390 DEBUG IntegerType:172 - returning '1' as column: VERSION0_
[1,1,P1,Paul,31/01/2000,true,2]
main : ----------- тест2
début transaction
Personne p1 [1,1,P1,Paul,31/01/2000,true,2] actuelle non persistée
Personne p1 [1,1,P1,Paul,31/01/2000,false,2] nouvelle non persistée
em.merge(p1)
Hibernate: select personne0_.ID as ID0_0_, personne0_.DATENAISSANCE as DATENAIS2_0_0_, personne0_.MARIE as MARIE0_0_, personne0_.NBENFANTS as NBENFANTS0_0_, personne0_.NOM as NOM0_0_, personne0_.PRENOM as PRENOM0_0_, personne0_.VERSION as VERSION0_0_ from jpa01_personne personne0_ where personne0_.ID=?
17:57:26,406 DEBUG IntegerType:133 - binding '1' to parameter: 1
17:57:26,406 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_0_
17:57:26,406 DEBUG BooleanType:172 - returning 'true' as column: MARIE0_0_
17:57:26,406 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_0_
17:57:26,406 DEBUG StringType:172 - returning 'P1' as column: NOM0_0_
17:57:26,406 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_0_
17:57:26,406 DEBUG IntegerType:172 - returning '1' as column: VERSION0_0_
Personne p1b [1,1,P1,Paul,31/01/2000,false,2] attachée
fin transaction
Hibernate: update jpa01_personne set DATENAISSANCE=?, MARIE=?, NBENFANTS=?, NOM=?, PRENOM=?, VERSION=? where ID=? and VERSION=?
17:57:26,406 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,406 DEBUG BooleanType:133 - binding 'false' to parameter: 2
17:57:26,406 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,421 DEBUG StringType:133 - binding 'P1' to parameter: 4
17:57:26,421 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,421 DEBUG IntegerType:133 - binding '2' to parameter: 6
17:57:26,421 DEBUG IntegerType:133 - binding '1' to parameter: 7
17:57:26,421 DEBUG IntegerType:133 - binding '1' to parameter: 8
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
17:57:26,453 DEBUG IntegerType:172 - returning '1' as column: ID0_
17:57:26,453 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_
17:57:26,453 DEBUG BooleanType:172 - returning 'false' as column: MARIE0_
17:57:26,453 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_
17:57:26,453 DEBUG StringType:172 - returning 'P1' as column: NOM0_
17:57:26,453 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_
17:57:26,453 DEBUG IntegerType:172 - returning '2' as column: VERSION0_
[1,2,P1,Paul,31/01/2000,false,2]
BUILD SUCCESSFUL (total time: 3 seconds)

Запитання: встановіть зв’язок між кодом Java та відображеними результатами.