2. Об'єкти JPA
2.1. Приклад 1 — Об'єктне представлення однієї таблиці
2.1.1. Таблиця [personne]
Розглянемо базу даних, що містить єдину таблицю [personne], призначенням якої є зберігання певної інформації про осіб:
![]() |
первинний ключ таблиці | |
версія рядка в таблиці. Кожного разу, коли особа змінюється, її номер версії збільшується. | |
ім’я особи | |
її ім’я | |
дата народження | |
ціле число 0 (неодружений) або 1 (одружений) | |
кількість дітей у цієї особи |
2.1.2. Елемент [Personne]
Ми знаходимося в такому середовищі виконання:
![]() |
Рівень JPA [5] повинен слугувати мостом між реляційним світом бази даних [7] та об’єктним світом [4], з яким працюють програми Java [3]. Цей міст створюється шляхом конфігурації, і для цього існує два способи:
- за допомогою файлів XML. Це був практично єдиний спосіб до появи JDK 1.5
- за допомогою 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() {
...
}
// методи getter та setter
...
}
Налаштування здійснюється за допомогою анотацій Java @Annotation. Анотації Java обробляються або компілятором, або спеціалізованими інструментами під час виконання. За винятком анотації в рядку 3, призначеної для компілятора, усі інші анотації тут призначені для використовуваної реалізації JPA — Hibernate або Toplink. Отже, вони будуть оброблятися під час виконання. За відсутності інструментів, здатних їх інтерпретувати, ці анотації ігноруються. Таким чином, наведений вище клас [Personne] може використовуватися в контексті поза JPA.
Слід розрізняти два випадки використання анотацій JPA у класі C, пов’язаному з таблицею T:
- таблиця T вже існує: у цьому випадку анотації JPA повинні відтворювати існуючі дані (назви та визначення стовпців, обмеження цілісності, зовнішні ключі, первинні ключі тощо)
- таблиця 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 використовують однакову техніку генерації значень первинного ключа. Наприклад:
використовує генератор значень, який викликається перед кожним вставленням | |
поле первинного ключа визначено як тип Identity. Результат схожий на генератор значень Firebird, за винятком того, що значення ключа стає відомим лише після вставки рядка. | |
використовує об’єкт із назвою 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.
2.1.3. Проєкт Eclipse для тестування
Ми проведемо наші перші експерименти з попередньою сутністю [Personne]. Ми будемо працювати з такою архітектурою:
![]() |
- в [7]: база даних, яка буде згенерована на основі анотацій сутності [Personne], а також додаткових налаштувань, зроблених у файлі з назвою [persistence.xml]
- в [5, 6]: шар JPA, реалізований за допомогою Hibernate
- у [4]: суть [Personne]
- у [3]: тестова програма консольного типу
Ми проведемо різні експерименти:
- згенеруємо схему BD за допомогою скрипта Ant та інструменту Hibernate Tools
- згенеруємо BD та ініціалізуємо його деякими даними
- використання BD та виконання чотирьох базових операцій над таблицею [personne] (вставка, оновлення, видалення, запит)
Необхідні інструменти:
- Eclipse та його плагіни, описані в розділі 5.2.
- проект [hibernate-personnes-entites], який можна знайти в папці <exemples>/hibernate/direct/personnes-entites
- різні файли SGBD, описані в додатках (розділ 5 і далі).
Проєкт Eclipse має таку назву:
![]() |
- у [1]: папка проекту Eclipse
- у [2]: проект, імпортований в Eclipse (File / Import)
- у [3]: об’єкт [Personne], що підлягає тестуванню
- у [4]: тестові програми
- en [5]: [persistence.xml] — це файл конфігурації шару JPA
- у [6]: використовувані бібліотеки. Вони описані в розділі 1.5.
- у [8]: скрипт ant, який буде використовуватися для генерації таблиці, пов’язаної з об’єктом [Personne]
- en [9]: файли [persistence.xml] для кожного з використовуваних SGBD
- у [10]: схеми згенерованої бази даних для кожного з використаних SGBD
Ми опишемо ці елементи по черзі.
2.1.4. Елемент [Personne] (2)
Ми вносимо незначну зміну до наведеного раніше опису сутності [Personne], а також додаємо додаткову інформацію:
package entites;
...
@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_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) {
....
}
// 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());
}
// методи отримання та встановлення
...
}
- рядок 7: ми присвоюємо таблиці, пов’язаній з сутністю [Personne], ім’я [jpa01_personne]. У цьому документі будуть створені різні таблиці у схемі, яка завжди називатиметься jpa. Наприкінці цього посібника схема jpa міститиме багато таблиць. Щоб читачеві було легше орієнтуватися, пов’язані між собою таблиці матимуть однаковий префікс jpaxx_.
- рядок 45: метод [toString] для виведення об’єкта [Personne] на консоль.
2.1.5. Налаштування рівня доступу до даних
У наведеному вище проєкті Eclipse налаштування шару JPA забезпечується файлом [META-INF/persistence.xml]:
![]() |
Під час виконання файл [META-INF/persistence.xml] шукається у папці classpath додатка. У нашому проєкті Eclipse все, що міститься в папці [/src] [1], копіюється в папку [/bin] [2]. Ця папка є частиною classpath проекту. Саме тому [META-INF/persistence.xml] буде знайдено під час налаштування шару JPA.
За замовчуванням Eclipse не розміщує вихідні коди в папці [/src] проекту, а безпосередньо в самій папці. Усі наші проекти Eclipse будуть налаштовані таким чином, щоб вихідні коди знаходилися в [/src], а скомпільовані класи — в [/bin], як показано в розділі 5.2.1.
Розглянемо конфігурацію шару 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 на SGBD. Це дуже корисно на етапі навчання. Через реляційно-об’єктний міст додаток працює з об’єктами, що зберігаються у базі даних, до яких він застосовує операції типу [persist, merge, remove]. Дуже цікаво дізнатися, які саме команди SQL насправді генеруються під час цих операцій. Вивчаючи їх, поступово можна вгадати команди SQL, які Hibernate згенерує під час виконання певної операції над об’єктами, що зберігаються, і реляційно-об’єктний міст починає набувати чітких обрисів у свідомості.
- рядок 11: команди SQL, що відображаються на консолі, можна гарно відформатувати, щоб полегшити їхнє читання
- рядок 12: команди SQL, що виводяться, також будуть супроводжуватися коментарями
- рядки 15–19 визначають шар JDBC (шар [6] в архітектурі):
- рядок 15: клас драйвера JDBC для SGBD, тут — MySQL5
- рядок 16: URL-адреса використовуваної бази даних
- рядки 17, 18: ім’я користувача для підключення та його пароль
- Тут ми використовуємо елементи, пояснені в додатках до розділу 5.5. Читачеві рекомендується ознайомитися з цим розділом на прикладі MySQL5.
- рядок 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), якщо вони раніше існували. Слід зауважити, що, звісно, це не слід робити з базою даних у виробничому середовищі...
Тестування показало, що фаза видалення/створення таблиць може завершитися невдачею. Зокрема, це траплялося, коли в рамках одного й того самого тесту відбувався перехід з шару JPA/Hibernate на шар JPA/Toplink або навпаки. На основі одних і тих самих об’єктів @Entity обидві реалізації не генерують абсолютно однакові таблиці, генератори, послідовності тощо, і іноді траплялося, що фаза видалення/створення завершувалася невдало, і доводилося видаляти таблиці вручну. У розділі «Додатки», параграф 5 і далі, описано програми, які можна використовувати для виконання цієї роботи вручну. Слід зазначити, що реалізація JPA/Hibernate виявилася найефективнішою на цьому етапі початкового створення вмісту бази даних: збій у роботі траплявся вкрай рідко.
Інструменти, що використовуються рівнем JPA / Hibernate, містяться в бібліотеці [jpa-hibernate], представленій у параграфі 1.5 на сторінці 8. Драйвери JDBC, необхідні для доступу до SGBD, знаходяться в бібліотеці [jpa-divers]. Ці дві бібліотеки були розміщені в classpath досліджуваного тут проєкту. Нижче наводимо їхній зміст:
![]() |
2.1.6. Створення бази даних за допомогою скрипта Ant
Як ми щойно побачили, Hibernate надає інструменти для створення бази даних-образу об’єктів @Entity додатка. Hibernate може:
- згенерувати текстовий файл команд SQL, що створюють базу даних. При цьому використовується лише діалект, визначений у [persistence.xml].
- створювати таблиці, що відображають об’єкти @Entity, у цільовій базі даних, визначеній у [persistence.xml]. У цьому випадку використовується весь файл [persistence.xml].
Ми представимо скрипт Ant, здатний згенерувати схему бази даних та зображення об’єктів @Entity. Цей скрипт не є моїм: він базується на аналогічному скрипті з [ref1]. Ant (Another Neat Tool) — це інструмент для пакетного виконання завдань на Java. Скрипти Ant нелегко зрозуміти новачкові. Ми будемо використовувати лише один із них — той, який зараз коментуємо:
![]() |
- у [1]: дерево прикладів цього підручника.
- у [2]: папка [personnes-entites] проекту Eclipse, який ми зараз вивчаємо
- в [3]: папка <lib>, що містить п’ять бібліотек JAR, визначених у параграфі 1.5.
- en [4]: архів [hibernate-tools.jar], необхідний для виконання одного із завдань скрипта [ant-hibernate.xml], який ми будемо вивчати.
![]() |
- у [5]: проект Eclipse та скрипт [ant-hibernate.xml]
- у [6]: папка [src] проекту
Скрипт [ant-hibernate.xml] [5] використовуватиме JAR-архіви з папки <lib> [3], зокрема архів [hibernate-tools.jar] [4] із папки [lib/hibernate]. Ми відтворили структуру папок, щоб читач побачив: щоб знайти папку [lib], починаючи з папки [personnes-entites] [2] скрипта [ant-hibernate.xml], потрібно пройти за таким шляхом: ../../../lib.
Розглянемо скрипт [ant-hibernate.xml]:
<project name="jpa-hibernate" default="compile" basedir=".">
<!-- назва проєкту та версія -->
<property name="proj.name" value="jpa-hibernate" />
<property name="proj.shortname" value="jpa-hibernate" />
<property name="version" value="1.0" />
<!-- загальні властивості -->
<property name="src.java.dir" value="src" />
<property name="lib.dir" value="../../../lib" />
<property name="build.dir" value="bin" />
<!-- Classpath проекту -->
<path id="project.classpath">
<fileset dir="${lib.dir}">
<include name="**/*.jar" />
</fileset>
</path>
<!-- конфігураційні файли, які мають бути у classpath-->
<patternset id="conf">
<include name="**/*.xml" />
<include name="**/*.properties" />
</patternset>
<!-- Очищення проекту -->
<target name="clean" description="Nettoyer le projet">
<delete dir="${build.dir}" />
<mkdir dir="${build.dir}" />
</target>
<!-- Компіляція проекту -->
<target name="compile" depends="clean">
<javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
</target>
<!-- Копіювання файлів конфігурації в classpath -->
<target name="copyconf">
<mkdir dir="${build.dir}" />
<copy todir="${build.dir}">
<fileset dir="${src.java.dir}">
<patternset refid="conf" />
</fileset>
</copy>
</target>
<!-- Інструменти Hibernate -->
<taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />
<!-- Генерувати DDL бази даних -->
<target name="DDL" depends="compile, copyconf" description="Génération DDL base">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- Використання META-INF/persistence.xml -->
<jpaconfiguration />
<!-- експорт -->
<hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
<!-- Створити базу даних -->
<target name="BD" depends="compile, copyconf" description="Génération BD">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- Використовувати META-INF/persistence.xml -->
<jpaconfiguration />
<!-- експорт -->
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
</project>
- рядок 1: проект [ant] має назву «jpa-hibernate». Він об’єднує набір завдань, одне з яких є завданням за замовчуванням: у даному випадку це завдання з назвою «compile». Для виконання завдання T викликається скрипт ant. Якщо завдання не вказано, виконується завдання за замовчуванням. parametr basedir="." вказує, що для всіх відносних шляхів, знайдених у скрипті, початковою точкою є папка, в якій знаходиться скрипт ant, у даному випадку папка <exemples>/hibernate/direct/personnes-entites.
- рядки 3–11: визначають змінні скрипта за допомогою тегу <property name="nomVariable" value="valeurVariable"/>. Згодом цю змінну можна використовувати у скрипті у формі ${nomVariable}. Імена можуть бути будь-якими. Зупинимося на змінних, визначених у рядках 9–11:
- рядок 9: визначає змінну з іменем «src.java.dir» (ім’я можна вибрати довільно), яка в подальшому тексті скрипта вказуватиме на папку, що містить вихідний код Java. Її значенням є «src» — відносний шлях до папки, вказаної атрибутом basedir (рядок 1). Отже, це шлях «./src», де «.» позначає папку <exemples>/hibernate/direct/personnes-entites. Саме в папці <personnes-entites>/src містяться вихідні коди Java (див. [6] вище).
- рядок 10: визначає змінну з іменем «lib.dir», яка в подальшому тексті скрипта вказуватиме на папку, що містить архіви jars, необхідні для виконання Java-завдань скрипта. Її значення ../../../lib вказує на папку <exemples>/lib (див. [3] вище).
- рядок 11: визначає змінну з іменем «build.dir», яка в подальшому тексті скрипта вказуватиме на папку, де мають бути згенеровані файли .class у результаті компіляції вихідних файлів .java. Її значення «bin» вказує на папку <personnes-entites>/bin. Ми вже пояснювали, що в розглянутому проєкті Eclipse папка <bin> була тією, де генерувалися файли .class. Ant буде робити те саме.
- рядки 14–18: тег <path> слугує для визначення елементів classpath, які повинні використовувати завдання ant. Тут шлях «project.classpath» (назва може бути довільною) об’єднує всі архіви .jar із дерева папок <exemples>/lib.
- рядки 21–24: тег <patternset> використовується для позначення набору файлів за допомогою шаблонів імен. Тут patternset з назвою «conf» позначає всі файли з розширенням .xml або .properties. Цей patternset слугуватиме для позначення файлів .xml та .properties у папці <src> (persistence.xml, log4j.properties) (див. [6]), які є конфігураційними файлами додатка. Під час виконання певних завдань ці файли потрібно скопіювати до папки <bin>, щоб вони опинилися в classpath проекту. Тоді для їх позначення буде використовуватися patternset conf.
- рядки 27–30: тег <target> позначає завдання скрипта. Це перше завдання, з яким ми стикаємося. Усе, що йшло перед цим, стосується налаштування середовища виконання скрипта ant. Завдання називається clean. Воно виконується у два етапи: папка <bin> видаляється (рядок 28), а потім відтворюється (рядок 29).
- рядки 33–35: завдання compile, яке є завданням за замовчуванням скрипта (рядок 1). Воно залежить (атрибут depends) від завдання clean. Це означає, що перед виконанням завдання «compile» скрипт ant повинен виконати завдання «clean» (c.a.d) для очищення папки <bin>. Мета завдання «compile» полягає в компіляції вихідного коду Java з папки <src>.
- рядок 34: виклик Java-компілятора з трьома параметрами:
- srcdir: папка, що містить вихідні Java-коди, у даному випадку папка <src>
- destdir: папка, куди мають бути збережені згенеровані файли .class, у даному випадку папка <bin>
- classpathref: шлях до класів, який слід використовувати для компіляції; у даному випадку — усі архівні файли jar у дереві папок <lib>
- (продовження)
- рядки 38–45: завдання copyconf, метою якого є копіювання у папку <bin> усіх файлів .xml та .properties з папки <src>.
- рядок 48: визначення завдання за допомогою тегу <taskdef>. Таке завдання призначене для повторного використання в інших місцях скрипта. Це спрощує процес кодування. Оскільки завдання використовується в різних місцях скрипта, його визначають один раз за допомогою тегу <taskdef>, а потім, за потреби, повторно використовують, посилаючись на його ім’я.
- Завдання має назву hibernatetool (атрибут name).
- її клас визначається атрибутом classname. У цьому випадку вказаний клас буде знайдено в архіві [hibernate-tools.jar], про який ми вже згадували.
- Атрибут classpathref вказує ant, де шукати попередній клас
- (продовження)
- рядки 51–60 стосуються завдання, яке нас тут цікавить, а саме генерації схеми бази даних-зображення об’єктів @Entity нашого проєкту Eclipse.
- рядок 51: завдання називається DDL (як Data Definition Language, SQL, пов’язане зі створенням об’єктів бази даних). Воно залежить від завдань compile та copyconf у такому порядку. Отже, завдання DDL спричинить, у зазначеному порядку, виконання завдань clean, compile та copyconf. Коли запускається завдання DDL, папка <bin> містить файли .class, згенеровані з вихідних файлів .java, зокрема об’єктів @Entity, а також файл [META-INF/persistence.xml], який налаштовує шар JPA / Hibernate.
- рядки 53–59: викликається завдання [hibernatetool], визначене в рядку 48. Йому передаються численні параметри, окрім тих, що вже визначені в рядку 48:
- рядок 53: папкою для виведення результатів, отриманих у результаті виконання завдання, буде поточна папка.
- рядок 54: папкою <bin> для завдання classpath буде папка <bin>
- рядок 56: вказує завданню [hibernatetool], як воно може дізнатися про своє середовище виконання: тег <jpaconfiguration/> вказує їй, що вона знаходиться в середовищі JPA і тому повинна використовувати файл [META-INF/persistence.xml], який вона знайде тут, у своєму classpath.
- у рядку 58 задаються умови створення бази даних: drop=true вказує, що перед створенням таблиць мають бути виконані команди SQL drop table; create=true вказує, що має бути створено текстовий файл з командами SQL для створення бази даних; outputfilename вказує ім’я цього файлу SQL — у даному випадку schema.sql — у папці <ddl> проекту Eclipse, а export=false вказує, що згенеровані команди SQL не повинні виконуватися у з’єднанні з SGBD. Цей момент важливий: він означає, що для виконання завдання цільовий SGBD не потрібно запускати. delimiter визначає символ, який розділяє дві команди SQL у згенерованій схемі, а format=true вимагає виконання базового форматування згенерованого тексту.
- рядки 51–60 стосуються завдання, яке нас тут цікавить, а саме генерації схеми бази даних-зображення об’єктів @Entity нашого проєкту Eclipse.
- (продовження)
- рядки 63–72 визначають завдання з назвою BD. Воно ідентичне попередньому завданню DDL, за винятком того, що цього разу воно генерує базу даних (export="true" у рядку 70). Завдання відкриває з’єднання з SGBD, використовуючи інформацію, знайдену в [persistence.xml], щоб виконати схему SQL і створити базу даних. Отже, для виконання завдання BD необхідно, щоб завдання SGBD було запущено.
2.1.7. Виконання завдання перед DDL
Щоб виконати скрипт [ant-hibernate.xml], спочатку потрібно виконати кілька налаштувань у Eclipse.
![]() |
- у [1]: вибрати [External Tools]
- у [2]: створити нову конфігурацію ant
![]() |
- в [3]: надати ім’я конфігурації ant
- в [5]: вказати скрипт ant за допомогою кнопки [4]
- у [6]: застосувати зміни
- в [7]: створено конфігурацію ant DDL
![]() |
![]() |
- у [8]: на вкладці JRE визначається JRE, який слід використовувати. Поле [10] зазвичай заповнюється автоматично значенням JRE, яке використовує Eclipse. Тому зазвичай на цій панелі нічого робити не потрібно. Проте я зіткнувся з випадком, коли скрипт ant не міг знайти компілятор <javac>. Він знаходиться не в JRE (Java Runtime Environment), а в JDK (Java Development Kit). Інструмент ant у Eclipse знаходить цей компілятор через змінну середовища JAVA_HOME (Пуск / Панель керування / «Продуктивність та обслуговування» / «Система» / вкладка «Додатково» / кнопка «Змінні середовища») [A]. Якщо ця змінна не була визначена, можна дозволити ant знайти компілятор <javac>, вказавши в [10] не JRE, а JDK. Він знаходиться в тій самій папці, що й JRE та [B]. Ми скористаємося кнопкою [9], щоб вказати JDK серед доступних JRE та [C], щоб потім мати змогу вибрати його в [10].
- у [12]: на вкладці [Targets] вибираємо завдання DDL. Таким чином, конфігурація ant, яку ми назвали DDL [7], відповідатиме виконанню завдання під назвою DDL [12], яке, як відомо, генерує схему DDL бази даних зображень об’єктів @Entity додатка.
![]() |
- у [13]: перевіряється конфігурація
- у [14]: виконуємо її
У вікні [console] відображаються журнали виконання завдання ant DDL:
Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
[delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
[mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
[javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
[copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
- нагадаємо, що завдання DDL має ім’я [hibernatetool] (рядок 10) і залежить від завдань clean (рядок 2), compile (рядок 5) та copyconf (рядок 7).
- рядок 10: завдання [hibernatetool] використовує файл [persistence.xml] з конфігурації JPA
- рядок 11: завдання [hbm2ddl] згенерує схему DDL бази даних
- рядки 12–22: схема бази даних DDL
Нагадаємо, що ми доручили завданню [hbm2ddl] згенерувати схему DDL у певному місці:
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
- рядок 74: схема має бути згенерована у файлі ddl/schema.sql. Перевіримо:
![]() |
- у [1]: файл ddl/schema.sql дійсно присутній (виконайте F5, щоб оновити дерево)
- на [2]: його вміст. Це схема бази даних MySQL5. Файл конфігурації шару [persistence.xml] для шару JPA дійсно вказував на SGBD та MySQL5 (рядок 8 нижче):
<!-- вхід JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
<!-- автоматичне створення схеми -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- діалект -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- властивості DataSource c3p0 -->
...
Розглянемо об’єктно-реляційний міст, який було створено тут, проаналізувавши конфігурацію об’єкта @Entity Personne та згенеровану схему DDL:
![]() |
![]() |
Звернемо увагу на кілька моментів:
- A1-B1: назва таблиці, вказана в A1, дійсно збігається з тією, що використовується в B1. Зверніть увагу на запис drop, який у файлі B1 передує запису create.
- A2-B2: показує спосіб генерації первинного ключа. Режим AUTO, вказаний у A2, призвів до атрибута autoincrement, властивого MySQL5. Режим генерації первинного ключа найчастіше є специфічним для SGBD.
- A3-B3: демонструє біт типу SQL, властивий MySQL5, для представлення типу boolean у Java.
Повторімо цей тест з іншим SGBD:
![]() |
- папка [conf] [1] містить файли [persistence.xml] для різних SGBD. Візьмемо, наприклад, файл Oracle [2] і помістимо його в папку [META-INF] [3] замість попереднього. Його вміст такий:
<?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="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<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.OracleDialect" />
<!-- властивості 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>
Читачеві рекомендується ознайомитися з розділом про Oracle у додатках (параграф 5.7), зокрема для розуміння конфігурації JDBC.
Тут дійсно важливим є лише рядок 25: Hibernate отримує вказівку, що відтепер SGBD є SGBD Oracle. Виконання завдання ant DDL дає результат [4], наведений вище. Слід зауважити, що схема Oracle відрізняється від схеми MySQL5. Це одна з сильних сторін JPA: розробнику не потрібно турбуватися про ці деталі, що значно підвищує переносимість його розробок.
2.1.8. Виконання завдання , що відповідає BD
Можливо, ви пам’ятаєте, що завдання ant, яке називається BD, виконує те саме, що й завдання ant та DDL, але додатково створює базу даних. Отже, необхідно запустити завдання SGBD. Розглянемо випадок із завданнями SGBD та MySQL5 і пропонуємо читачеві скопіювати файл [conf/mysql5/persistence.xml] у папку [src/META-INF]. Щоб перевірити роботу завдання, ми скористаємося плагіном SQL Explorer (див. розділ 5.2.6), щоб перевірити стан файлу BD jpa до та після виконання завдання ant BD.
Перш за все, нам потрібно створити нову конфігурацію ant для виконання завдання BD. Читачеві пропонується дотримуватися покрокової інструкції, наведеної для конфігурації DDL у розділі 2.1.7. Нова конфігурація ant матиме назву BD:
![]() |
- у [1]: дублюємо попередню конфігурацію з назвою DDL
- у [2]: нову конфігурацію називаємо BD. Вона виконує завдання ant, BD та [3], які фізично створюють базу даних.
- Після цього запустіть SGBD та MySQL5 (параграф 5.5).
Тепер ми використовуємо плагін SQL Explorer для перегляду баз даних, що управляються SGBD. Читач повинен попередньо ознайомитися з цим плагіном, якщо це необхідно (див. параграф 5.2.6).
![]() |
- [1]: відкриваємо перспективу SQL Explorer [Window / Open Perspective / Other]
- [2]: за потреби створюємо з’єднання [mysql5-jpa] (див. розділ 5.5.5, стор. 252) та відкриваємо його
- [3]: входимо під ім’ям jpa / jpa
- [4]: встановлено з’єднання з MySQL5.
![]() |
- у [5]: BD jpa має лише одну таблицю: [articles]
- у [6]: запускається виконання завдання ant BD. Оскільки ми перебуваємо в перспективі [SQL Explorer], ми не бачимо виду [Console], який показує нам журнали завдання. Можна відкрити цей вигляд [Window / Show View / ...] або повернутися до перспективи Java [Window / Open Perspective / ...].
- У [7]: після завершення завдання ant BD можна повернутися до перспективи [SQL Explorer] та оновити дерево BD jpa.
- у [8]: бачимо таблицю [jpa01_personne], яка була створена.
Читачеві пропонується повторити це створення BD з іншими SGBD. Порядок дій такий:
- скопіювати файл [conf/<sgbd>/persistence.xml] у папку [src/META-INF], де <sgbd> — це протестований файл SGBD
- запустити <sgbd>, дотримуючись інструкцій у додатках, що стосуються саме цього файлу
- у середовищі SQL Explorer створіть з’єднання з <sgbd>. Це також пояснено в додатках для кожного з SGBD
- повторіть попередні тести
Дійшовши до цього етапу, ми отримали певні знання:
- ми краще розуміємо поняття об’єктно-реляційного мосту. Тут його реалізовано за допомогою Hibernate. Пізніше ми будемо використовувати Toplink.
- ми знаємо, що цей об’єктно-реляційний міст налаштовується у двох місцях:
- в об’єктах @Entity, де вказуються зв’язки між полями об’єктів та стовпцями таблиць у BD
- у [META-INF/persistence.xml], де ми надаємо реалізації JPA інформацію про обидва елементи об’єктно-реляційного мосту: об’єкти @Entity (об’єкт) та базу даних (реляційна частина).
- Ми створили два завдання Ant під назвами DDL та BD, які дозволяють нам створити базу даних на основі попередньої конфігурації ще до написання будь-якого коду Java.
Тепер, коли рівень JPA нашого додатка налаштовано належним чином, ми можемо почати досліджувати API та JPA за допомогою коду Java.
2.1.9. : контекст збереження даних у додатку
Трохи детальніше розглянемо середовище виконання клієнта JPA:
![]() |
Ми знаємо, що рівень JPA [2] створює міст між об’єктами [3] та реляційною базою даних [4]. «Контекст персистентності» — це сукупність об’єктів, що управляються шаром JPA у рамках цього об’єктно-реляційного мосту. Щоб отримати доступ до даних контексту персистентності, клієнт JPA [1] повинен звернутися до шару JPA [2]:
- він може створити об’єкт і звернутися до шару JPA із запитом на його збереження. Після цього об’єкт стає частиною контексту збереження.
- він може звернутися до шару [JPA] із запитом на посилання на існуючий персистентний об’єкт.
- він може змінити об’єкт, отриманий від шару JPA.
- він може звернутися до шару JPA із запитом на видалення об’єкта з контексту персистентності.
Рівень JPA надає клієнту інтерфейс під назвою [EntityManager], який, як випливає з назви, дозволяє керувати об’єктами @Entity у контексті персистентності. Нижче наведено основні методи цього інтерфейсу:
додає entity до контексту персистентності | |
видаляє entity з контексту персистентності | |
об'єднує об'єкт entity клієнта, який не керується контекстом збереження з об’єктом entity з контексту збереження, що має той самий первинний ключ. У результаті отримується об’єкт entity з контексту збереження. | |
вносить у контекст збереження об’єкт, знайдений у базі за допомогою його первинного ключа. Тип T об’єкта дозволяє шару JPA визначити, до якої таблиці слід звернутися. Створений таким чином об’єкт, що зберігається, повертається клієнту. | |
створює об’єкт Query на основі запиту JPQL (Java Persistence Query Language). Запит JPQL аналогічний запиту SQL, за винятком того, що запитуються об’єкти, а не таблиці. | |
метод, аналогічний попередньому, за винятком того, що queryText є команда SQL, а не JPQL. | |
метод ідентичний createQuery, за винятком того, що порядок JPQL queryText був винесений у файл конфігурації та пов’язаний з іменем. Саме ця назва є параметром методу. |
Об’єкт EntityManager має життєвий цикл, який не обов’язково збігається з життєвим циклом додатка. Він має початок і кінець. Таким чином, клієнт JPA може послідовно працювати з різними об’єктами EntityManager. Контекст збереження, пов’язаний з об’єктом EntityManager, має такий самий життєвий цикл, як і сам об’єкт. Вони нерозривно пов’язані між собою. Коли об’єкт EntityManager закривається, його контекст збереження, за необхідності, синхронізується з базою даних, після чого перестає існувати. Щоб знову отримати контекст збереження, потрібно створити новий об’єкт EntityManager.
Клієнт JPA може створити EntityManager, а отже, і контекст збереження, за допомогою такої інструкції:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
- javax.persistence.Persistence — це статичний клас, що дозволяє отримати фабрику (factory) об’єктів EntityManager. Ця фабрика пов’язана з конкретною одиницею персистентності. Нагадаємо, що конфігураційний файл [META-INF/persistence.xml] дозволяє визначати одиниці збереження, і що вони мають імена:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
У наведеному вище прикладі одиниця збереження має назву jpa. Разом із нею надається ціла конфігурація, що належить саме їй, зокрема SGBD, з яким вона працює. Інструкція [Persistence.createEntityManagerFactory("jpa")] створює фабрику об’єктів типу EntityManagerFactory, здатну надавати об’єкти EntityManager, призначені для управління контекстами персистентності, пов’язаними з одиницею персистентності з назвою jpa. Отримання об’єкта EntityManager, а отже, і контексту персистентності, здійснюється на основі об’єкта EntityManagerFactory таким чином:
Наступні методи інтерфейсу [EntityManager] дозволяють керувати життєвим циклом контексту персистентності:
контекст збереження закривається. Примусово синхронізує контекст збереження з базою даних:
| |
Контекст збереження очищено від усіх об’єктів, але не закрито. | |
контекст персистентності синхронізується з базою даних у спосіб, описаний для close() |
Клієнт JPA може примусово синхронізувати контекст персистентності з базою даних за допомогою методу [EntityManager].flush, описаного вище. Синхронізація може бути явною або неявною. У першому випадку клієнт повинен виконувати операції flush, коли він хоче здійснити синхронізацію; в іншому випадку синхронізація відбувається в певні моменти, які ми уточнимо. Режим синхронізації керується наступними методами інтерфейсу [EntityManager]:
Для параметра flushmode можливі два значення: FlushModeType.AUTO (за замовчуванням): синхронізація відбувається перед кожним запитом SELECT, що надсилається до бази даних. FlushModeType.COMMIT: синхронізація відбувається лише після завершення транзакцій у базі даних. | |
показує поточний режим синхронізації |
Підсумуємо. У режимі FlushModeType.AUTO, який є режимом за замовчуванням, контекст збереження даних синхронізується з базою даних у такі моменти:
- перед кожною операцією SELECT на основі
- наприкінці транзакції на базі
- після операції flush або close у контексті збереження даних
У режимі FlushModeType.COMMIT все відбувається так само, за винятком операції 1, яка не виконується. Звичайний режим взаємодії з шаром JPA — це транзакційний режим. Клієнт виконує різні операції з контекстом збереження даних у межах транзакції. У цьому випадку моменти синхронізації контексту збереження даних із базою даних відповідають випадкам 1 і 2, описаним вище для режиму AUTO, а для режиму COMMIT — лише випадку 2.
На завершення розглянемо API інтерфейсу Query, який дозволяє надсилати команди JPQL до контексту збереження або команди SQL безпосередньо до бази даних для пошуку даних. Інтерфейс Query має такий вигляд:
![]() |
Нам доведеться використовувати методи 1–4, наведені вище:
- 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.
2.1.10. Перший клієнт JPA
Повернемося до проекту з точки зору Java:
![]() |
Тепер ми знаємо майже все про цей проєкт, за винятком вмісту папки [src/tests], яку ми зараз розглядаємо. Папка містить дві програми для тестування шару JPA:
- [InitDB.java] — це програма, яка вносить кілька рядків у таблицю [jpa01_personne] бази даних. Її код надасть нам перші елементи шару JPA.
- [Main.java] — це програма, яка виконує операції CRUD над таблицею [jpa01_personne]. Аналіз її коду дозволить нам розглянути основні поняття контексту персистентності та життєвого циклу об’єктів цього контексту.
2.1.10.1. Код
Код програми [InitDB.java] має такий вигляд:
package tests;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import entites.Personne;
public class InitDB {
// константи
private final static String TABLE_NAME = "jpa01_personne";
public static void main(String[] args) throws ParseException {
// Одиниця збереження
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
// отримати EntityManagerFactory з одиниці збереження
EntityManager em = emf.createEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// Видалити записи з таблиці осіб
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// створити двох осіб
Personne 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);
// збереження даних про осіб
em.persist(p1);
em.persist(p2);
// перегляд осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// завершення транзакції
tx.commit();
// завершення EntityManager
em.close();
// завершення EntityManagerFactory
emf.close();
// журнал
System.out.println("terminé ...");
}
}
Цей код слід розглядати у світлі того, що було пояснено в розділі 2.1.9.
- рядок 19: запитується об’єкт EntityManagerFactory emf для одиниці персистентності jpa (визначеної в persistence.xml). Ця операція зазвичай виконується лише один раз протягом життєвого циклу додатка.
- рядок 21: запитується об’єкт EntityManager типу em для управління контекстом персистентності.
- рядок 23: запитується об’єкт Transaction для управління транзакцією. Нагадаємо, що операції з контекстом збереження виконуються в межах транзакції. Ми побачимо, що це не є обов’язковим, але в такому разі можуть виникнути проблеми. Якщо додаток виконується в контейнері EJB3, то операції з контекстом збереження завжди виконуються в межах транзакції.
- рядок 24: транзакція починається
- рядок 26: виконується команда SQL delete над таблицею «jpa01_personne» (nativeQuery). Це робиться для того, щоб очистити таблицю від усього вмісту і таким чином краще побачити результат виконання додатка [InitDB]
- рядки 28–29: створюються два об’єкти Personne — p1 та p2. Це звичайні об’єкти, які наразі не мають жодного стосунку до контексту персистентності. Щодо контексту персистентності, Hibernate визначає ці об’єкти як такі, що перебувають у тимчасовому (transient) стані, на відміну від персистентних (persistent) об’єктів, які управляються контекстом персистентності. Ми будемо використовувати терміни «неперсистентні об’єкти» (нефранцузький вираз), щоб вказати, що вони ще не управляються контекстом персистентності, та «персистентні об’єкти» для тих, що ним управляються. Існує ще третя категорія об’єктів — від’єднані (detached) об’єкти, які раніше були персистентними, але контекст персистентності для них було закрито. Клієнт може мати посилання на такі об’єкти, що пояснює, чому вони не обов’язково знищуються під час закриття контексту персистентності. У такому разі кажуть, що вони перебувають у від’єднаному стані. Операція [EntityManager].merge дозволяє знову приєднати їх до новоствореного контексту персистентності.
- рядки 31–32: особи p1 та p2 інтегруються в контекст персистентності за допомогою операції [EntityManager].persist. Після цього вони стають персистентними об’єктами.
- рядки 35–37: виконується запит JPQL «select p from Personne p order by p.nom asc». Personne — це не таблиця (вона називається jpa01_personne), а об’єкт @Entity, пов’язаний із цією таблицею. Тут ми маємо запит JPQL (Java Persistence Query Language) у контексті персистентності, а не операцію SQL у базі даних. Проте, за винятком об’єкта Personne, який замінив таблицю jpa01_personne, синтаксиси є ідентичними. Цикл for проходить по списку (осіб), отриманому в результаті виконання select, щоб вивести кожен елемент на консоль. Тут ми прагнемо перевірити, чи дійсно в таблиці містяться елементи, внесені в контекст персистентності в рядках 31–32. Прозоро відбудеться синхронізація контексту збереження з базою даних. Дійсно, буде відправлено запит select, і ми вже зазначали, що це один із випадків, коли відбувається синхронізація. Отже, саме в цей момент у фоновому режимі JPA / Hibernate відправить дві команди SQL та insert, які вставлять цих двох осіб у таблицю jpa01_personne. Операція persist цього не зробила. Ця операція додає об’єкти до контексту персистентності, не впливаючи при цьому на базу даних. Фактичні зміни відбуваються під час синхронізацій, у даному випадку безпосередньо перед операцією select у базі даних.
- рядок 39: завершується транзакція, розпочата в рядку 24. Знову відбудеться синхронізація. Тут нічого не відбудеться, оскільки контекст збереження не змінився з моменту останньої синхронізації.
- рядок 41: закривається контекст збереження.
- рядок 43: закривається фабрика EntityManager.
2.1.10.2. виконання коду
- запустити SGBD MySQL5
- при необхідності помістити conf/mysql5/persistence.xml у META-INF/persistence.xml
- запустити додаток [InitDB]
Отримуємо такі результати:
![]() |
- у [1]: вивід на консоль у перспективі Java. Отримуємо те, що очікували.
- у [2]: перевіряємо вміст таблиці [jpa01_personne] за допомогою перспективи SQL Explorer, як було пояснено в розділі 2.1.8. Можна відзначити два моменти:
- первинний ключ ID було згенеровано автоматично
- те саме стосується номера версії. Помітно, що перша версія має номер 0..
Ось ми й маємо перші елементи культури JPA. Нам вдалося ввести дані в таблицю. Ми будемо спиратися на ці знання, щоб написати другий тест, але спочатку поговоримо про логи.
2.1.11. Впровадження логів Hibernate
Можна дізнатися, які команди SQL були відправлені до бази даних шаром JPA / Hibernate. Цікаво їх знати, щоб перевірити, чи рівень JPA є настільки ж ефективним, як і розробник, який би самостійно написав команди SQL.
За допомогою JPA / Hibernate журнали SQL можна перевірити у файлі [persistence.xml]:
<!-- Постійні класи -->
<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" />
- рядки 4–6: журнали SQL поки що не були активовані. Тепер їх активують, видаливши тег коментарів із рядків 3 та 7.
Запускаємо програму [InitDB] ще раз. Вивід на консолі виглядає наступним чином:
- рядки 2–4: команда SQL delete, що походить від інструкції:
// видалити елементи з таблиці осіб
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
- рядки 5–18: команди SQL insert, отримані з інструкцій:
// збереження даних про осіб
em.persist(p1);
em.persist(p2);
- рядки 21–32: команда SQL select, що походить з інструкції:
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList())
Якщо виконати проміжні виводи на консоль, можна побачити, що запис логів SQL для інструкції I з Java-коду відбувається під час виконання інструкції I. Це не означає, що відображена команда SQL виконується в базі даних саме в цей момент. Насправді вона зберігається в кеші для виконання під час наступної синхронізації контексту персистентності з базою даних.
Інші записи журналу можна отримати через файл [src/log4j.properties]:
![]() |
- у [1] файл [log4j.properties] обробляється архівом [log4j-1.2.13.jar] [2] інструментом під назвою LOG4j (Logs for Java), доступного за адресою [http://logging.apache.org/log4j/docs/index.html]. Розміщений у папці [src] проекту Eclipse, ми знаємо, що [log4j.properties] буде автоматично скопійовано до папки [bin] проекту [3]. Після цього файл опиняється у папці classpath цього проєкту, і саме звідти архів [2] його витягне.
Файл [log4j.properties] дозволяє нам контролювати певні журнали Hibernate. Під час попередніх запусків його вміст був таким:
# Направляти повідомлення журналу в 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
# Параметри журналу Root
log4j.rootLogger=ERROR, stdout
# Параметри реєстрації в режимі глибокого сну (INFO відображає лише повідомлення про запуск)
#log4j.logger.org.hibernate=INFO
# Реєструвати аргументи виконання зв’язувальних параметрів JDBC
#log4j.logger.org.hibernate.type=DEBUG
Я не буду детально коментувати цю конфігурацію, оскільки ніколи не приділяв часу, щоб серйозно ознайомитися з LOG4j.
- рядки 1–8 містяться у всіх файлах log4j.properties, з якими мені доводилося стикатися
- рядки 10–14 присутні у файлах log4j.properties із прикладів Hibernate.
- Рядок 11: контролює загальні журнали Hibernate. Оскільки цей рядок закомментований, ці журнали тут вимкнені. Може бути кілька рівнів журналів: INFO (загальна інформація про те, що робить Hibernate), WARN (Hibernate попереджає про можливу проблему), DEBUG (детальні журнали). Рівень INFO є найменш детальним, а режим DEBUG — найдетальнішим. Увімкнення рядка 11 дозволяє дізнатися, що робить Hibernate, зокрема під час запуску додатка. Це часто буває цікаво.
- Рядок 12, якщо він активний, дозволяє дізнатися, які аргументи фактично використовуються під час виконання налаштованих запитів SQL.
Почнемо з того, що розкоментуємо рядок 14
# Журнал аргументів виконання зв'язувальних параметрів JDBC
log4j.logger.org.hibernate.type=DEBUG
і знову виконаємо [InitDB]. Нові записи в журналі, що з’явилися внаслідок цієї зміни, такі (частковий вигляд):
- рядки 8–10 — це нові записи в журналі, що з’явилися внаслідок активації рядка 14 у [log4j.properties]. Вони вказують 5 значень, присвоєних формальним параметрам ? у параметризованому запиті рядків 2–7. Отже, бачимо, що стовпець VERSION отримає значення 0 (рядок 8).
Тепер активуємо рядок 11 файлу [log4j.properties]:
# Параметри журналювання Hibernate (INFO відображає лише повідомлення про запуск)
log4j.logger.org.hibernate=INFO
і знову виконаємо [InitDB]:
Аналіз цих логів дає багато цікавої інформації:
- рядок 7: Hibernate вказує ім’я класу @Entity, який він знайшов
- рядок 8: вказує, що клас [Personne] буде пов'язаний із таблицею [jpa01_personne]
- рядок 9: вказує пул з’єднань C3P0, який буде використовуватися, ім’я драйвера JDBC, URL-адресу бази даних, що буде оброблятися
- рядок 10: містить інші характеристики з’єднання JDBC: власник, тип коміту, ...
- рядок 14: діалект, що використовується для взаємодії з SGBD
- рядок 15: тип використовуваної транзакції. JDBCTransactionFactory вказує, що додаток самостійно керує своїми транзакціями. Він не виконується в контейнері EJB3, який би надавав власний сервіс транзакцій.
- наступні рядки стосуються параметрів конфігурації Hibernate, з якими ми не стикалися. Зацікавленим читачам рекомендується ознайомитися з документацією Hibernate.
- рядок 37: команди SQL будуть виведені на консоль. Це було зазначено в [persistence.xml]:
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="use_sql_comments" value="true" />
- рядки 43–45: схема бази даних експортується до файлів SGBD та c.a.d. База даних очищується, а потім створюється заново. Цей механізм зумовлений налаштуваннями, виконаними у файлі [persistence.xml] (рядок 4 нижче):
...
<property name="hibernate.connection.password" value="jpa" />
<!-- Автоматичне створення схеми -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Діалект -->
...
Коли додаток «вилітає» з незрозумілим винятком Hibernate, для з’ясування ситуації слід спочатку увімкнути журнали Hibernate у режимі DEBUG у файлі [log4j.properties]:
# Параметр кореневого логера
log4j.rootLogger=ERROR, stdout
# Параметри журналювання Hibernate (INFO відображає лише повідомлення про запуск)
log4j.logger.org.hibernate=DEBUG
У подальшому тексті цього документа журнали за замовчуванням вимкнені, щоб зробити вивід консолі більш читабельним.
2.1.12. Ознайомлення з мовою JPQL / HQL за допомогою консолі Hibernate
Примітка: Для цього розділу потрібен плагін Hibernate Tools (параграф 5.2.5).
У коді додатка [InitDB] ми використали запит JPQL. JPQL (Java Persistence Query Language) — це мова для формування запитів до контексту персистентності. Запит, який ми зустріли, мав такий вигляд:
Він відбирав усі елементи таблиці, пов’язаної з @Entity [Personne], і повертав їх у порядку зростання імені. У наведеному вище запиті p.nom — це поле «ім’я» екземпляра p класу [Personne]. Отже, запит JPQL оперує об’єктами @Entity з контексту персистентності, а не безпосередньо з таблицями бази даних. Натомість рівень JPA перетворить цей запит JPQL на запит SQL, відповідний для SGBD, з яким він працює. Таким чином, у випадку реалізації JPA / Hibernate, пов’язаної з SGBD та MySQL5, попередній запит JPQL перетворюється на наступний запит SQL:
select
personne0_.ID as ID0_,
personne0_.VERSION as VERSION0_,
personne0_.NOM as NOM0_,
personne0_.PRENOM as PRENOM0_,
personne0_.DATENAISSANCE as DATENAIS5_0_,
personne0_.MARIE as MARIE0_,
personne0_.NBENFANTS as NBENFANTS0_
from
jpa01_personne personne0_
order by
personne0_.NOM asc
Шар JPA використав конфігурацію об’єкта @Entity [Personne] для генерації правильного замовлення SQL. У цьому випадку було реалізовано об’єктно-реляційний міст.
Плагін [Hibernate Tools] (розділ 5.2.5) надає інструмент під назвою «Консоль Hibernate», який дозволяє
- видавати запити JPQL або їх надмножину HQL (Hibernate Query Language) у контексті персистентності
- отримувати їхні результати
- дізнатися, який еквівалент SQL було виконано в базі
Консоль Hibernate — це надзвичайно цінний інструмент для вивчення мови JPQL та ознайомлення з мостом JPQL / SQL. Відомо, що JPA значною мірою натхненний такими інструментами, як Hibernate або Toplink. JPQL дуже нагадує мову HQL від Hibernate, але не підтримує всіх її функцій. У консолі Hibernate можна вводити команди HQL, які нормально виконуються в консолі, але не є частиною мови JPQL і тому не можуть використовуватися в клієнті JPA. У таких випадках ми будемо про це повідомляти.
Створимо консоль Hibernate для нашого поточного проєкту Eclipse:
![]() |
- [1]: переходимо до перспективи [Hibernate Console] (Window / Open Perspective / Other)
- [2]: створюємо нову конфігурацію у вікні [Hibernate Configuration]
- за допомогою кнопки [4] вибираємо проект Java, для якого створюється конфігурація Hibernate. Її назва відображається в [3].
- У [5] ми задаємо бажану назву для цієї конфігурації. Тут ми використали [3].
- у [6] ми вказуємо, що використовуємо конфігурацію JPA, щоб інструмент знав, що йому слід використовувати файл [META-INF/persistence.xml]
- у [7]: ми вказуємо, що в цьому файлі [META-INF/persistence.xml] слід використовувати модуль збереження даних під назвою jpa.
- У файлі [8] ми підтверджуємо конфігурацію.
Далі потрібно запустити SGBD. Тут мова йде про MySQL5.
![]() |
- у [1]: створена конфігурація має дерево з трьома гілками
- у [2]: гілка [Configuration] містить перелік об’єктів, які консоль використовувала для налаштування: тут це @Entity Personne.
- у [3]: Session Factory — це поняття Hibernate, близьке до EntityManager з JPA. Воно забезпечує об’єктно-реляційний міст завдяки об’єктам гілки [Configuration]. У [3] представлені об’єкти контексту персистентності, зокрема, знову @Entity Personne.
- У [4]: база даних, доступ до якої здійснюється за допомогою конфігурації, що міститься в [persistence.xml]. У ній міститься таблиця [jpa01_personne].
![]() |
- у [1] створюється редактор HQL
- у редакторі HQL,
- у [2] вибираємо конфігурацію Hibernate, яку потрібно використовувати, якщо їх декілька
- у [3] вводимо команду JPQL, яку хочемо виконати
- у [4] — виконуємо її
- у [5] результати запиту відображаються у вікні [Hibernate Query Result]. Тут можуть виникнути дві проблеми:
- не отримуємо нічого (жодного рядка). Консоль Hibernate використала вміст [persistence.xml] для створення з’єднання з SGBD. Однак ця конфігурація має властивість, яка вказує на очищення бази даних:
<property name="hibernate.hbm2ddl.auto" value="create" />
Тому перед повторним виконанням наведеної вище команди JPQL потрібно запустити програму [InitDB] ще раз.
- (продовження)
- Вікно [Hibernate Query Result] відсутнє. Його викликають за допомогою команди [Window / Show View / ...]
Вікно [Hibernate Dynamic SQL preview] ([1] нижче) дозволяє переглянути запит SQL, який буде виконано для реалізації команди JPQL, яку ми зараз пишемо. Як тільки синтаксис команди JPQL буде правильним, у цьому вікні з’явиться відповідна команда SQL:
![]() |
- у [2] видаляємо попередню команду HQL
- у [3] виконується нова команда
- на [4], результат
- у [5], команда SQL, яка була виконана на основі
Редактор HQL надає допомогу у написанні команд HQL:
![]() |
- в [1]: як тільки редактор розпізнає, що p є об’єктом Personne, він може під час введення тексту пропонувати нам поля p.
- у [2]: неправильна команда HQL. Потрібно написати where p.marie=true.
- у [3]: про помилку повідомляється у вікні [SQL Preview]
Пропонуємо читачеві виконати інші команди HQL / JPQL на базі.
2.1.13. Другий клієнт JPA
Повернемося до проекту з точки зору Java:
![]() |
- [InitDB.java] — це програма, яка вносила кілька рядків у таблицю [jpa01_personne] бази даних. Аналіз її коду дозволив нам отримати перші елементи API та JPA.
- [Main.java] — це програма, яка виконує операції CRUD над таблицею [jpa01_personne]. Аналіз її коду дозволить нам повернутися до основних понять контексту збереження даних та життєвого циклу об’єктів цього контексту.
2.1.13.1. Структура коду
[Main.java] буде виконувати серію тестів, кожен з яких має на меті продемонструвати певний аспект JPA:
![]() |
Метод [main]
- послідовно викликає методи від test1 до test11. Ми окремо розглянемо код кожного з цих методів.
- Крім того, він використовує приватні допоміжні методи: clean, dump, log, getEntityManager, getNewEntityManager.
Ми представляємо метод main та так звані допоміжні методи:
package tests;
...
import entites.Personne;
@SuppressWarnings("unchecked")
public class Main {
// константи
private final static String TABLE_NAME = "jpa01_personne";
// Контекст збереження даних
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// Спільні об’єкти
private static Personne p1, p2, newp1;
public static void main(String[] args) throws Exception {
// Очищення бази даних
log("clean");clean();
// зливання таблиці
dump();
// test1
log("test1");test1();
...
// тест11
log("test11");test11();
// завершення контексту збереження
if (em.isOpen())
em.close();
// закриття EntityManagerFactory
emf.close();
}
// отримати поточний EntityManager
private static EntityManager getEntityManager() {
if (em == null || !em.isOpen()) {
em = emf.createEntityManager();
}
return em;
}
// отримати новий EntityManager
private static EntityManager getNewEntityManager() {
if (em != null && em.isOpen()) {
em.close();
}
em = emf.createEntityManager();
return em;
}
// перегляд вмісту таблиці
private static void dump() {
// поточний контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// відображення осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// завершення транзакції
tx.commit();
}
// очищення BD
private static void clean() {
// контекст збереження даних
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалити елементи з таблиці PERSONNES
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// завершення транзакції
tx.commit();
}
// журнали
private static void log(String message) {
System.out.println("main : ----------- " + message);
}
// створення об’єктів
public static void test1() throws ParseException {
...
}
// зміна об’єкта контексту
public static void test2() {
...
}
// запит об'єктів
public static void test3() {
...
}
// видалення об’єкта, що належить до контексту збереження
public static void test4() {
....
}
// від'єднання, повторне приєднання та зміна
public static void test5() {
...
}
// видалити об’єкт, що не належить до контексту збереження
public static void test6() {
...
}
// змінити об’єкт, що не належить до контексту збереження
public static void test7() {
...
}
// повторно приєднати об’єкт до контексту збереження
public static void test8() {
...
}
// запит SELECT викликає синхронізацію
// бази даних із контекстом збереження
public static void test9() {
....
}
// контроль версій (оптимістична блокування)
public static void test10() {
...
}
// відкат транзакції
public static void test11() throws ParseException {
...
}
}
- рядок 13: об’єкт EntityManagerFactory emf, побудований на основі одиниці персистентності jpa, визначеної в [persistence.xml]. Він дозволить нам створювати в процесі роботи додатка різні контексти персистентності.
- рядок 14: контекст персистентності EntityManager, який ще не ініціалізовано
- рядок 17: три об’єкти [Personne], які використовуються у всіх тестах
- рядок 21: таблиця jpa01_personne очищується, а потім виводиться на екран у рядку 24, щоб переконатися, що ми починаємо з порожньої таблиці.
- рядки 27–31: послідовність тестів
- рядки 34–35: закриття контексту персистентності, якщо він був відкритий.
- рядок 38: закриття об’єкта EntityManagerFactory emf.
- рядки 42–47: метод [getEntityManager] встановлює EntityManager (або контекст збереження) як поточний або створює новий, якщо його не існує (рядки 43–44).
- рядки 50–56: метод [getNewEntityManager] створює новий контекст збереження. Якщо раніше такий контекст існував, він закривається (рядки 51–52)
- рядки 59–72: метод [dump] виводить вміст таблиці [jpa01_personne]. Цей код уже зустрічався в [InitDB].
- рядки 75–85: метод [clean] очищає таблицю [jpa01_personne]. Цей код уже зустрічався в [InitDB].
- рядки 88–90: метод [log] виводить на консоль повідомлення, яке передається йому як параметр, щоб його можна було помітити.
Тепер можемо перейти до вивчення тестів.
2.1.13.2. Тест 1
Код тесту 1 такий:
// створення об’єктів
public static void test1() throws ParseException {
// контекст збереження даних
EntityManager em = getEntityManager();
// створення осіб
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збереження осіб
em.persist(p1);
em.persist(p2);
// завершення транзакції
tx.commit();
// відображення таблиці
dump();
}
Цей код ми вже зустрічали в [InitDB]: він створює двох осіб і розміщує їх у контексті збереження.
- рядок 4: запитується поточний контекст збереження
- рядки 6–7: створюються дві особи
- рядки 9–15: обидва користувачі розміщуються в контексті збереження даних у межах транзакції.
- рядок 15: внаслідок фіксації транзакції відбувається синхронізація контексту збереження з базою даних. Обидві особи будуть додані до таблиці [jpa01_personne].
- рядок 17: виводиться таблиця
Вивід на консоль цього першого тесту виглядає так:
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
2.1.13.3. Тест 2
Код тесту 2 такий:
// зміна об’єкта контексту
public static void test2() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшується кількість дітей p1
p1.setNbenfants(p1.getNbenfants() + 1);
// змінюється сімейний стан
p1.setMarie(false);
// об'єкт p1 автоматично зберігається (перевірка змін)
// під час наступної синхронізації (commit або select)
// завершення транзакції
tx.commit();
// виводиться нова таблиця
dump();
}
- Мета тесту 2 — змінити об’єкт контексту персистентності, а потім відобразити вміст таблиці, щоб перевірити, чи відбулася зміна
- рядок 4: отримуємо поточний контекст збереження
- рядки 6–7: операції будуть виконуватися в рамках транзакції
- рядки 9, 11: змінюється кількість дітей особи p1, а також її сімейний стан
- рядок 15: завершення транзакції, тобто синхронізація контексту збереження з базою даних
- рядок 17: виведення таблиці
Вивід на консоль тесту 2 виглядає так:
- рядок 4: особа p1 до внесення змін
- рядок 8: особа p1 після зміни. Зверніть увагу, що її номер версії змінився на 1. Цей номер збільшується на 1 при кожному оновленні рядка.
2.1.13.4. Тест 3
Код тесту 3 виглядає так:
// запит об’єктів
public static void test3() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// запит на особу p1
Personne p1b = em.find(Personne.class, p1.getId());
// оскільки p1 вже знаходиться в контексті збереження, доступ до бази даних не відбувся
// p1b і p1 — це одна й та сама посилання
System.out.format("p1==p1b ? %s%n", p1 == p1b);
// запит об’єкта, що не існує, призводить до того, що 1 покажчик стає нульовим
Personne px = em.find(Personne.class, -4);
System.out.format("px==null ? %s%n", px == null);
// завершення транзакції
tx.commit();
}
- Тест 3 присвячений методу [EntityManager.find], який дозволяє витягнути об’єкт із бази даних та помістити його в контекст збереження. Відтепер ми більше не пояснюємо транзакцію, яка відбувається у всіх тестах, за винятком випадків, коли вона використовується нестандартним чином.
- рядок 9: до контексту персистентності надсилається запит щодо особи, яка має той самий первинний ключ, що й особа p1. Існують два випадки:
- p1 вже знаходиться в контексті збереження. Саме так і є в даному випадку. Тому доступ до бази даних не здійснюється. Метод find просто повертає посилання на збережений об’єкт.
- p1 не перебуває в контексті персистентності. Тоді здійснюється доступ до бази даних за допомогою вказаного первинного ключа. Отриманий запис поміщається в контекст персистентності, і find повертає посилання на цей новий об’єкт, збережений у контексті персистентності.
- рядок 12: перевіряється, чи find вже повернув посилання на об’єкт p1, який перебуває в контексті
- рядок 14: запитується об’єкт, який не існує ані в контексті збереження, ані в базі даних. Метод find повертає покажчик null. Цей момент перевіряється в рядку 15.
Вивід на консоль тесту 3 виглядає наступним чином:
2.1.13.5. Тест 4
Код тесту 4 виглядає так:
// видалення об’єкта, що належить до контексту збереження
public static void test4() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видаляється об’єкт, збережений у контексті p2
em.remove(p2);
// завершення транзакції
tx.commit();
// виводиться нова таблиця
dump();
}
- Тест 4 стосується методу [EntityManager.remove], який дозволяє видалити елемент із контексту збереження, а отже, і з бази даних.
- рядок 9: особа p2 видаляється з контексту збереження
- рядок 11: синхронізація контексту з базою даних
- рядок 13: виведення таблиці. Зазвичай особа p2 вже не повинна там бути.
Вивід на консоль тесту 4 виглядає так:
- рядок 3: особа p2 у test1
- рядки 12–14: вона більше не існує після завершення test4.
2.1.13.6. Тест 5
Код тесту 5 виглядає наступним чином:
// від'єднати, знову приєднати та змінити
public static void test5() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// p1 від'єднано
Personne oldp1=p1;
// p1 знову приєднано до нового контексту
p1 = em.find(Personne.class, p1.getId());
// перевірка
System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);
// завершення транзакції
tx.commit();
// збільшується кількість дочірніх елементів p1
p1.setNbenfants(p1.getNbenfants() + 1);
// виводиться нова таблиця
dump();
}
- тест 5 присвячений життєвому циклу об’єктів, що зберігаються у декількох послідовних контекстах збереження. До цього моменту ми завжди використовували один і той самий контекст збереження у різних тестах.
- рядок 4: запитується новий контекст збереження. Метод [getNewEntityManager] закриває попередній і відкриває новий. Це призводить до того, що об’єкти p1 і p2, які зберігаються додатком, більше не перебувають у стані збереження. Вони належали до контексту, який було закрито. Кажуть, що вони перебувають у від’єднаному стані. Вони не належать до нового контексту збереження.
- рядки 6–7: початок транзакції. Тут вона буде використана нестандартним чином.
- рядок 9: записується адреса об’єкта p1, який тепер є від’єднаним.
- рядок 11: до контексту персистентності надсилається запит щодо особи p1 (за первинним ключем p1). Оскільки контекст новий, особи p1 у ньому немає. Тому відбудеться звернення до бази даних. Повернутий об’єкт буде поміщений у новий контекст.
- рядок 13: перевіряється, чи об’єкт p1 у контексті відрізняється від об’єкта oldp1, який був колишнім від’єднаним об’єктом p1.
- рядок 15: транзакція завершена
- рядок 17: поза транзакцією змінюється новий збережений об’єкт p1. Що відбувається в цьому випадку? Ми хочемо це з’ясувати.
- рядок 19: запитується виведення таблиці. Нагадаємо, що через select, виданий методом dump, автоматично виконується синхронізація контексту збереження з базою даних.
Вивід на консоль тесту 5 виглядає так:
- рядок 5: метод find дійсно здійснив доступ до бази даних, інакше обидва покажчики були б рівними
- рядки 7 і 3: кількість дочірніх елементів p1 дійсно збільшилася на 1. Отже, зміна, внесена поза транзакцією, була врахована. Це, власне, залежить від використовуваного методу SGBD. У SGBD команда SQL завжди виконується в межах транзакції. Якщо клієнт JPA самостійно не ініціює явну транзакцію, SGBD ініціює неявну транзакцію. Існують два типові випадки:
- 1 — кожна окрема команда SQL є предметом транзакції, яка відкривається перед командою та закривається після неї. Це називається режимом автокомміту. Отже, все відбувається так, ніби клієнт JPA виконує транзакції для кожного запиту SQL.
- 2 — SGBD не перебуває в режимі автокомміту і розпочинає неявну транзакцію з першого замовлення SQL, яке клієнт JPA надсилає поза транзакцією, і дозволяє клієнту її закрити. Усі команди SQL, відправлені клієнтом JPA, стають частиною цієї неявної транзакції. Вона може завершитися внаслідок різних подій: клієнт закриває з’єднання, починає нову транзакцію тощо.
Ми маємо ситуацію, що залежить від конфігурації SGBD. Отже, ми маємо код, що не є переносимим. Трохи далі ми покажемо код без транзакцій і побачимо, що не всі SGBD поводяться однаково щодо цього коду. Тому ми вважатимемо, що робота поза транзакціями є помилкою програмування.
- рядок 7: зверніть увагу, що номер версії змінився на 2.
2.1.13.7. Тест 6
Код тесту 6 такий:
// видалити об’єкт, що не належить до контексту збереження
public static void test6() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видаляється p1, що не належить до нового контексту
try {
em.remove(p1);
// завершення транзакції
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
// виконується відкат транзакції
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// виводиться нова таблиця
dump();
}
- Тест 6 намагається видалити об’єкт, який не належить до контексту персистентності.
- рядок 4: запитується новий контекст збереження. Отже, попередній закривається, а об’єкти, що містилися в ньому, стають від’єднаними. Це стосується об’єкта p1 з попереднього тесту 5.
- рядки 6–7: початок транзакції.
- рядок 10: видаляємо відокремлений об’єкт p1. Ми знаємо, що це спричинить виняток, тому операцію обгорнули в блоці try/catch.
- рядок 12: фіксація не відбудеться.
- рядки 16–21: транзакція має завершитися кодом commit (усі операції транзакції підтверджено) або rollback (усі операції транзакції скасовано). Сталося виключення, тому ми виконуємо rollback для транзакції. Скасовувати нічого, оскільки єдина операція транзакції завершилася невдало, але rollback завершує транзакцію. Це перший раз, коли ми використовуємо операцію [EntityTransaction].rollback. Ми мали б зробити це ще з перших прикладів. Ми не робили цього, щоб зберегти простоту коду. Проте читач повинен пам’ятати, що випадок виконання rollback для транзакції завжди має бути передбачений у коді.
- рядок 24: виводимо таблицю. Зазвичай вона не повинна змінитися.
Вивід на консоль тесту 6 виглядає так:
- рядок 6: видалення p1 не вдалося. Повідомлення про виняток пояснює, що ми намагалися видалити відокремлений об’єкт, який не входить до контексту. Це неможливо.
- рядок 8: об’єкт p1 все ще існує.
2.1.13.8. Тест 7
Код тесту 7 виглядає так:
// змінюємо об’єкт, що не належить до контексту збереження
public static void test7() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшується кількість дочірніх елементів p1, що не належать до нового контексту
p1.setNbenfants(p1.getNbenfants() + 1);
// завершення транзакції
tx.commit();
// виводиться нова таблиця — вона, мабуть, не змінилася
dump();
}
- Тест 7 має на меті змінити об’єкт, який не належить до контексту персистентності, та перевірити, як це вплине на базу даних. Можна припустити, що це не матиме жодного впливу. Саме це й показують результати тесту.
- рядок 4: запитується новий контекст збереження. Отже, ми маємо новий контекст, у якому немає збережених об’єктів.
- рядки 6–7: початок транзакції.
- рядок 9: змінюється відокремлений об’єкт p1. Ця операція не зачіпає контекст збереження em. Тому не слід очікувати винятку чи чогось подібного. Це базова операція над POJO.
- рядок 11: операція commit викликає синхронізацію контексту з базою даних. Цей контекст порожній. Отже, база даних не змінюється.
- рядок 24: виводимо таблицю. Зазвичай вона не мала змінитися.
Вивід на консоль тесту 7 виглядає так:
- рядок 7: дані про особу p1 у базі даних не змінилися. Проте для наступного тесту слід пам’ятати, що в пам’яті кількість її дітей тепер становить 5.
2.1.13.9. Тест 8
Код тесту 8 такий:
// повторно прив’язуємо об’єкт до контексту збереження
public static void test8() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// від'єднаний об'єкт p1 знову прив'язується до нового контексту
newp1 = em.merge(p1);
// тепер до контексту входить newp1, а не p1
// кінець транзакції
tx.commit();
// виводиться нова таблиця — кількість дочірніх елементів p1 мала змінитися
dump();
}
- Тест 8 знову приєднує від'єднаний об'єкт до контексту персистентності.
- рядок 4: запитується новий контекст збереження. Отже, ми маємо новий контекст, у якому немає об’єктів, що зберігаються.
- рядки 6–7: початок транзакції.
- рядок 9: до контексту персистентності знову приєднується від'єднаний об'єкт p1. Операція merge може передбачати кілька операцій:
- випадок 1: у контексті персистентності існує персистентний об’єкт ps1, який має той самий первинний ключ, що й від’єднаний об’єкт p1. Вміст p1 копіюється в ps1, а merge стає посиланням на ps1.
- Випадок 2: у контексті персистентності не існує персистентного об’єкта ps1, що має той самий первинний ключ, що й від’єднаний об’єкт p1. Тоді виконується запит до бази даних, щоб з’ясувати, чи існує шуканий об’єкт у базі. Якщо так, цей об’єкт переноситься в контекст персистентності, стає персистентним об’єктом ps1, і ми повертаємося до попереднього випадку 1.
- Випадок 3: ні в контексті персистентності, ні в базі даних не існує об’єкта з тим самим первинним ключем, що й відокремлений об’єкт p1. Тоді створюється новий об’єкт [Personne] (new), який потім поміщається в контекст персистентності. Далі ми повертаємося до випадку 1.
- У підсумку: відокремлений об’єкт p1 залишається відокремленим. Операція merge повертає посилання (у даному випадку newp1) на персистентний об’єкт ps1, утворений з merge. Клієнтська програма відтепер має працювати з об’єктом, що зберігається в базі даних, ps1, а не з відокремленим об’єктом p1.
- Слід зазначити різницю між випадками 1 і 3 щодо замовлення SQL, запланованого для merge: у випадках 1 і 2 це наказ UPDATE, тоді як у випадку 3 — це наказ INSERT.
- рядок 12: операція commit викликає синхронізацію контексту з базою даних. Цей контекст більше не є порожнім. Він містить об’єкт newp1. Цей об’єкт буде збережено в базі даних.
- рядок 24: для перевірки виводимо таблицю.
Вивід на консолі тесту 8 виглядає так:
- кількість дочірніх елементів p1 у тесті 6 становила 4 (рядок 4), потім у тесті 7 вона змінилася на 5, але не була збережена в базі даних (рядок 7). Після merge у базі даних було збережено newp1: у рядку 10 дійсно вказано 5 дочірніх елементів.
- рядок 10: номер версії newp1 змінився на 3.
2.1.13.10. Тест 9
Код тесту 9 такий:
// запит SELECT викликає синхронізацію
// бази даних із контекстом персистентності
public static void test9() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшується кількість дітей у newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// відображення осіб — кількість дітей у newp1 мала змінитися
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// завершення транзакції
tx.commit();
}
- Тест 9 має на меті продемонструвати механізм синхронізації контексту, який відбувається автоматично перед виконанням select.
- рядок 5: контекст збереження не змінюється. Отже, newp1 знаходиться всередині нього.
- рядки 7–8: початок транзакції.
- рядок 10: кількість дочірніх елементів об’єкта персистентності newp1 збільшується на 1 (5 → 6).
- рядки 12–15: таблиця виводиться за допомогою оператора SELECT. Контекст буде синхронізовано з базою даних перед виконанням select.
- рядок 17: завершення транзакції
Щоб побачити синхронізацію, увімкнемо виведення журналів Hibernate у режимі DEBUG (log4j.properties):
# Параметр кореневого логера
log4j.rootLogger=ERROR, stdout
# Параметри журналювання Hibernate (INFO показує лише повідомлення про запуск)
log4j.logger.org.hibernate=DEBUG
Вивід на консоль тесту 9 виглядає так:
- рядок 1: тест 9 запускається
- рядки 2–6: починається транзакція Jdbc. Режим autocommit у SGBD вимкнено (рядок 5)
- рядок 7: виведення, викликане рядком 12 коду Java. Наступні рядки коду Java спричинять виконання select і, отже, синхронізацію контексту персистентності з базою даних.
- рядок 8: операція JPQL, яку ми хочемо виконати, вже була виконана. Hibernate знаходить її у своєму кеші «підготовлених запитів».
- рядок 9: Hibernate повідомляє, що збирається виконати очищення контексту персистентності
- рядки 11–12: Hibernate (Hb) виявляє, що суть Personne#1 (з первинним ключем 1) була змінена (dirty).
- рядки 12–13: Hb повідомляє, що оновлює цей елемент і змінює його номер версії з 3 на 4.
- рядок 15: синхронізація контексту призведе до 0 вставлень, 1 оновлення (update), 0 видалень (delete)
- рядки 17–34: синхронізація контексту (flush). Зверніть увагу: збільшення номера версії (рядок 19), підготовлений запит SQL update (рядок 21), значення параметрів запиту update (рядки 24–31).
- рядок 35: починається select
- рядок 38: замовлення SQL, яке буде виконано
- рядок 40: select повертає лише один рядок
- рядок 42: Hb виявляє, що в його контексті персистентності вже є суть Personne#1, яку оператор SELECT повернув із бази даних. Тому він не копіює отриманий із бази даних рядок у контекст — операцію, яку він називає «гідратацією».
- рядок 43: він перевіряє, чи об’єкти, повернуті select, мають залежності (зазвичай — зовнішні ключі), які також потрібно завантажити (неліниві колекції). У цьому випадку їх немає.
- рядок 44: виведення даних, ініційоване кодом Java
- рядок 45: завершення транзакції Jdbc, ініційованої кодом Java
- рядок 46: починається автоматична синхронізація контексту, яка відбувається під час виконання commit.
- рядок 48: Hb виявляє, що контекст не змінився з часу попередньої синхронізації.
- рядок 50: завершення commit.
Знову ж таки, журнали Hibernate в режимі DEBUG виявляються дуже корисними для того, щоб точно зрозуміти, що саме робить Hibernate.
2.1.13.11. Тест 10
Код тесту 10 такий:
// контроль версій (оптимістична блокування)
public static void test10() {
// Контекст збереження даних
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшення версії newp1 безпосередньо в базі даних (нативний запит)
em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
// завершення транзакції
tx.commit();
// початок нової транзакції
tx = em.getTransaction();
tx.begin();
// збільшуємо кількість дочірніх елементів newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// завершення транзакції — вона має завершитися з помилкою, оскільки newp1 більше не має правильної версії
try {
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
// виконується відкат транзакції
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// закриття контексту, який більше не є актуальним
em.close();
// вивантаження таблиці — версія p1 мала змінитися
dump();
}
- Тест 10 має на меті продемонструвати механізм, запроваджений полем version класу @Entity Personne, який має атрибут JPA @Version. Ми пояснювали, що ця анотація призводить до того, що в базі даних значення стовпця, пов’язаного з анотацією @Version, збільшується при кожному update, виконаному над рядком, до якого він належить. Цей механізм, який також називають оптимістичною блокуванням (optimistic locking), вимагає, щоб клієнт, який хоче змінити об’єкт O в базі даних, мав його останню версію. Якщо він її не має, це означає, що об’єкт було змінено з моменту його отримання, і про це потрібно попередити клієнта.
- рядок 4: контекст збереження не змінюється. Отже, newp1 залишається в ньому.
- рядки 6–7: початок транзакції.
- рядок 9: версія об’єкта newp1 збільшується на 1 (4 → 5) безпосередньо в базі даних. Запити типу nativeQuery обходять контекст збереження даних і звертаються безпосередньо до бази даних. В результаті об’єкт збереження newp1 та його відображення в базі даних більше не мають однакової версії.
- рядок 10: кінець першої транзакції
- рядки 13–14: початок другої транзакції
- рядок 16: кількість дочірніх об’єктів персистентного об’єкта newp1 збільшується на 1 (6 → 7).
- рядок 19: кінець транзакції. Отже, відбувається синхронізація. Вона призведе до оновлення кількості дочірніх об’єктів newp1 у базі даних. Оновлення не відбудеться, оскільки постійний об’єкт newp1 має версію 4, тоді як у базі даних об’єкт, що підлягає оновленню, має версію 5. Буде згенеровано виняток, що обґрунтовує використання блоку try/catch у коді.
- рядок 21: виводиться виняток та його причина.
- рядок 25: відкат транзакції
- рядок 33: виведення таблиці: ми повинні побачити, що версія newp1 у базі даних дорівнює 5.
Вивід на консоль тесту 10 виглядає так:
- рядок 5: операція commit справді викликає виняток. Він має тип [javax.persistence.RollbackException]. Повідомлення про це є нечітким. Якщо розглянути причину цього винятку (Exception.getCause), то бачимо, що це виняток Hibernate, спричинений спробою змінити рядок у базі даних без наявності правильної версії.
- рядок 7: бачимо, що версія newp1 у базі даних дійсно була змінена на 5 за допомогою nativeQuery.
2.1.13.12. Тест 11
Код тесту 11 такий:
// відкат транзакції
public static void test11() throws ParseException {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = null;
try {
tx = em.getTransaction();
tx.begin();
// повторне приєднання p1 до контексту шляхом його пошуку в базі даних
p1 = em.find(Personne.class, p1.getId());
// збільшується кількість дітей p1
p1.setNbenfants(p1.getNbenfants() + 1);
// відображення осіб — кількість дітей p1 мала змінитися
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// створення 2 осіб з однаковим ім’ям, що заборонено DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// збереження осіб
em.persist(p3);
em.persist(p4);
// завершення транзакції
tx.commit();
} catch (RuntimeException e1) {
// виникла проблема
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// поточний контекст покинуто
em.clear();
}
// дамп — таблиця не мала змінитися через відкат
dump();
}
- Тест 11 присвячений механізму транзакції rollback. Транзакція працює за принципом «все або нічого»: операції SQL, що входять до її складу, або виконуються успішно (commit), або скасовуються у разі збою хоча б однієї з них (rollback).
- рядок 4: робота продовжується з тим самим контекстом збереження даних. Читач, можливо, пам’ятає, що контекст було закрито після збою попереднього тесту. У цьому випадку [getEntityManager] створює абсолютно новий, тобто порожній, контекст.
- рядки 7–27: єдиний блок try/catch для контролю проблем, з якими ми зіткнемося
- рядки 8–9: початок транзакції, яка міститиме кілька операцій SQL
- рядок 11: p1 шукається в базі даних і поміщається в контекст
- рядок 13: збільшується кількість дочірніх елементів p1 (6 → 7)
- рядки 15–18: виводиться вміст бази даних, що змусить контекст синхронізуватися. У базі кількість дітей p1 зміниться на 7, що має підтвердитися на екрані консолі.
- рядки 20–21: створення двох осіб p3 та p4 з однаковими іменами. Однак поле «ім’я» в @Entity Personne має атрибут unique=true, що призвело до створення обмеження унікальності для стовпця NOM у таблиці [jpa01_personne].
- рядки 23–24: особи p3 та p4 додаються до контексту збереження.
- рядок 26: транзакція фіксується. Далі відбувається друга синхронізація контексту, перша з яких відбулася під час обробки запису select. JPA видасть дві команди SQL та insert для осіб p3 та p4. p3 буде вставлено. Щодо p4, SGBD викличе виняток, оскільки p4 має те саме ім’я, що й p3. Отже, p4 не вставляється, і драйвер JDBC повертає виняток клієнту.
- рядок 27: обробка винятку
- рядки 29–31: виводимо виняток та дві попередні причини у ланцюжку винятків, які привели нас до цього моменту.
- рядок 34: виконується відкат поточної активної транзакції. Ця транзакція розпочалася в рядку 9 коду Java. З того часу була виконана операція update для зміни кількості дітей у записі p1, а потім операція insert для особи p3. Все це буде скасовано в результаті відкоту.
- рядок 39: контекст збереження даних очищено
- рядок 42: відображається таблиця [jpa01_personne]. Необхідно перевірити, чи p1 як і раніше має 6 дітей і чи в таблиці немає ні p3, ні p4.
Вивід на консолі тесту 11 виглядає так:
main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
- рядок 3: кількість дочірніх елементів p1 у базі даних змінилася з 6 на 7, версія p1 змінилася на 6.
- рядок 4: виняток, зафіксований під час фіксації транзакції. Якщо уважно прочитати, можна побачити, що причиною є дубльований ключ X (ім'я). Цю помилку спричиняє вставка запису p4, тоді як раніше вставлений запис p3 також має ім'я X.
- рядок 7: таблиця після відкоту. p1 повернувся до версії 5 і кількості дочірніх записів 6, p3 та p4 не були вставлені.
2.1.13.13. Тест 12
Код тесту 12 такий:
// виконуємо те саме, але без транзакцій
// отримуємо той самий результат, що й раніше, з SGBD: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
// з SQLSERVER таблиця залишається порожньою. З’єднання залишається в стані, що унеможливлює повторне виконання
// програми. У такому випадку потрібно перезапустити сервер.
// те саме з SGBD Derby
// HSQL вставляє першу особу — відкат не відбувається
public static void test12() throws ParseException {
// повторно прив’язуємо p1
p1 = em.find(Personne.class, p1.getId());
// збільшуємо кількість дітей p1
p1.setNbenfants(p1.getNbenfants() + 1);
// відображення осіб — кількість дітей p1 мала змінитися
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// створення 2 осіб з однаковим ім'ям, що заборонено DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// збереження осіб
em.persist(p3);
em.persist(p4);
// зливання даних, що спричинить синхронізацію контексту em із BD
try {
dump();
} catch (RuntimeException e3) {
System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
.getCause().getMessage());
}
// закриваємо поточний контекст
em.close();
// зливання
dump();
}
- Тест 12 повторює те саме, що й тест 11, але поза транзакцією. Ми хочемо побачити, що відбувається в цьому випадку.
- рядки 1–6: містять результати тестів із різними значеннями SGBD:
- з певною кількістю SGBD (Firebird, Oracle, MySQL5, Postgres) ми отримуємо той самий результат, що й у тесті 11. Це наводить на думку, що ці SGBD самостійно ініціювали транзакцію, яка охоплювала всі отримані команди SQL аж до тієї, що спричинила помилку, і що вони самі ініціювали rollback.
- Разом з іншими SGBD (SQL Server, Apache Derby) відбувається збій у роботі додатка та/або SGBD.
- з SGBD та HSQLDB, схоже, що транзакція, відкрита SGBD, перебуває в режимі autocommit: зміна кількості дочірніх елементів p1 та вставлення p3 стають постійними. Лише вставлення p4 завершується невдало.
Отже, ми маємо результат, що залежить від SGBD, що робить додаток непереносимим. Слід пам’ятати, що операції з контекстом збереження даних завжди повинні виконуватися в межах транзакції.
2.1.14. Зміна SGBD
Повернемося до тестової архітектури нашого поточного проєкту:
![]() |
Клієнтський додаток [3] бачить лише інтерфейс JPA [5]. Він не бачить ані його фактичної реалізації, ані цільового SGBD. Отже, ми повинні мати можливість змінити ці два елементи ланцюга без внесення змін у клієнт [3]. Саме це ми зараз і намагаємося перевірити, почавши зі зміни SGBD. До цього часу ми використовували MySQL5. Ми пропонуємо ще шість варіантів, описаних у додатках (параграф 5), сподіваючись, що серед них знайдеться улюблений читачем SGBD.
У будь-якому разі, зміна, яку потрібно внести в проєкт Eclipse, є простою (див. нижче): замінити файл конфігурації шару persistence.xml [1] на один із файлів із папки conf [2] цього проєкту. Драйвери JDBC та SGBD вже присутні в бібліотеці [jpa-divers], [3] та [4].
![]() |
2.1.14.1. Oracle 10g Express
Oracle 10g Express представлено в додатках у розділі 5.7. Файл persistence.xml від Oracle має такий вигляд:
<?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="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<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.OracleDialect" />
<!-- властивості 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>
Ця конфігурація ідентична тій, що була створена для SGBD та MySQL5, за винятком таких деталей:
- рядки 15–18, які налаштовують з’єднання JDBC з базою даних
- рядок 22: у якому задано діалект SQL, що має використовуватися
У наступних прикладах ми будемо вказувати лише ті рядки, які змінюються. Пояснення щодо конфігурації наведено у додатку, присвяченому використовуваному SGBD. У ньому щоразу наводиться приклад використання зв’язку JDBC у контексті плагіна [SQL Explorer]. За допомогою інформації з додатка читач зможе повторити процедуру перевірки результату роботи програми [InitDB], описану в розділі 2.1.10.2.
Діємо так, як зазначено у вищезгаданому параграфі:
- запускаємо Oracle-скрипт SGBD
- вставити файл conf/oracle/persistence.xml у META-INF/persistence.xml
- запустити програму [InitDB]
На консолі з’являються такі результати:
![]() |
Надалі ми більше не будемо наводити цей знімок екрана, оскільки він завжди однаковий. Більш цікавим є перегляд у SQL зв’язку між JDBC та SGBD. Ми будемо дотримуватися процедури, описаної в розділі 2.1.8.
![]() |
- у [1]: з’єднання з Oracle
- в [2]: дерево з’єднання після виконання [InitDB]
- у [3]: структура таблиці [jpa01_personne]
- у [4]: її вміст.
Після цього читачеві пропонується запустити програму [Main], а потім зупинити SGBD.
2.1.14.2. PostgreSQL 8.2
PostgreSQL 8.2 наведено в додатках до пункту 5.6. Його файл 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">
...
<!-- вхід JDBC -->
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Діалект -->
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
</persistence-unit>
</persistence>
Щоб виконати [InitDB]:
- запустити SGBD PostgreSQL
- встановити файл conf/postgres/persistence.xml у META-INF/persistence.xml
- запустити додаток [InitDB]
Перспектива SQL Explorer щодо зв’язку JDBC з SGBD є такою:
![]() |
- у [1]: зв’язок із PostgreSQL
- у [2]: дерево зв’язків після виконання [InitDB]
- у [3]: структура таблиці [jpa01_personne]
- в [4]: її вміст.
Після цього читачеві пропонується запустити програму [Main], а потім зупинити SGBD
2.1.14.3. SQL Server Express 2005
SQL Server Express 2005 наведено в додатках до параграфа 5.8, сторінка 270. Його файл 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">
...
<!-- вхід JDBC -->
<property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
<property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Діалект -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
</persistence-unit>
</persistence>
Щоб запустити [InitDB]:
- запустіть SGBD SQL Server
- помістіть файл conf/sqlserver/persistence.xml у папку META-INF/persistence.xml
- запустити додаток [InitDB]
Перспектива SQL Explorer для зв’язку JDBC з SGBD виглядає наступним чином:
![]() |
- у [1]: з’єднання з сервером SQL
- у [2]: дерево з’єднань після виконання [InitDB]
- у [3]: структура таблиці [jpa01_personne]
- у [4]: її вміст.
Після цього читачеві пропонується запустити програму [Main], а потім зупинити SGBD
2.1.14.4. Firebird 2.0
Firebird 2.0 представлено в додатках у розділі 5.4. Його файл 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">
...
<!-- з'єднання JDBC -->
<property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
<property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
<property name="hibernate.connection.username" value="sysdba" />
<property name="hibernate.connection.password" value="masterkey" />
...
<!-- Діалект -->
<property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
</persistence-unit>
</persistence>
Щоб запустити [InitDB]:
- запустіть файл SGBD Firebird
- помістіть файл conf/firebird/persistence.xml у папку META-INF/persistence.xml
- запустіть програму [InitDB]
Перспектива SQL Explorer щодо зв’язку JDBC з SGBD є такою:
![]() |
- у [1]: з'єднання з Firebird
- у [2]: дерево з’єднань після виконання [InitDB]
- у [3]: структура таблиці [jpa01_personne]
- у [4]: її вміст.
Після цього користувачеві пропонується запустити програму [Main], а потім зупинити SGBD.
2.1.14.5. Apache Derby
Apache Derby представлено в Додатках у розділі 5.10. Його файл 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">
...
<!-- з'єднання JDBC -->
<property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
<property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- Діалект -->
...
</persistence-unit>
</persistence>
Щоб запустити [InitDB]:
- запустіть файл SGBD Apache Derby
- помістіть conf/derby/persistence.xml у META-INF/persistence.xml
- запустіть додаток [InitDB]
У перспективі SQL Explorer зв'язок між JDBC та SGBD виглядає наступним чином:
![]() |
- у [1]: з’єднання з Apache Derby
- у [2]: дерево зв’язків після виконання [InitDB]. Зверніть увагу на таблицю [HIBERNATE_UNIQUE_KEY], створену JPA / Hibernate для автоматичного генерування послідовних значень первинного ключа ID. Ми вже зазначали, що цей механізм часто є пропрієтарним. Це чітко видно тут. Завдяки JPA розробнику не доводиться заглиблюватися в ці деталі SGBD.
- у [3]: структура таблиці [jpa01_personne]
- у [4]: її вміст.
Після цього читачеві пропонується запустити програму [Main], а потім зупинити SGBD.
2.1.14.6. HSQLDB
HSQLDB наведено в додатках до пункту 5.9. Його файл 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">
...
<!-- з'єднання JDBC -->
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
<property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
<property name="hibernate.connection.username" value="sa" />
<!--
<property name="hibernate.connection.password" value="" />
-->
...
<!-- Діалект -->
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
</properties>
</persistence-unit>
</persistence>
Щоб запустити [InitDB]:
- запустіть SGBD HSQL
- вставити файл conf/hsql/persistence.xml у META-INF/persistence.xml
- запустити додаток [InitDB]
Перспектива SQL Explorer для зв’язку JDBC з SGBD виглядає наступним чином:
![]() |
- у [1]: зв’язок із HSQL
- у [2]: дерево зв’язків після виконання [InitDB].
- у [3]: структура таблиці [jpa01_personne]
- у [4]: її вміст.
Після цього читачеві пропонується запустити програму [Main], а потім зупинити SGBD.
2.1.15. Зміна реалізації JPA
Повернемося до архітектури тестування нашого поточного проєкту:
![]() |
Попереднє дослідження показало, що ми змогли замінити SGBD на [7], не змінюючи код клієнта [3]. Тепер ми змінюємо реалізацію JPA [6] і знову демонструємо, що це відбувається прозоро для клієнтського коду [3]. Візьмемо реалізацію TopLink та [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:
![]() |
2.1.15.1. Проєкт Eclipse
З нагоди зміни реалізації JPA ми створюємо новий проект Eclipse, щоб не захаращувати існуючий проект. Справа в тому, що новий проект використовує бібліотеки збереження даних, які можуть конфліктувати з бібліотеками Hibernate:
![]() |
- у [1]: папка [<exemples>/toplink/direct/personnes-entites] містить проект Eclipse. Імпортуйте його.
- в [2]: імпортований проєкт [toplink-personnes-entites]. Він ідентичний (був отриманий шляхом копіювання) проєкту [hibernate-personne-entites], за винятком двох деталей:
- файл [META-INF/persistence.xml] [3] тепер налаштовує шар JPA / Toplink
- бібліотека [jpa-hibernate] була замінена на бібліотеку [jpa-toplink], [4] та [5] (див. параграф 1.5).
- у [6]: папка [conf] містить версію файлу [persistence.xml] для кожного SGBD.
- у [7]: папка [ddl], яка міститиме скрипти SQL для генерації схеми бази даних.
2.1.15.2. Конфігурація шару JPA / Toplink
Ми знаємо, що шар JPA налаштовується файлом [META-INF/persistence.xml]. Цей файл тепер налаштовує реалізацію JPA / Toplink. Його вміст для шару JPA, що взаємодіє з SGBD та MySQL5, є таким:
<?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>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- постійні класи -->
<class>entites.Personne</class>
<!-- властивості одиниці збереження -->
<properties>
<!-- з'єднання JDBC -->
<property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="MySQL4" />
<!-- сервер додатків -->
<property name="toplink.target-server" value="None" />
<!-- генерація схеми -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- журнали -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
- рядок 3: не змінився
- рядок 5: провайдером тепер є Toplink. Клас, названий тут, можна знайти в бібліотеці [jpa-toplink] ([1] нижче):
![]() |
- рядок 7: тег <class> слугує для зазначення всіх класів @Entity у проєкті, тут — лише класу Personne. У Hibernate була опція конфігурації, яка дозволяла нам не вказувати ці класи. Він сканував файл classpath проєкту, щоб знайти там класи @Entity.
- рядок 9: тег <properties>, який вводить властивості, характерні для використовуваної реалізації JPA, у даному випадку Toplink.
- рядки 11–14: налаштування з’єднання JDBC із SGBD MySQL5
- рядки 15–18: налаштування пулу з’єднань JDBC, який нативно керується Toplink:
- рядки 15, 16: максимальна та мінімальна кількість з’єднань у пулі з’єднань для читання. За замовчуванням (2,2)
- рядки 17, 18: максимальна та мінімальна кількість з’єднань у пулі з’єднань для запису. За замовчуванням (10, 2)
- рядок 20: цільовий SGBD. Список доступних SGBD міститься в пакеті [oracle.toplink.essentials.platform.database] (див. [2] вище). SGBD MySQL5 відсутні у списку [2], тому було обрано MySQL4. Toplink підтримує дещо менше SGBD, ніж Hibernate. Отже, із семи SGBD, використаних у наших прикладах, Firebird не підтримується. У списку також відсутній Oracle. Насправді він міститься в іншому пакеті ([3], зазначеному вище). Якщо в цих двох пакетах цільовий SGBD позначається класом <Sgbd>Platform.class, тег буде виглядати так:
<property name="toplink.target-database" value="<Sgbd>" />
- рядок 22: встановлює сервер додатків, якщо додаток виконується на такому сервері. Поточні можливі значення (None, OC4J_10_1_3, SunAS9). За замовчуванням (None).
- рядки 24–28: під час ініціалізації шару JPA йому буде доручено очистити базу даних, визначену зв’язком JDBC у рядках 11–14. Таким чином, ми почнемо з порожньої бази даних.
- рядок 24: Toplink отримує запит на виконання операції drop, а потім — create для таблиць схеми бази даних
- рядок 25: ми попросимо Toplink згенерувати скрипти SQL для операцій drop та create. Параметр application-location визначає папку, в якій будуть згенеровані ці скрипти. За замовчуванням: (поточна папка).
- рядок 26: ім’я скрипта SQL для операцій create.. За замовчуванням: createDDL.jdbc.
- рядок 27: ім’я скрипта SQL для операцій drop.. За замовчуванням: dropDDL.jdbc.
- рядок 28: режим генерації схеми (за замовчуванням: both):
- both: скрипти та база даних
- database: тільки база даних
- sql-script: лише скрипти
- рядок 30: вимкнено (OFF) журнали Toplink. Доступні такі рівні входу: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. За замовчуванням: INFO.
Вичерпне визначення тегів <property>, які можна використовувати з Toplink, наведено за посиланням [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html].
2.1.15.3. Тест [InitDB]
Більше нічого робити не потрібно. Ми готові виконати перший тест [InitDB]:
- запустити SGBD, тут MySQL5
- виконати [InitDB]
![]() |
- у [1]: вивід на консоль. Ми бачимо результати, вже отримані за допомогою JPA / Hibernate.
- у [3]: відкриваємо перспективу [SQL Explorer], а потім — з’єднання [mysql5-jpa]
- у [4]: дерево бази даних jpa. Виявляється, що під час виконання [InitDB] було створено дві таблиці: [jpa01_personne], що було очікувано, та таблицю [sequence], що було несподівано.
![]() |
- у [5]: структура таблиці [jpa01_personne], а в [6] — її вміст
- у [7]: структура таблиці [sequence], а в [8] — її вміст.
Конфігураційний файл [persistence.xml] вимагав генерації скриптів з DDL:
<!-- генерація схеми -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
Давайте подивимося, що було згенеровано в папці [ddl/mysql5]:
![]() |
create.sql
CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- рядок 1: запис DDL з таблиці [jpa01_personne]. Можна помітити, що Toplink не використав атрибут autoincrement для первинного ключа ID. Це означає, що під час вставки рядків не відбувається його автоматичне інкрементування.
- рядок 2: DDL з таблиці [sequence]. Її назва, здається, вказує на те, що Toplink використовує цю таблицю для генерації значень первинного ключа ID.
- рядок 3: вставлення одного рядка в таблицю [SEQUENCE]
drop.sql
DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
- рядок 1: видалення таблиці [jpa01_personne]
- рядок 2: видалення певного рядка з таблиці [SEQUENCE]. Сама таблиця не видаляється, як і інші рядки, які вона може містити.
Щоб дізнатися більше про роль таблиці [SEQUENCE], у [persistence.xml] активують журнали Toplink на рівні FINE — рівні, який фіксує команди SQL, що видаються Toplink:
<!-- журнали -->
<property name="toplink.logging.level" value="FINE" />
Повторно виконуємо InitDB. Нижче наведено лише фрагмент виводу консолі:
...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--З'єднання(30708295)--Потік(Thread[main,5,main])--Підключено: jdbc:mysql://localhost:3306/jpa
User: jpa@localhost
Database: MySQL Version: 5.0.37-community-nt
Driver: MySQL-AB JDBC Driver Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--З'єднання(19255406)--Потік(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--З'єднання(30708295)--Потік(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) за замовчуванням 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--З’єднання(19255406)--Потік(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Потік(Thread[main,5,main])--Виняток [TOPLINK-4002] (Oracle TopLink Essentials — 2.0 (збірка b41-beta2 (30.03.2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: 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()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--З'єднання(30708295)--Потік(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--З'єднання(19255406)--Потік(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--З'єднання(30708295)--Потік(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) значення ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--видалити з jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--З'єднання(14069849)--Потік(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--З'єднання(30708295)--Потік(Thread[main,5,main])--відключення
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Потік(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa Вихід із системи успішно
...
terminé ...
- рядки 2–5: з’єднання з SGBD із відповідними параметрами. Насправді, як показують журнали, Toplink створює 3 з’єднання з SGBD. Слід перевірити, чи пов’язана ця кількість із одним із значень конфігурації, що використовуються для пулу з’єднань JDBC:
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
- рядок 7: видалення таблиці [jpa01_personne]. Це нормально, оскільки файл [persistence.xml] вимагає очищення бази даних jpa.
- рядок 8: створення таблиці [jpa01_personne]. Помітно, що первинний ключ ID не має атрибута autoincrement.
- рядок 9: створення таблиці [SEQUENCE], яка вже існує, оскільки була створена під час попереднього виконання.
- рядки 10–13: Toplink повідомляє про помилку під час створення таблиці [SEQUENCE].
- рядки 15–18: Toplink очищає таблицю [SEQUENCE]. Після очищення таблиця [SEQUENCE] містить один запис (SEQ_NAME, SEQ_COUNT) із значеннями ('SEQ_GEN', 1).
- рядок 18: таблиця [jpa01_personne] очищена.
- рядки 19–20: Toplink пропускає єдиний рядок, у якому SEQ_NAME = 'SEQ_GEN', з таблиці [SEQUENCE], зі значення ('SEQ_GEN', 1) до значення ('SEQ_GEN', 51)
- рядок 21: Toplink отримує значення 51 із рядка ('SEQ_GEN', 51) таблиці [SEQUENCE].
- рядки 24–27: Toplink вставляє в таблицю [jpa01_personne] двох осіб — «Martin» та «Durant». Тут є загадка: первинні ключі цих двох рядків отримують значення 2 та 3, причому невідомо, як саме були отримані ці значення. Невідомо, чи значення SEQ_COUNT (51), отримане у рядку 21, було використано для чогось. Зазначимо, що значення версії рядків дорівнює 1, тоді як Hibernate починав з 0.
- рядок 28: Toplink генерує SELECT, щоб отримати всі рядки таблиці [jpa01_personne]
- рядки 29–30: рядки, що відображаються Java-клієнтом
- рядки 31–32: Toplink закриває з’єднання. Він повторить цю операцію для кожного з початково відкритих з’єднань.
Зрештою, ми не знаємо точно, яку роль відіграє таблиця [SEQUENCE], але, схоже, вона все ж має значення у формуванні значень первинного ключа ID. Якщо вибрати найдетальніший рівень журналів — FINEST — можна дізнатися трохи більше про роль таблиці [SEQUENCE].
<!-- журнали -->
<property name="toplink.logging.level" value="FINEST" />
Нижче ми навели лише ті записи, що стосуються внесення двох осіб до таблиці. Саме тут можна побачити механізм формування значень первинного ключа:
- рядок 4: бачимо, що число 51, отримане з таблиці [SEQUENCE] у рядку 2, використовується для визначення інтервалу значень первинного ключа: [2,51]
- рядок 5: перша особа отримує значення 2 для первинного ключа
- рядок 8: друга особа отримує значення 3 як первинний ключ
- рядок 12: показує управління версіями для першої особи
- рядок 17: те саме для другої особи
Рівень журналів [FINEST] також показує межі транзакцій, що генеруються Toplink. Аналіз цих журналів дозволяє побачити, що саме робить Toplink, і є чудовим засобом для розуміння об’єктно-реляційного мосту.
З вищесказаного слід винести наступне:
- що різні реалізації JPA генерують різні схеми баз даних. У цьому прикладі Hibernate та Toplink згенерували різні схеми.
- що рівні журналів FINE, FINER, FINEST у Toplink слід використовувати, коли потрібно з’ясувати, що саме робить Toplink.
2.1.15.4. Тест [Main]
Тепер виконуємо тест [Main]:
![]() |
- у [1]: усі тести пройшли успішно, крім тесту 11 [2]
- у [3]: рядок 376 — рядок коду, де сталося виключення
Код, що викликає виняток, такий:
} catch (RuntimeException e1) {
// виникла проблема
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
...
- рядок [3]: рядок, у якому сталося виключення. Ми маємо NullPointerException, що дозволяє припустити, що один із методів getCause у рядках 4 та 5 повернув покажчик null. Вираз на кшталт [e1.getCause().getCause()] передбачає, що ланцюжок винятків має 3 елементи [e1.getCause().getCause(), e1.getCause(), e1]. Якщо їх лише два, перший вираз спричинить виняток.
Ми змінюємо попередній код так, щоб він відображав лише два останні винятки з ланцюжка винятків:
} catch (RuntimeException e1) {
// виникла проблема
System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage());
try {
...
Під час виконання ми отримуємо такий результат:
...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
Цього разу тест 11 проходить успішно. Виведення інформації про виняток (рядки 6–10) було ініційовано кодом Java (рядок 3 у наведеному вище коді). Нагадаємо, що тест 11 поєднував у одній транзакції кілька операцій SQL, одна з яких завершилася невдало і мала спричинити відкат транзакції. Стан таблиці [jpa01_personne] до (рядок 3) і після тесту (рядок 12) є ідентичним, що свідчить про те, що відкат відбувся.
Тут слід зазначити важливий момент: реалізації JPA / Hibernate та JPA / Toplink не є на 100 % взаємозамінними. У цьому прикладі нам потрібно змінити код клієнта JPA, щоб уникнути помилки NullPointerException. Ми зіткнемося з цією проблемою пізніше, знову в контексті винятку.
2.1.16. Заміна SGBD на JPA / Toplink у реалізації
Повернемося до архітектури тестування нашого поточного проєкту:
![]() |
Раніше SGBD, що використовувався в [7], мав назву MySQL5. Разом з Oracle ми покажемо, як змінити SGBD. У будь-якому разі, зміна, яку потрібно внести в проєкт Eclipse, є простою (див. нижче): замінити файл конфігурації шару [1] на один із файлів із папки conf ([2] та [3]) цього проєкту.
![]() |
2.1.16.1. Oracle 10g Express
Oracle 10g Express представлено в додатках у розділі 5.7. Файл persistence.xml від Oracle для Toplink має такий вигляд:
<?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>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- персистентні класи -->
<class>entites.Personne</class>
<!-- властивості модуля збереження -->
<properties>
<!-- з'єднання JDBC -->
<property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
<property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="Oracle" />
<!-- сервер додатків -->
<property name="toplink.target-server" value="None" />
<!-- генерація схеми -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/oracle" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- журнали -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
Ця конфігурація ідентична тій, що була виконана для SGBD та MySQL5, за винятком таких деталей:
- рядки 11–14, які налаштовують зв’язок JDBC із базою даних
- рядок 20: який визначає цільовий SGBD
- рядок 25: який визначає папку для генерації скриптів SQL з DDL
Щоб виконати тест [InitDB]:
- запустіть SGBD Oracle
- додати файл conf/oracle/persistence.xml у META-INF/persistence.xml
- запустити додаток [InitDB]
У консолі та в перспективі [SQL Explorer] ми отримуємо такі результати:
![]() |
- [1]: вивід на консолі
- [2]: підключення [oracle-jpa] у SQL Explorer
- [3]: база даних jpa
- [4]: InitDB створив дві таблиці: JPA01_PERSONNE та SEQUENCE, як і у випадку з MySQL5. Іноді в [4] з’являються таблиці [BIN*]. Вони відповідають видаленим таблицям. Щоб побачити це явище, достатньо повторно запустити [InitDB]. Етап ініціалізації шару JPA передбачає очищення бази даних jpa, під час якого таблиця [JPA01_PERSONNE] видаляється:
![]() |
У [A] з’являється таблиця [BIN]. Oracle не видаляє остаточно таблицю, яка пройшла drop, а переміщує її до кошика [Recycle Bin]. Цю корзину можна переглянути за допомогою інструменту SQL Developer, описаного в розділі 5.7.4. У [B] можна очистити таблицю [JPA01_PERSONNE], яка знаходиться в кошику. Це очищає кошик [C]. Якщо в SQL Explorer оновити (правий клік / Refresh) таблиці, можна побачити, що таблиця BIN більше не існує ([D]).
- [5, 6]: структура та вміст таблиці [JPA01_PERSONNE]
- [7, 8]: структура та вміст таблиці [SEQUENCE]
Ось і все! Тепер читачеві пропонується запустити додаток [Main] на Oracle.
2.1.16.2. Інші SGBD
Про інші SGBD ми розповімо небагато. Потрібно просто повторити процедуру, яку ми виконували для Oracle. Зверніть увагу на наступні моменти:
- незалежно від того, який саме SGBD, Toplink завжди використовує одну й ту саму техніку для генерації значень первинного ключа ID таблиці [JPA01_PERSONNE]: він використовує таблицю [SEQUENCE], про яку йшлося вище.
- Toplink не розпізнає SGBD Firebird. Для таких випадків існує загальна база даних:
З цією загальною базою даних під назвою [Auto] тести з Firebird завершуються з помилками синтаксису SQL. Toplink використовує для первинного ключа ID тип SQL Number(10), який Firebird не розпізнає. Тому слід вибрати SGBD з типом SQL, який підтримується Firebird (у цьому прикладі). Так робиться в Apache Derby:
<!-- підключення JDBC -->
<property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
<!-- SGBD -->
<!--
TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
-->
<property name="toplink.target-database" value="Derby" />
...
- Toplink не може згенерувати вихідну схему бази даних для SGBD HSQLDB. Тобто директива:
<!-- генерація схеми -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
не працює для HSQLDB. Причиною цього є синтаксична помилка під час створення таблиці [jpa01_personne]:
[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Підключення(29775659)--Потік(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--З'єднання(29775659)--Потік(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Потік(Thread[main,5,main])--Виняток [TOPLINK-4002] (Oracle TopLink Essentials — 2.0 (збірка b41-beta2 (30.03.2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]
Рядок 4, синтаксис NOM VARCHAR(30) UNIQUE NOT NULL не приймається HSQL. Hibernate використовував такий синтаксис: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).
Загалом Hibernate виявився ефективнішим за Toplink у розпізнаванні SGBD, з якими проводилися тести, описані в цьому документі.
2.1.17. Висновок
Дослідження @Entity [Personne] на цьому завершується. З концептуальної точки зору зроблено досить мало: ми розглянули об’єктно-реляційний міст у найпростішому випадку: об’єкт @Entity <--> таблиця. Однак його вивчення дозволило нам представити інструменти, які ми використовуватимемо протягом усього документа. Це дасть нам змогу надалі трохи пришвидшити вивчення інших випадків об’єктно-реляційного мосту, які ми будемо розглядати:
- до попереднього @Entity [Personne] ми додамо поле adresse, змодельоване класом [Adresse]. Що стосується бази даних, ми розглянемо дві можливі реалізації. Об’єкти [Personne] та [Adresse] утворюють
- єдину таблицю [personne], що містить адресу
- дві таблиці [personne] та [adresse], пов’язані відношенням із зовнішнім ключем типу «один до одного».
- приклад відношення «один до багатьох», де таблиця [article] пов’язана з таблицею [categorie] за допомогою зовнішнього ключа
- Приклад відношення «багато-до-багатьох», де дві таблиці [personne] та [activite] пов’язані за допомогою таблиці з’єднання [personne_activite].
2.2. Приклад 2: відношення «один до одного» через включення
2.2.1. Схема бази даних
1 ![]() | 2 |
- у [1]: база даних (плагін Azurri Clay)
- у [2]: DDL, згенерована Hibernate для MySQL5
Таблиця [jpa02_personne] — це таблиця [jpa01_personne], розглянута раніше, до якої було додано адресу (рядки 12–18 у DDL).
2.2.2. Об’єкти @Entity, що представляють базу даних
Адреса особи буде представлена наступним класом [Adresse]:
package entites;
...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {
// поля
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
// конструктори
public Adresse() {
}
public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
}
// методи getter та setter
...
// toString
public String toString() {
return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
}
}
- головна інновація полягає в анотації @Embeddable у рядку 5. Клас [Adresse] не призначений для створення таблиці, тому він не має анотації @Entity. Анотація @Embeddable вказує, що клас призначений для вбудовування в об’єкт @Entity і, отже, у пов’язану з ним таблицю. Саме тому у схемі бази даних клас [Adresse] не відображається як окрема таблиця, а як частина таблиці, пов’язаної з @Entity [Personne].
@Entity [Personne] майже не змінився порівняно з попередньою версією: до нього просто додано поле adresse:
package entites;
...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@Embedded
private Adresse adresse;
// конструктори
public Personne() {
}
...
}
- зміна відбувається у рядках 33–34. Об’єкт [Personne] тепер має поле adresse типу Adresse. Це стосується POJO. Анотація @Embedded призначена для об’єктно-реляційного мосту. Вона вказує, що поле [Adresse adresse] має бути інкапсульоване в тій самій таблиці, що й об’єкт [Personne].
2.2.3. Середовище тестування
Ми проведемо тести, дуже схожі на ті, що розглядали раніше. Вони будуть виконуватися в такому контексті:
![]() |
Використовується реалізація JPA / Hibernate [6]. Проєкт тестів у Eclipse має такий вигляд:
![]() |
Проєкт Eclipse [1] відрізняється від попереднього лише кодом Java [2]. Середовище (бібліотеки — persistence.xml — СКБД — конфігураційні папки, DDL — скрипт Ant) є тим самим, що вже розглядалося раніше, зокрема в розділі 2.1.5. Це завжди буде актуально для майбутніх проектів Hibernate, і, за винятком окремих випадків, ми більше не будемо повертатися до цього середовища. Зокрема, файли persistence.xml, які налаштовують рівень JPA/Hibernate для різних SGBD, — це ті самі файли, що вже розглядалися раніше і знаходяться в папці <conf>.
Якщо у читача виникнуть сумніви щодо процедур, які слід виконати, йому пропонується повернутися до тих, що були розглянуті в попередньому дослідженні.
Проєкт Eclipse міститься у файлі [3] у папці з прикладами [4]. Його потрібно імпортувати.
2.2.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій, наведених у розділі 2.1.7, файл DDL, отриманий для SGBD MySQL5, має такий вигляд:
drop table if exists jpa02_hb_personne;
create table jpa02_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
Hibernate правильно визначив, що адреса особи має бути включена до таблиці, пов’язаної з @Entity Personne (рядки 11–17).
2.2.5. InitDB
Код для [InitDB] виглядає так:
package tests;
...
public class InitDB {
// константи
private final static String TABLE_NAME = "jpa02_hb_personne";
public static void main(String[] args) throws ParseException {
// Контекст збереження
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// отримуємо EntityManager на основі попереднього EntityManagerFactory
em = emf.createEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// запит
Query sql1;
// видалити елементи з таблиці PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
sql1.executeUpdate();
// створення осіб
Personne 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);
// створення адрес
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// взаємозв'язки «особа» <--> «адреса»
p1.setAdresse(a1);
p2.setAdresse(a2);
// збереження осіб
em.persist(p1);
em.persist(p2);
// відображення осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// завершення транзакції
tx.commit();
// завершення EntityManager
em.close();
// завершення EntityManagerFactory
emf.close();
// журнал
System.out.println("terminé...");
}
}
У цьому коді немає нічого нового. Все це вже зустрічалося раніше. Виконання [InitDB] разом із MySQL5 дає такі результати:
![]() |
![]() |
- [1]: вивід на консоль
- [2]: таблиця [jpa02_hb_personne] у перспективі SQL Explorer
- [3] та [4]: її структура та вміст.
2.2.6. Головна
Клас [Main] має такий вигляд:
package tests;
...
import entites.Adresse;
import entites.Personne;
@SuppressWarnings( { "unused", "unchecked" })
public class Main {
// константи
private final static String TABLE_NAME = "jpa02_hb_personne";
// Контекст збереження
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// спільні об’єкти
private static Personne p1, p2, newp1;
private static Adresse a1, a2, a3, a4, newa1, newa4;
public static void main(String[] args) throws Exception {
// отримуємо EntityManager з EntityManagerFactory
em = emf.createEntityManager();
// очищення бази даних
log("clean");clean();
// вивантаження таблиці
dumpPersonne();
// test1
log("test1"); test1();
// тест2
log("test2"); test2();
// тест3
log("test3"); test3();
// тест4
log("test4"); test4();
// тест5
log("test5");test5();
// кінець контексту збереження
if (em != null && em.isOpen())
em.close();
// закриття EntityManagerFactory
emf.close();
}
// отримати поточний EntityManager
private static EntityManager getEntityManager() {
...
}
// отримати новий EntityManager
private static EntityManager getNewEntityManager() {
...
}
// перегляд вмісту таблиці «Особа»
private static void dumpPersonne() {
...
}
// очистити BD
private static void clean() {
...
}
// журнали
private static void log(String message) {
...
}
// створення об’єктів
public static void test1() throws ParseException {
// контекст збереження
EntityManager em = getEntityManager();
// створення осіб
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// створення адрес
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// зв'язки «особа» <--> «адреса»
p1.setAdresse(a1);
p2.setAdresse(a2);
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збереження даних про осіб
em.persist(p1);
em.persist(p2);
// завершення транзакції
tx.commit();
// зливання даних
dumpPersonne();
}
// зміна об’єкта контексту
public static void test2() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшується кількість дітей p1
p1.setNbenfants(p1.getNbenfants() + 1);
// змінюється його сімейний стан
p1.setMarie(false);
// об'єкт p1 автоматично зберігається (перевірка змін)
// під час наступної синхронізації (commit або select)
// завершення транзакції
tx.commit();
// відображається нова таблиця
dumpPersonne();
}
// видалити об’єкт, що належить до контексту збереження
public static void test4() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалено приєднаний об’єкт p2
em.remove(p2);
// кінець транзакції
tx.commit();
// відображення нової таблиці
dumpPersonne();
}
// від'єднати, знову приєднати та змінити
public static void test5() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// p1 знову прив'язується до нового контексту
p1 = em.find(Personne.class, p1.getId());
// завершення транзакції
tx.commit();
// зміна адреси p1
p1.getAdresse().setVille("Paris");
// виводиться нова таблиця
dumpPersonne();
}
}
Знову ж таки, нічого нового. Вивід на консоль виглядає так:
Читачеві пропонується встановити зв’язок між результатами та кодом.
2.2.7. Реалізація JPA / Toplink
Зараз ми використовуємо реалізацію JPA / Toplink:
![]() |
Новий проект тестів у Eclipse виглядає так:
![]() |
Код Java ідентичний коду попереднього проєкту Hibernate. Середовище (бібліотеки — persistence.xml — СКБД — конфігураційні папки, DDL — скрипт Ant) є тим самим, що вже розглядалося в розділі 2.1.15.2. Це буде актуально й для майбутніх проектів Toplink, і, за винятком окремих випадків, ми більше не будемо повертатися до цього середовища. Зокрема, файли persistence.xml, які налаштовують рівень JPA/Toplink для різних SGBD, — це ті самі файли, що вже розглядалися і знаходяться в папці <conf>.
Якщо у читача виникнуть сумніви щодо процедур, які слід виконати, пропонуємо звернутися до тих, що розглядалися в попередньому дослідженні.
Проєкт Eclipse [3] знаходиться у папці з прикладами [4]. Його потрібно імпортувати.
Виконання [InitDB] разом із SGBD та MySQL5 дає такі результати:
![]() |
![]() |
- [1]: вивід на консоль
- [2]: таблиці [jpa02_tl_personne] та [SEQENCE] у перспективі SQL Explorer
- [3] та [4]: структура та вміст [jpa02_tl_personne].
Скрипти SQL, згенеровані в ddl/mysql5 [5], є такими:
create.sql
CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.3. Приклад 3: відношення «один до одного» через зовнішній ключ
2.3.1. -схема бази даних
1 ![]() | 2 |
- у [1]: база даних. Цього разу адреса особи розміщена в окремій таблиці [adresse]. Таблиця [personne] пов’язана з цією таблицею за допомогою зовнішнього ключа.
- у [2]: таблиця DDL, згенерована Hibernate для MySQL5:
- рядки 9–20: таблиця [adresse], яка буде пов’язана з класом [Adresse], що став об’єктом @Entity.
- рядок 10: первинний ключ таблиці [adresse]
- рядок 30: замість повної адреси в таблиці [personne] тепер міститься ідентифікатор [adresse_id] цієї адреси.
- рядки 34–38: особа (adresse_id) є зовнішнім ключем для адреси (id).
2.3.2. Об’єкти @Entity, що представляють базу даних
Особа з адресою тепер представлена наступним класом [Personne]:
package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
...
}
- рядки 32–34: адреса особи
- рядок 32: анотація @OneToOne позначає відношення «один до одного»: особа має щонайменше та щонайбільше одну адресу. Атрибут cascade = CascadeType.ALL означає, що будь-яка операція (persist, merge, remove) над @Entity [Personne] має поширюватися на @Entity [Adresse]. З точки зору контексту збереження em це означає наступне. Якщо p — це особа, яка має свою адресу:
- явна операція em.persist(p) спричинить неявну операцію em.persist(a)
- явна операція em.merge(p) спричинить неявну операцію em.merge(a)
- Явна операція em.remove(p) призведе до неявної операції em.remove(a)
- рядок 32: анотація @OneToOne позначає відношення «один до одного»: особа має щонайменше та щонайбільше одну адресу. Атрибут cascade = CascadeType.ALL означає, що будь-яка операція (persist, merge, remove) над @Entity [Personne] має поширюватися на @Entity [Adresse]. З точки зору контексту збереження em це означає наступне. Якщо p — це особа, яка має свою адресу:
Досвід показує, що такі неявні каскади не є панацеєю. Розробник зрештою забуває, що вони роблять. У коді можна віддати перевагу явним операціям. Існують різні типи каскадів. Анотацію @OneToOne можна було б написати так:
//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)
Атрибут cascade тут приймає як значення масив констант, що визначають типи бажаних каскадів.
Атрибут fetch=FetchType.LAZY вимагає від Hibernate завантажити залежність у останній момент. Коли ми додаємо список осіб до контексту персистентності, ми не обов’язково хочемо додавати туди їхні адреси. Наприклад, ця адреса може знадобитися лише для конкретної особи, обраної користувачем через веб-інтерфейс. Атрибут fetch=FetchType.EAGER, у свою чергу, вимагає негайного завантаження залежностей.
- (продовження)
- рядок 33: анотація @JoinColumn визначає зовнішній ключ, який має таблиця @Entity [Personne] у таблиці @Entity [Adresse]. Атрибут name визначає назву стовпця, який слугує зовнішнім ключем. Атрибут unique=true забезпечує відношення «один до одного»: у стовпці [adresse_id] не може бути двох однакових значень. Атрибут nullable=false гарантує, що кожна особа має адресу.
Адреса особи тепер представлена наступним @Entity [Adresse]:
package entites;
...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {
// поля
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
// конструктори
public Adresse() {
}
...
}
- рядок 4: клас [Adresse] стає об’єктом @Entity. Отже, він стане об’єктом таблиці в базі даних.
- рядки 9–12: як і будь-який об’єкт @Entity, [Adresse] має первинний ключ. Він отримав назву Id і має ті самі (стандартні) анотації, що й первинний ключ Id об’єкта @Entity [Personne].
- рядки 39–40: відношення «один до одного» з @Entity [Personne]. Тут є кілька нюансів:
- по-перше, поле personne не є обов’язковим. Воно дозволяє нам на основі адреси визначити єдину особу, яка має цю адресу. Якби ми не бажали такої зручності, поле personne не існувало б, і все одно все працювало б.
- Відношення «один до одного», що пов’язує дві сутності [Personne] та [Adresse], вже було налаштоване в @Entity [Personne]:
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
Щоб ці дві конфігурації «один до одного» не конфліктували між собою, одна з них розглядається як principale, а інша — як inverse. Саме цей зв’язок, позначений як principale, обробляється об’єктно-реляційним мостом. Інший зв’язок, позначений як inverse, не обробляється безпосередньо: він обробляється опосередковано через зв’язок principale. У @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
атрибут mappedBy визначає вищезазначене відношення «один до одного», а відношення inverse — це відношення «один до одного» з principale, яке визначається полем adresse сутності @Entity [Personne].
2.3.3. Проєкт Eclipse / Hibernate 1
Реалізація JPA, що використовується тут, належить до Hibernate. Проект Eclipse для тестування має такий вигляд:
![]() |
Проект [3] знаходиться у папці з прикладами [4]. Його потрібно імпортувати.
2.3.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій з розділу 2.1.7, файл DDL, отриманий для SGBD та MySQL5, є тим, що показано на початку цього розділу.
2.3.5. InitDB
Код для [InitDB] є таким:
package tests;
...
import entites.Adresse;
import entites.Personne;
public class InitDB {
// константи
private final static String TABLE_PERSONNE = "jpa03_hb_personne";
private final static String TABLE_ADRESSE = "jpa03_hb_adresse";
public static void main(String[] args) throws ParseException {
// Контекст збереження
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// отримуємо EntityManager на основі попереднього EntityManagerFactory
em = emf.createEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// запит
Query sql1;
// видалити елементи з таблиці PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// видалити елементи з таблиці ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// створення осіб
Personne 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);
// створення адрес
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// взаємозв’язки «особа» <--> «адреса»
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// збереження осіб та, відповідно, їхніх адрес
em.persist(p1);
em.persist(p2);
// та адреси a3 і a4, не пов’язані з особами
em.persist(a3);
em.persist(a4);
// відображення осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// відображення адрес
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
// завершення транзакції
tx.commit();
// завершення EntityManager
em.close();
// завершення EntityManagerFactory
emf.close();
// журнал
System.out.println("terminé...");
}
}
Ми коментуємо лише те, що представляє новий інтерес порівняно з тим, що вже було розглянуто:
- рядки 31–32: створюються дві особи
- рядки 34–37: створюються чотири адреси
- рядки 39–42: особи (p1, p2) пов’язуються з адресами (a1, a2). Адреси (a3, a4) є «сиротами». Жодна особа не посилається на них. Це передбачено кодом DDL. Якщо особа обов’язково має адресу, то зворотне не є правдою.
- рядки 44–45: зберігаються особи (p1, p2). Оскільки ми встановили атрибут cascade = CascadeType.ALL для відношення «один до одного», що пов’язує особу з її адресою, адреси (a1, a2) цих двох осіб також повинні отримати значення persist. Саме це ми й хочемо перевірити. Щодо «сирітських» адрес (a3, a4), ми змушені робити це явно (рядки 47–48).
- рядки 51–53: виведення таблиці осіб
- рядки 56–57: виведення таблиці адрес
Виконання [InitDB] разом із MySQL5 дає такі результати:
![]() |
![]() |
- [1]: вивід на консоль
- [2]: таблиці [jpa03_hb_*] у перспективі SQL Explorer
- [3]: таблиця осіб
- [4]: таблиця адрес. Усі вони тут є. Також слід звернути увагу на зв’язок між стовпцем [adresse_id] у таблиці [3] та стовпцем [id] у таблиці [4] (зовнішній ключ).
2.3.6. Головна
Клас [Main] містить шість тестів, які ми розглянемо.
2.3.6.1. Тест 1
Цей тест виглядає так:
// створення об’єктів
public static void test1() throws ParseException {
// контекст збереження
EntityManager em = getEntityManager();
// створення осіб
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// створення адрес
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// зв'язки «особа» <--> «адреса»
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збереження осіб
em.persist(p1);
em.persist(p2);
// та адреси a3 і a4, не пов'язані з особами
em.persist(a3);
em.persist(a4);
// кінець транзакції
tx.commit();
// відображення таблиць
dumpPersonne();
dumpAdresse();
}
Цей код взято з [InitDB]. Його результат такий:
Обидві таблиці заповнено.
2.3.6.2. Тест 2
Цей тест виглядає так:
// редагування об’єкта контексту
public static void test2() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// збільшується кількість дітей p1
p1.setNbenfants(p1.getNbenfants() + 1);
// змінюється сімейний стан
p1.setMarie(false);
// об'єкт p1 автоматично зберігається (перевірка змін)
// під час наступної синхронізації (commit або select)
// завершення транзакції
tx.commit();
// відображається нова таблиця
dumpPersonne();
}
Його результат такий:
- рядок 4: у особи p1 кількість дітей збільшилася на 1, а її версія змінилася з 0 на 1
2.3.6.3. Тест 4
Цей тест виглядає так:
// видалити об’єкт, що належить до контексту збереження
public static void test4() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалення приєднаного об’єкта p2
em.remove(p2);
// завершення транзакції
tx.commit();
// відображення нових таблиць
dumpPersonne();
dumpAdresse();
}
- рядок 9: видаляємо особу p2. Вона має каскадний зв’язок з адресою a2. Отже, адресу a2 також слід видалити.
Результат тесту 4 такий:
- особа p2, яка була присутня у рядку 3 тесту 1, більше не присутня у тесті 4
- те саме стосується її адреси a2, яка була в рядку 7 тесту 1, але відсутня в тесті 4.
2.3.6.4. Тест 5
Цей тест має такий вигляд:
// від'єднати, знову приєднати та змінити
public static void test5() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// p1 приєднується до нового контексту
p1 = em.find(Personne.class, p1.getId());
// зміна адреси p1
p1.getAdresse().setVille("Paris");
// завершення транзакції
tx.commit();
// відображення нових таблиць
dumpPersonne();
dumpAdresse();
}
- рядок 4: маємо новий контекст збереження, отже, порожній.
- рядок 9: ми додаємо туди особу p1. p1 шукається в базі даних, оскільки його немає в контексті. Елементи, що залежать від p1 (його адреса), не витягуються з бази даних, оскільки було записано:
@OneToOne(..., fetch=FetchType.LAZY)
Це концепція «lazy loading» або «завантаження в потрібний момент»: залежності об’єкта, що зберігається, завантажуються в пам’ять лише тоді, коли вони потрібні.
- рядок 11: ми змінюємо поле «місто» в адресі p1. Через наявність getAdresse, а також якщо адреса p1 ще не була в контексті персистентності, вона буде завантажена з бази даних.
- рядок 13: підтверджується транзакція, що призведе до синхронізації контексту збереження з базою даних. Контекст збереження виявить, що адреса особи p1 була змінена, і збереже її.
Виконання test5 дає такі результати:
- місто проживання особи p1 (рядок 3 тесту 4, рядок 10 тесту 5) дійсно змінилося з Анже (рядок 5 тесту 4) на Париж (рядок 12 тесту 5).
2.3.6.5. Тест 6
Цей тест виглядає так:
// видалити об’єкт «Адреса»
public static void test6() {
EntityTransaction tx = null;
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// початок транзакції
tx = em.getTransaction();
tx.begin();
// адресу a3 знову прив'язують до нового контексту
a3 = em.find(Adresse.class, a3.getId());
System.out.println(a3);
// видалення
em.remove(a3);
// кінець транзакції
tx.commit();
// вивід таблиці «Адреса»
dumpAdresse();
}
- рядок 5: ми перебуваємо в новому контексті збереження, отже, порожньому.
- рядок 10: ми вносимо адресу a3 у контекст збереження
- рядок 13: її видаляємо. Це була «сирота» (адреса, не пов’язана з жодною особою). Тому її можна видалити.
Результат виконання такий:
- адреса a3 з тесту 5 (рядок 6) зникла з адрес тесту 6 (рядки 11–12)
2.3.6.6. Тест 7
Цей тест виглядає так:
// відкат
public static void test7() {
EntityTransaction tx = null;
try {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// початок транзакції
tx = em.getTransaction();
tx.begin();
// адресу a1 прив'язують до нового контексту
newa1 = em.find(Adresse.class, a1.getId());
// адресу a4 знову прив'язують до нового контексту
newa4 = em.find(Adresse.class, a4.getId());
// робиться спроба їх видалення — має бути згенеровано виняток, оскільки неможливо видалити адресу, пов’язану з особою, що саме і є у випадку з newa1
em.remove(newa4);
em.remove(newa1);
// завершення транзакції
tx.commit();
} catch (RuntimeException e1) {
// виникла проблема
System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
.getCause());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// поточний контекст покинуто
em.clear();
}
// вивід даних — таблиця «Адреса» не мала змінитися через відкат
dumpAdresse();
}
- test7: перевіряється відкат транзакції
- рядок 6: ми перебуваємо в новому контексті збереження даних, отже, порожньому.
- рядок 11: додаємо адресу a1 до контексту збереження під посиланням newa1
- рядок 13: додаємо адресу a4 до контексту збереження під посиланням newa4
- рядки 15–16: видаляємо обидві адреси newa1 та newa4. newa1 — це адреса особи p1, а отже, у базі даних p1 посилається на newa1 за допомогою зовнішнього ключа. Отже, видалення newa1 завершиться невдачею та викличе виняток під час синхронізації контексту збереження даних при фіксації транзакції (рядок 18). Ця транзакція зазнає rollback (рядок 25), і, отже, обидві операції транзакції будуть скасовані. Отже, слід констатувати, що адреса newa4, яку можна було б легально видалити, не була видалена.
Виконання дає такий результат:
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- таблиця адрес тесту 7 (рядки 12–13) ідентична таблиці тесту 6 (рядки 4–5). Схоже, що відкат відбувся. Проте повідомлення про помилку в рядку 9 є загадкою і заслуговує на детальне вивчення. Схоже, що виняток, який стався, не є тим, якого очікували. Щоб розібратися в ситуації, потрібно перевести журнали Hibernate з log4j.properties у режим DEBUG:
# Параметр «Root logger»
log4j.rootLogger=ERROR, stdout
# Параметри ведення журналу в режимі Hibernate (INFO відображає лише повідомлення про запуск)
log4j.logger.org.hibernate=DEBUG
Тоді можна побачити, що коли адреса a1 була розміщена в контексті персистентності, Hibernate також розмістив там особу p1, ймовірно, через відношення «один до одного» @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
Хоча тут було запитано «LazyLoading», залежність [Personne] все ж завантажується негайно. Це, ймовірно, означає, що атрибут fetch=FetchType.LAZY тут не має сенсу. Далі ми бачимо, що під час фіксації транзакції Hibernate підготував не лише видалення адрес a1 та a4, а й збереження особи p1. І саме тут виникає виняток: оскільки для особи p1 встановлено каскадне збереження її адреси, Hibernate намагається зберегти також адресу a1, хоча вона щойно була видалена. Виняток генерує саме Hibernate, а не драйвер JDBC. Звідси й повідомлення у 9-му рядку вище. Крім того, можна помітити, що rollback у 25-му рядку ніколи не виконується, оскільки транзакція стала неактивною. Отже, тест у 24-му рядку запобігає виконанню rollback.
Отже, бажаної мети — продемонструвати відкат — досягнуто не було. Насправді жодна команда SQL не була відправлена до бази даних. Зазначимо кілька моментів:
- корисність увімкнення детальних журналів для розуміння того, що робить ORM
- хоча ORM може полегшити роботу розробника, він також може її ускладнити, приховуючи поведінку, про яку розробнику потрібно знати. У даному випадку — спосіб завантаження залежностей @Entity.
2.3.7. Проєкт Eclipse / Hibernate 2
Ми копіюємо та вставляємо проект Eclipse / Hibernate, щоб дещо змінити конфігурацію об’єктів @Entity:
![]() |
Проєкт [3] знаходиться у папці з прикладами [4]. Імпортуємо його.
Ми змінюємо лише @Entity [Adresse], щоб вона більше не мала зворотного відношення «один до одного» з @Entity [Personne]:
package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {
// поля
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
...
@Column(length = 20, nullable = false)
private String pays;
// @OneToOne(mappedBy = "адреса", fetch=FetchType.LAZY)
// private Personne personne;
// конструктори
public Adresse() {
}
- рядки 25–26: зворотний зв’язок @OneToOne видалено. Слід чітко розуміти, що зворотний зв'язок ніколи не є обов'язковим. Обов'язковим є лише основний зв'язок. Зворотний зв'язок може використовуватися для зручності. У даному випадку він дозволяв просто отримати власника адреси. Зворотний зв'язок завжди можна замінити запитом JPQL. Саме це ми продемонструємо в наступному прикладі.
Тестові програми наведено без змін. Нас цікавить лише тест 7 — той, у якому ми бачили, як працює обернена відношення «один до одного». Крім того, ми додаємо тест 8, щоб показати, як без оберненої відношення «Адреса -> Особа» все одно можна отримати особу, яка має таку-то адресу.
Тест 7 не змінюється. Його виконання тепер дає такі результати (журнали вимкнені):
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- Цього разу ми дійсно отримали очікуване виключення: те, що було згенеровано драйвером Jdbc, оскільки ми спробували видалити з таблиці [adresse] рядок, на який посилається чужий ключ рядка з таблиці [personne]. Рядок [10] чітко вказує на причину помилки.
- Откат відбувся успішно: за результатами тесту 7 таблиця [adresse] (рядки 12–13) відповідає тій, що була за результатами тесту 6 (рядки 4–5).
У чому полягає відмінність від тесту 7 попереднього проєкту Eclipse? Чому тут виникає виняток Jdbc, якого не було під час попереднього тесту? Оскільки @Entity [Adresse] більше не має зворотного відношення «один до одного» з @Entity [Personne], Hibernate обробляє її ізольовано. Коли адреса newa1 була додана до контексту персистентності, Hibernate не додав до цього контексту особу p1, яка має цю адресу. Отже, видалення адрес newa1 та newa4 відбулося без наявності сутностей Personne у контексті.
Тепер, як на основі адреси newa1 можна визначити особу p1, яка має цю адресу? Це цілком слушне запитання. Відповідь на нього міститься в наступному тесті № 8:
// один-до-одного зворотний зв'язок
// реалізована за допомогою запиту JPQL
public static void test8() {
EntityTransaction tx = null;
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// початок транзакції
tx = em.getTransaction();
tx.begin();
// адресу a1 знову прив’язують до нового контексту
newa1 = em.find(Adresse.class, a1.getId());
// отримано дані про власника цієї адреси
Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
.getSingleResult();
// відображаються
System.out.println("adresse=" + newa1);
System.out.println("personne=" + p1);
// кінець транзакції
tx.commit();
}
- рядок 6: новий порожній контекст збереження
- рядки 8–9: початок транзакції
- рядок 11: адреса a1 вноситься до контексту збереження та посилається на неї newa1.
- рядок 13: за допомогою запиту JPQL отримується особа p1 з адресою newa1. Відомо, що [Personne] та [Adresse] пов’язані відношенням із зовнішнім ключем. У класі [Personne] саме поле [adresse], що має анотацію @OneToOne, реалізує цей зв’язок. Запис JPQL «select p from Personne p join p.adresse a» здійснює з'єднання між таблицями [personne] та [adresse]. Еквівалент SQL, згенерований у консолі Hibernate (див. приклади в розділі 2.1.12), має такий вигляд:
Чітко видно з'єднання двох таблиць. Кожна особа тепер пов'язана зі своєю адресою. Залишається уточнити, що нас цікавить лише адреса newa1. Запит набуває такого вигляду: «select p from Personne p join p.adresse a where a.id=:adresseId». Зверніть увагу на використання псевдонімів p та a. У запитах JPQL псевдоніми використовуються дуже активно. Так, вираз «from Personne p join p.adresse a» призводить до того, що особа представлена псевдонімом p, а її адреса (p.adresse) — псевдонімом a. Операція обмеження «where a.id=:adresseId» обмежує запитувані рядки лише тими особами p, які мають значення:adresseId як ідентифікатор їхньої адреси a. :adresseId називається параметром, а оператор JPQL — параметризованим оператором JPQL. Під час виконання цей параметр повинен отримати значення. Це метод
дозволяє присвоїти значення параметру, ідентифікованому за його ім’ям. Зазначимо, що setParameter повертає об’єкт Query, так само як і метод createQuery. Отже, можна послідовно викликати методи [em.createQuery(...).setParameter(...).getSingleResult(...)], оскільки методи [setParameter, getSingleResult] є методами інтерфейсу Query. Метод [getSingleResult] використовується для запитів Select, які повертають лише один результат. Саме так і є в даному випадку.
- рядки 16–17: для перевірки виводиться адреса newa1 та особа p1, яка має цю адресу.
Отримано такий результат:
Він є правильним. З цього прикладу можна зробити висновок, що зворотний відношення «один до одного» між @entity [Adresse] та @entity [Personne] не було обов’язковим. Досвід показав, що його видалення призвело до більш передбачуваної поведінки коду. Так буває часто.
2.3.8. Консоль Hibernate
У попередньому тесті 8 використовувалася команда JPQL для об’єднання сутностей Personne та Adresse. Хоча мови Hibernate, такі як JPQL, JPA або HQL, аналогічні мові SQL, вони вимагають вивчення, і консоль Hibernate чудово підходить для цього. Ми вже використовували її в розділі 2.1.12 для роботи з однією таблицею. Тепер повторимо цей процес, щоб попрацювати з двома таблицями, пов’язаними відношенням із зовнішнім ключем.
Створимо консоль Hibernate для нашого поточного проєкту Eclipse:
![]() |
- [1]: переходимо до перспективи [Hibernate Console] (Window / Open Perspective / Other)
- [2]: створюємо нову конфігурацію
- за допомогою кнопки [4], вибираємо Java-проект, для якого створюється конфігурація Hibernate. Її назва відображається в [3].
- У [5] ми задаємо бажану назву для цієї конфігурації. Тут ми використали назву Java-проєкту.
- у файлі [6] ми вказуємо, що використовуємо конфігурацію JPA, щоб програма знала, що їй потрібно обробити файл [META-INF/persistence.xml]
- у [7]: у цьому файлі [META-INF/persistence.xml] ми вказуємо, що слід використовувати модуль збереження даних під назвою jpa.
- У файлі [8] ми підтверджуємо конфігурацію.
Далі потрібно запустити файл SGBD. Тут йдеться про файл MySQL5.
![]() |
- у [1]: створена конфігурація має дерево з трьома гілками
- у [2]: гілка [Configuration] містить перелік об’єктів, які консоль використовувала для налаштування: у даному випадку це @Entity Personne та Adresse.
- у [3]: Session Factory — це поняття Hibernate, близьке до EntityManager з JPA. Воно забезпечує об’єктно-реляційний міст завдяки об’єктам гілки [Configuration]. У [3] представлені об’єкти контексту персистентності, тут знову @Entity Personne та Adresse.
- У [4]: база даних, доступ до якої здійснюється за допомогою конфігурації, що міститься в [persistence.xml]. Тут містяться таблиці [jpa04_hb_*], згенеровані нашим поточним проєктом Eclipse.
![]() |
- у [1] створюється редактор HQL
- у редакторі HQL,
- у [2] вибираємо конфігурацію Hibernate, яку потрібно використовувати, якщо їх декілька (як у цьому випадку)
- у [3] вводимо команду JPQL, яку хочемо виконати, у даному випадку — команду JPQL з тесту 8
- у [4] — виконуємо її
- у [5] отримуємо результати запиту у вікні [Hibernate Query Result].
- у [6] у вікні [Hibernate Dynamic SQL preview] можна переглянути запит SQL, який було виконано.
Інший спосіб отримати той самий результат:
![]() |
- у [1]: команда JPQL, що виконує з'єднання сутностей Personne та Adresse. [ref1] називає цю форму «тета-з'єднанням».
- у [2]: еквівалент SQL
- у [3]: результат
Третя форма, яку підтримує лише Hibernate (HQL):
![]() |
- у [1]: команда HQL. JPQL не підтримує нотацію p.adresse.id. Він підтримує лише один рівень опосередкування.
- у [2]: еквівалент SQL. Бачимо, що він уникає з'єднання таблиць.
- у [3]: результат
Ось інші приклади:
![]() |
- у [1]: список осіб із їхніми адресами
- у [2]: еквівалент SQL.
- у [3]: результат
![]() |
- у [1]: список адрес із їхніми власниками, якщо такі є, або жодного, якщо їх немає (правий зовнішній з'єднання: суть Adresse, яка надасть рядки, що не пов'язані з Personne, знаходиться праворуч від ключового слова join).
- у [2]: еквівалент SQL.
- у [3]: результат
Слід зауважити, що лише суть Personne має зв’язок із сутністю Adresse. Зворотне вже не є правдою, оскільки було видалено зворотний зв’язок «один до одного» під назвою personne в об’єкті Adresse. Якби цей зворотний зв’язок існував, можна було б написати:
![]() |
- у [1]: список адрес із їхніми власниками, якщо такі є, або без них, якщо їх немає (лівий зовнішній з’єднання: суть Adresse, яка надасть рядки, що не пов’язані з Personne, знаходиться ліворуч від ключового слова join).
- у [2]: еквівалент SQL.
- у [3]: результат
Ми настійно рекомендуємо читачеві потренуватися у використанні мови JPQL за допомогою консолі Hibernate.
2.3.9. Реалізація JPA / Toplink
Зараз ми використовуємо реалізацію JPA / Toplink:
![]() |
Новий проект тестів у Eclipse має такий вигляд:
![]() |
Код Java ідентичний коду попереднього проекту Hibernate. Середовище (бібліотеки — persistence.xml — СКБД — папки conf, ddl — скрипт ant) є тим самим, що розглядалося в розділі 2.1.15.2. Проєкт Eclipse [3] знаходиться у папці прикладів [4]. Його потрібно імпортувати.
Файл <persistence.xml> змінено в одному місці, а саме в частині оголошених об’єктів:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- провайдер -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- персистентні класи -->
<class>entites.Personne</class>
<class>entites.Adresse</class>
<!-- властивості одиниці збереження -->
...
- рядки 5 і 6: обидва керовані об’єкти
Виконання [InitDB] разом із SGBD та MySQL5 дає такі результати:
![]() |
У [1], вивід на консоль, у [2] — дві згенеровані таблиці [jpa04_tl], у [3] — згенеровані скрипти SQL. Їхній вміст такий:
create.sql
CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.4. Приклад 4: відношення «один до багатьох»
2.4.1. -схема бази даних
1 ![]() | 2 |
- у [1] — база даних, а в [2] — її DDL (MySQL5)
Стаття A(id, версія, назва) належить саме до однієї категорії C(id, версія, назва). Категорія C може містити 0, 1 або кілька статей. Маємо відношення «один до багатьох» (Категорія -> Стаття) та зворотне відношення «багато до одного» (Стаття -> Категорія). Цей зв’язок реалізується за допомогою зовнішнього ключа, який таблиця [article] має до таблиці [categorie] (рядки 24–28 таблиці DDL).
2.4.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: множина (набір) статей категорії
- рядок 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].
2.4.3. Проєкт Eclipse / Hibernate 1
Реалізація JPA, яка використовується тут, належить до Hibernate. Проект Eclipse для тестування має такий вигляд:
![]() |
Проект [3] знаходиться у папці з прикладами [4]. Його потрібно імпортувати.
2.4.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій з розділу 2.1.7, файл DDL, отриманий для SGBD та MySQL5, є тим, що показано на початку цього прикладу, у розділі 2.4.1.
2.4.5. InitDB
Код [InitDB] є таким:
package tests;
...
public class InitDB {
// константи
private final static String TABLE_ARTICLE = "jpa05_hb_article";
private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";
public static void main(String[] args) {
// Контекст збереження
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// отримано EntityManager на основі попереднього EntityManagerFactory
em = emf.createEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// запит
Query sql1;
// видалити елементи з таблиці ARTICLE
sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
sql1.executeUpdate();
// видалити елементи з таблиці CATEGORIE
sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
sql1.executeUpdate();
// створити три категорії
Categorie categorieA = new Categorie();
categorieA.setNom("A");
Categorie categorieB = new Categorie();
categorieB.setNom("B");
Categorie categorieC = new Categorie();
categorieC.setNom("C");
// створити 3 статті
Article articleA1 = new Article();
articleA1.setNom("A1");
Article articleA2 = new Article();
articleA2.setNom("A2");
Article articleB1 = new Article();
articleB1.setNom("B1");
// прив’язати їх до відповідних категорій
categorieA.addArticle(articleA1);
categorieA.addArticle(articleA2);
categorieB.addArticle(articleB1);
// зберегти категорії та каскадно (вставити) статті
em.persist(categorieA);
em.persist(categorieB);
em.persist(categorieC);
// відобразити категорії
System.out.println("[categories]");
for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
System.out.println(p);
}
// відображення статей
System.out.println("[articles]");
for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
System.out.println(p);
}
// завершення транзакції
tx.commit();
// завершення EntityManager
em.close();
// завершення EntityMangerFactory
emf.close();
// журнал
System.out.println("terminé...");
}
}
- рядки 22–27: таблиці [article] та [categorie] очищаються. Слід зауважити, що необхідно починати з тієї таблиці, яка містить зовнішній ключ. Якби ми почали з таблиці [categorie], то видалили б категорії, на які посилаються рядки таблиці [article], а це призвело б до відмови з боку SGBD.
- рядки 29–34: створюються три категорії A, B, C
- рядки 36–41: створюються три статті A1, A2, B1 (літера вказує на категорію)
- рядки 43–45: ці 3 товари розміщуються у відповідних категоріях
- рядки 47–49: три категорії додаються до контексту збереження. Завдяки каскаду «Категорія → Стаття» їхні статті також будуть розміщені там. Отже, усі створені об’єкти тепер знаходяться в контексті збереження.
- рядки 50–59: контекст збереження використовується для отримання списку категорій та статей. Відомо, що це спричинить синхронізацію контексту з базою даних. Саме в цей момент категорії та статті будуть записані у відповідні таблиці.
Виконання [InitDB] разом із MySQL5 дає такі результати:
![]() |
- [1]: вивід на консоль
- [2]: таблиці [jpa05_hb_*] у перспективі SQL Explorer
- [3]: таблиця категорій
- [4]: таблиця статей. Зверніть увагу на зв’язок між [categorie_id] у [4] та [id] у [3] (зовнішній ключ).
2.4.6. Головна
Клас [Main] об’єднує тести, які ми розглядаємо, за винятком тестів 1 і 2, які використовують код з [InitDB] для ініціалізації бази даних.
2.4.6.1. Тест 3
Цей тест виглядає так:
// пошук конкретного елемента
public static void test3() {
// новий контекст збереження
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// завантаження категорії
Categorie categorie = em.find(Categorie.class, categorieA.getId());
// відображення категорії та пов'язаних з нею товарів
System.out.format("Articles de la catégorie %s :%n", categorie);
for (Article a : categorie.getArticles()) {
System.out.println(a);
}
// завершення транзакції
tx.commit();
}
- рядок 4: маємо новий контекст персистентності, отже, він порожній
- рядки 6–7: початок транзакції
- рядок 9: категорія A переноситься з бази даних у контекст персистентності
- рядок 11: відображається категорія A
- рядки 12–14: відображаються товари категорії A. Тут простежується користь зворотного зв’язку OneToMany — товари з @Entity Categorie. Її наявність дозволяє уникнути виконання запиту JPQL для отримання товарів категорії A. Щоб отримати їх, використовується метод get поля articles.
Результати такі:
- рядок 20: категорія A
- рядки 21–22: обидва товари категорії А
2.4.6.2. Test4
Цей тест виглядає так:
// видалити товар
@SuppressWarnings("unchecked")
public static void test4() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// завантаження товару A1
Article newarticle1 = em.find(Article.class, articleA1.getId());
// видалення статті A1 (наразі не завантажено жодної категорії)
em.remove(newarticle1);
// toplink: товар потрібно видалити з категорії, інакше test6 дасть збій
// hibernate: це не потрібно
newarticle1.getCategorie().getArticles().remove(newarticle1);
// завершення транзакції
tx.commit();
// зливання статей
dumpArticles();
}
- тест 4 видаляє статтю A1
- рядок 5: починаємо з нового порожнього контексту
- рядок 10: стаття A1 переноситься в контекст збереження. Там вона буде посилатися на newarticle1.
- рядок 12: вона видаляється з контексту
- рядок 15: категорії A, B і C та статті A1, A2 і B1, якщо вони більше не є постійними, все ж залишаються в пам’яті. Вони просто від’єднані від контексту збереження. Елемент A1, який входить до категорії A, видаляється з неї. Це дозволить згодом знову приєднати категорію A до контексту збереження. Якщо цього не зробити, категорія A буде приєднана до набору елементів, один із яких було видалено. Це, здається, не заважає роботі Hibernate, але призводить до збою Toplink.
- рядок 19: виводимо всі статті, щоб перевірити, чи зник A1.
Результати такі:
Стаття A1 дійсно зникла.
2.4.6.3. Тест 5
Цей тест полягає в наступному:
// зміна 1 статті
public static void test5() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// зміна articleA2
articleA2.setNom(articleA2.getNom() + "-");
// articleA2 повернено в контекст збереження
em.merge(articleA2);
// завершення транзакції
tx.commit();
// вивантаження товарів
dumpArticles();
}
- тест 5 змінює назву статті A2
- рядок 4: починаємо з нового, порожнього контексту
- рядок 9: змінюємо назву відокремленого елемента A2, який стане «A2-».
- рядок 11: відокремлений елемент A2 знову приєднується до контексту збереження. Зазначимо, що A2 залишається відокремленим об’єктом. Саме об’єкт em.merge (articleA2) відтепер є частиною контексту збереження. Цей об’єкт тут не було збережено у змінній, як це зазвичай робиться. Тому він є недоступним.
- рядок 13: синхронізація контексту збереження з базою даних. Стаття A2 буде змінена в базі даних, і її номер версії зміниться з N на N+1. Відокремлена версія в пам’яті articleA2 більше не є дійсною. Те саме стосується відокремленого об’єкта, що представляє категорію A, оскільки він містить articleA2 серед своїх статей.
- рядок 15: відображаються всі позиції для перевірки зміни назви позиції A2
Результати такі:
Назва позиції A2 дійсно змінилася.
2.4.6.4. Тест 6
Цей тест полягає в наступному:
// зміна 1 категорії та її товарів
public static void test6() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// завантаження категорії
categorieA = em.find(Categorie.class, categorieA.getId());
// перелік товарів категорії А
for (Article a : categorieA.getArticles()) {
a.setNom(a.getNom() + "-");
}
// зміна назви категорії
categorieA.setNom(categorieA.getNom() + "-");
// завершення операції
tx.commit();
// вивантаження категорій та товарів
dumpCategories();
dumpArticles();
}
- тест 6 змінює назву категорії A та всіх її статей
- рядок 4: починаємо з нового, порожнього контексту
- рядок 9: шукаємо категорію A в базі даних. Не виконуємо merge для відокремленого об’єкта categorieA, оскільки відомо, що він має посилання на статтю A2, яка стала застарілою. Тому починаємо з нуля.
- рядки 11–12: змінюємо назви всіх статей категорії A. Знову використовуємо зворотний зв’язок OneToMany за допомогою методу getArticles.
- рядок 15: назва категорії також змінюється
- рядок 17: кінець транзакції. Виконується синхронізація контексту з базою даних. Усі об’єкти контексту, які були змінені, будуть оновлені в базі даних.
- рядки 21–22: для перевірки відображаються товари та категорії
Результати такі:
Артикул A2 знову змінив назву, а також категорію A.
2.4.6.5. Test7
Цей тест виглядає так:
// видалення категорії
public static void test7() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// збереження даних catégorieB та каскадне об'єднання (merge) пов'язаних товарів
Categorie mergedcategorieB = em.merge(categorieB);
// видалення категорії та каскадне видалення (delete) пов'язаних товарів
em.remove(mergedcategorieB);
// завершення транзакції
tx.commit();
// вивантаження категорій та товарів
dumpCategories();
dumpArticles();
}
- тест 7 видаляє категорію B і, відповідно, статті в ній
- рядок 4: починаємо з нового порожнього контексту
- рядок 9: категорія B існує в пам'яті як об'єкт, відокремлений від контексту збереження. Її знову інтегрують (merge) у контекст збереження. Внаслідок цього її статті (стаття B1) піддаються операції merge і, отже, знову інтегруються в контекст збереження.
- рядок 11: тепер, коли категорія B знаходиться в контексті, її можна видалити (remove). За каскадним принципом її статті також піддадуться операції remove. Ця операція можлива саме тому, що операція merge у рядку 9 повернула їх до контексту збереження.
- рядок 13: кінець транзакції. Контекст буде синхронізовано. Об’єкти контексту, що пройшли операцію remove, будуть видалені з бази даних.
- рядки 15–16: для перевірки відображаються товари та категорії
Результати такі:
Категорія B та товар B1 дійсно зникли.
2.4.6.6. Test8
Цей тест виглядає так:
// запити
@SuppressWarnings("unchecked")
public static void test8() {
// новий контекст збереження даних
EntityManager em = getNewEntityManager();
// транзакція
EntityTransaction tx = em.getTransaction();
tx.begin();
// список товарів категорії A
List articles = em
.createQuery(
"select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
.getResultList();
// перегляди товарів
System.out.println("Articles de la catégorie A");
for (Object a : articles) {
System.out.println(a);
}
// завершення транзакції
tx.commit();
}
- тест 7 демонструє, як отримати статті з категорії без використання зворотного зв’язку. Це свідчить про те, що він не є обов’язковим.
- рядок 4: починаємо з нового, порожнього контексту
- рядок 10: запит JPQL, який запитує всі статті категорії, назва якої починається на літеру A
- рядки 15–17: відображення результату запиту.
Результати такі:
2.4.7. Проєкт Eclipse / Hibernate 2
Ми копіюємо та вставляємо проект Eclipse / Hibernate, щоб уточнити поняття «головного зв’язку» та «зворотного зв’язку», які ми створили на основі анотації @ManyToOne (головний) для @Entity [Article] та зворотного зв’язку @OneToMany (зворотний) для @Entity [Categorie]. Ми хочемо показати, що якщо останній зв’язок не оголошено оберненим до іншого, то схема, згенерована для бази даних, буде зовсім іншою, ніж та, що була згенерована раніше.
![]() |
У [1] — новий проєкт Eclipse. У файлі [2] — код Java, у файлі [3] — скрипт ant, який згенерує схему SQL бази даних. Проєкт знаходиться [4] у папці з прикладами [5]. Ми його імпортуємо.
Ми змінюємо лише @Entity [Categorie], щоб її відношення @OneToMany з @Entity [Article] більше не була визначена як обернена до відношення @ManyToOne, яке має @Entity [Article] з @Entity [Categorie]:
...
@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) Категорія (one) -> Товар (many)
// реалізовано за допомогою таблиці з'єднання Categorie_Article, щоб з категорії
// можна було перейти до статей цієї категорії
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
// виробників
...
- рядки 18–22: ми хочемо зберегти можливість пошуку статей певної категорії за допомогою відношення @OneToMany у рядку 21. Але ми хочемо з’ясувати вплив атрибута mappedBy, який робить відношення оберненим до головного відношення, визначеного в іншому місці, в іншому @Entity. Тут mappedBy було видалено.
Ми виконуємо завдання ant-DLL (див. розділ 2.1.7) з атрибутами SGBD та MySQL5. Отримана схема має такий вигляд:
![]() |
Слід звернути увагу на наступні моменти:
- була створена нова таблиця [categorie_article] [1]. Раніше її не існувало.
- це таблиця з'єднання між таблицями [categorie] [2] та [article] [3]. Якщо об’єкти «Артикул» a1, a2 належать до категорії c1, у таблиці з’єднання будуть такі рядки:
де c1, a1, a2 — це первинні ключі відповідних об’єктів.
- Таблиця з'єднання [categorie_article] [1] була створена Hibernate для того, щоб на основі об'єкта Categorie c можна було знайти об'єкти Article a, що належать до c. Саме відношення @OneToMany зумовило створення цієї таблиці. Оскільки ми не оголосили її оберненою до головного зв’язку @ManyToOne сутності @Entity Article, Hibernate не знав, що може використовувати цей головний зв’язок для отримання статей з категорії c. Тому він впорався з цим іншим способом.
- На цьому прикладі легше зрозуміти поняття відносин principale та inverse. Одна (обернена) використовує властивості іншої (головної).
Схема SQL цієї бази даних для MySQL5 виглядає наступним чином:
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D26D17756;
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D424C61C9;
alter table jpa06_hb_article
drop
foreign key FK4547168FECCE8750;
drop table if exists jpa05_hb_categorie;
drop table if exists jpa05_hb_categorie_jpa06_hb_article;
drop table if exists jpa06_hb_article;
create table jpa05_hb_categorie (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
primary key (id)
) ENGINE=InnoDB;
create table jpa05_hb_categorie_jpa06_hb_article (
jpa05_hb_categorie_id bigint not null,
articles_id bigint not null,
primary key (jpa05_hb_categorie_id, articles_id),
unique (articles_id)
) ENGINE=InnoDB;
create table jpa06_hb_article (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
categorie_id bigint not null,
primary key (id)
) ENGINE=InnoDB;
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id),
add constraint FK79D4BA1D26D17756
foreign key (jpa05_hb_categorie_id)
references jpa05_hb_categorie (id);
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D424C61C9 (articles_id),
add constraint FK79D4BA1D424C61C9
foreign key (articles_id)
references jpa06_hb_article (id);
alter table jpa06_hb_article
add index FK4547168FECCE8750 (categorie_id),
add constraint FK4547168FECCE8750
foreign key (categorie_id)
references jpa05_hb_categorie (id);
- рядки 19–24 — створення таблиці [categorie], а рядки 33–39 — створення таблиці [article]. Зазначимо, що вони ідентичні тим, що були в попередньому прикладі.
- рядки 26–31: створення таблиці з’єднання [categorie_article] через наявність неінверсної відношення @OneToMany для @Entity Categorie. Записи цієї таблиці мають тип [c,a], де c є первинним ключем категорії c, а a — первинним ключемтовару a, що належить до категорії c. Первинний ключ цієї таблиці з’єднання складається з двох об’єднаних первинних ключів [c,a] (рядок 29).
- рядки 41–45: обмеження зовнішнього ключа з таблиці [categorie_article] до таблиці [categorie]
- рядки 47–51: обмеження зовнішнього ключа з таблиці [categorie_article] до таблиці [article]
- рядки 53–57: обмеження зовнішнього ключа з таблиці [article] до таблиці [categorie]
Читачеві пропонується виконати тести [InitDB] та [Main]. Вони дають ті самі результати, що й раніше. Однак схема бази даних є надлишковою, і продуктивність погіршиться порівняно з попередньою версією. Безсумнівно, слід детальніше дослідити це питання щодо зворотних та основних зв’язків, щоб з’ясувати, чи не спричиняє нова конфігурація додаткових конфліктів через те, що для представлення одного й того самого використовуються два незалежні зв’язки: зв’язок «багато-до-одного» між таблицею [article] та таблицею [categorie].
2.4.8. Реалізація JPA / Toplink — 1
Тепер ми використовуємо реалізацію JPA / Toplink:
![]() |
Проєкт Eclipse з Toplink є копією проєкту Eclipse з Hibernate, версія 1:
![]() |
Код Java ідентичний коду попереднього проєкту Hibernate — версія 1. Середовище (бібліотеки — persistence.xml — СКБД — папки conf, ddl — скрипт ant) є тим самим, що розглядалося в розділі 2.1.15.2. Проект Eclipse знаходиться під назвою [3] у папці прикладів [4]. Його потрібно імпортувати.
Файл <persistence.xml> [2] змінено в одному місці, а саме в частині оголошених сутностей:
...
<!-- постійні класи -->
<class>entites.Categorie</class>
<class>entites.Article</class>
...
- рядки 3 і 4: обидва керовані об’єкти
Виконання [InitDB] разом із SGBD та MySQL5 дає такі результати:
![]() |
У [1], вивід на консоль; у [2] — дві згенеровані таблиці [jpa05_tl]; у [3] — згенеровані скрипти SQL. Їхній вміст такий:
create.sql
CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
Виконання [Main] проходить без помилок.
2.4.9. Реалізація JPA / Toplink - 2
Цей проект Eclipse створено шляхом копіювання попереднього. Оскільки він реалізовано з використанням Hibernate, атрибут mappedBy видалено з відношення @OneToMany сутності @Entity Categorie.
@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {
// поля
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private int version;
@Column(length = 30)
private String nom;
// необернена реляція OneToMany (відсутність mappedby) Категорія (one) ->
// Стаття (many)
// реалізована за допомогою таблиці з'єднання Categorie_Article, щоб на основі
// категорії
// можна було отримати кілька товарів
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
Схема SQL, згенерована для MySQL5, має такий вигляд:
create.sql
CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- рядок 2: таблиця з'єднання, що реалізує попередню неінверсну реляцію @OneToMany.
Виконання [InitDB] проходить без помилок, але виконання [Main] завершується з помилкою на тесті 7 із такими записами в журналі (FINEST):
- рядок 3: merge у категорії B
- рядок 4: залежний елемент B1 додано до контексту
- рядок 5: те саме стосується самої категорії B
- рядок 6: remove у категорії B
- рядок 7: remove для товару B1 (каскадним способом)
- рядок 8: код Java запитує commit з транзакції
- рядок 9: починається транзакція — отже, вона, мабуть, ще не розпочалася.
- рядок 10: товар B1 буде видалено операцією DELETE у таблиці [article]. Саме в цьому полягає проблема. Таблиця з'єднання [categorie_article] має посилання на рядок B1 у таблиці [article]. Видалення запису B1 з таблиці [article] призведе до порушення обмеження зовнішнього ключа.
- рядки 13 і далі: виникає виняток
Який висновок можна зробити?
- Знову маємо проблему сумісності між Hibernate та Toplink: Hibernate успішно пройшов цей тест
- Toplink погано справляється з ситуацією, коли дві відносини є фактично оберненими одна до одної, але одна з них не оголошена як головна, а інша — як обернена. З цим можна змиритися, оскільки такий випадок фактично є помилкою конфігурації. У нашому прикладі таблиця [article] не має зв’язку з таблицею з’єднання [categorie_article]. Тож цілком природно, що під час операції з таблицею [article] Toplink не намагається працювати з таблицею [categorie_article].
2.5. Приклад 5: відношення «багато-до-багатьох» з явною таблицею з’єднання
2.5.1. Схема бази даних
![]() |
- у [1], база даних MySQL5
Ми вже знайомі з таблицями [personne], [2] та [adresse], [3]. Їх було розглянуто в розділі 2.3.1. Ми розглядаємо варіант, у якому адреса особи є предметом окремої таблиці [adresse] та [3]. У таблиці [personne] зв’язок, що пов’язує особу з її адресою, реалізовано за допомогою обмеження зовнішнього ключа.
Особа займається певними видами діяльності. Ці види діяльності містяться в таблицях [activite] та [4]. Одна особа може займатися кількома видами діяльності, а одним видом діяльності можуть займатися кілька осіб. Отже, таблиці [personne] та [activite] пов’язані відношенням «багато-до-багатьох». Це відношення реалізується за допомогою таблиці з’єднання [personne_activite] [5].
2.5.2. Об’єкти @Entity, що представляють базу даних
Вищезазначені таблиці будуть представлені такими @Entity:
- @Entity Personne представлятиме таблицю [personne]
- @Entity Adresse представлятиме таблицю [adresse]
- @Entity Activite представлятиме таблицю [activite]
- @Entity PersonneActivite представлятиме таблицю [personne_activite]
Відносини між цими сутностями такі:
- відношення «один до одного» пов’язує суть Personne із сутністю Adresse: особа p має адресу a. Суть Personne, яка містить зовнішній ключ, матиме головний зв’язок, а суть Adresse — зворотний зв’язок.
- Відношення «багато-до-багатьох» пов’язує сутності Personne та Activite: одна особа має кілька видів діяльності, а одним видом діяльності займаються кілька осіб. Цей зв’язок можна було б реалізувати безпосередньо за допомогою анотації @ManyToMany у кожній з двох сутностей, причому одна з них була б визначена як зворотна до іншої. Це рішення буде розглянуто пізніше. Тут ми реалізуємо зв’язок «багато-до-багатьох» за допомогою двох зв’язків «один-до-багатьох»:
- відношення «один до багатьох», яке пов’язує суть Personne із суттю PersonneActivite: на один (One) запис таблиці [personne] посилаються кілька (Many) записів таблиці [personne_activite]. Таблиця [personne_activite], що містить зовнішній ключ, буде мати головний зв’язок @ManyToOne, а суть Personne — зворотний зв’язок @OneToMany.
- відношення «один до багатьох», яке пов’язує суть Activite із сутністю PersonneActivite: на один (One) запис таблиці [activite] посилаються кілька (Many) записів таблиці [personne_activite]. Таблиця [personne_activite], що містить зовнішній ключ, буде мати головний зв’язок @ManyToOne, а суть Activite — зворотний зв’язок @OneToMany.
@Entity Personne має такий вигляд:
@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// основний зв’язок «Особа» (one) -> «Адреса» (one)
// реалізовано за допомогою зовнішнього ключа «Особа» (adresse_id) -> «Адреса»
// каскадне вставлення «Особа» -> вставлення «Адреса»
// каскадне оновлення «Особа» -> оновлення «Адреса»
// каскадне видалення «Особа» -> видалення «Адреса»
// одна особа повинна мати 1 адресу (nullable=false)
// 1 адреса належить лише 1 особі (unique=true)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// відношення «Особа» (one) -> PersonneActivite (many)
// зворотний напрямок існуючого зв’язку PersonneActivite (many) -> Особа (one)
// каскадне видалення «Особа» -> видалення PersonneActivite
@OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();
// конструктори
Ця @Entity відома. Ми коментуємо лише її зв’язки з іншими сутностями:
- рядки 30–39: відношення «один до одного» @OneToOne з @Entity Adresse, реалізована через зовнішній ключ [adresse_id] (рядок 38), який таблиця [personne] матиме у таблиці [adresse].
- рядки 41–45: відношення «один до багатьох» @OneToMany з @Entity PersonneActivite. Одна особа (One) посилається на кілька (Many) рядків таблиці з'єднання [personne_activite], представленої @Entity PersonneActivite. Ці об’єкти PersonneActivite будуть розміщені в типі Set<PersonneActivite>, де PersonneActivite — це тип, який ми незабаром визначимо.
- рядок 44: визначена тут відношення «один до багатьох» є оберненим відношенням до основного відношення, визначеного для поля personne сутності @Entity PersonneActivite (ключове слово mappedBy). Ми маємо каскад «Особа» → «Діяльність» при видаленні: видалення особи p призведе до видалення постійних елементів типу PersonneActivite, що містяться в наборі p.activites.
@Entity Adresse має такий вигляд:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// поля
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- рядки 28–29: відношення @OneToOne є оберненим до відношення @OneToOne, що вказує на @Entity Personne (рядки 37–38 у Personne).
@Entity Activite має такий вигляд
@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {
// поля
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// відношення «Діяльність» (один) -> PersonneActivite (багато)
// зворотний напрямок існуючого зв’язку PersonneActivite (багато) -> «Дія» (один)
// каскадне видалення «Дія» -> видалення PersonneActivite
@OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();
- рядки 6–9: первинний ключ активності
- рядки 11–13: номер версії активності
- рядки 15–16: назва активності
- рядки 18–22: відношення «один до багатьох», що пов’язує @Entity Activite з @Entity PersonneActivite: одна активність (One) посилається на кілька (Many) рядків таблиці з'єднання [personne_activite], представленої @Entity PersonneActivite. Ці об’єкти PersonneActivite будуть розміщені в типі Set<PersonneActivite>.
- рядок 22: визначена тут відношення «один до багатьох» є оберненим відношенням до основного відношення, визначеного для поля activite в @Entity PersonneActivite (ключове слово mappedBy). Існує каскад «Activite» → PersonneActivite при видаленні: видалення таблиці [activite] зактивності a призведе до видалення таблиці з'єднання [personne_activite] з персистентними елементами типу PersonneActivite, знайденими в наборі a.personnes.
@Entity PersonneActivite має такий вигляд:
@Entity
// таблиця з'єднання
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {
@Embeddable
public static class Id implements Serializable {
// компоненти складеного ключа
// вказує на особу
@Column(name = "PERSONNE_ID")
private Long personneId;
// вказує на діяльність
@Column(name = "ACTIVITE_ID")
private Long activiteId;
// конструктори
...
// гетери та сеттери
...
// toString
public String toString() {
return String.format("[%d,%d]", getPersonneId(), getActiviteId());
}
}
// поля класу Personne_Activite
// складений ключ
@EmbeddedId
private Id id = new Id();
// головна реляція PersonneActivite (many) -> Особа (one)
// реалізовано за допомогою зовнішнього ключа: personneId (PersonneActivite (багато) -> Особа (один)
// personneId одночасно є елементом складеного первинного ключа
// JPA не повинен керувати цим зовнішнім ключем (insertable = false, updatable = false), оскільки це робить сама програма у своєму конструкторі
@ManyToOne
@JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
private Personne personne;
// головна реляція PersonneActivite -> Діяльність
// реалізовано за допомогою зовнішнього ключа: activiteId (PersonneActivite (багато) -> Дія (одна)
// activiteId одночасно є елементом складеного первинного ключа
// JPA не повинен керувати цим зовнішнім ключем (insertable = false, updatable = false), оскільки це робить сама програма у своєму конструкторі
@ManyToOne()
@JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
private Activite activite;
// конструктори
public PersonneActivite() {
}
public PersonneActivite(Personne p, Activite a) {
// зовнішні ключі задаються додатком
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// двосторонні зв’язки
this.setPersonne(p);
this.setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
// методи getter та setter
...
// toString
public String toString() {
return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
}
}
Цей клас є складнішим за попередні.
- Таблиця [personne_activite] містить рядки у вигляді [p,a], де p — це первинний ключ особи, а a — первинний ключ діяльності. Кожна таблиця повинна мати первинний ключ, і [personne_activite] не є винятком із цього правила. До цього моменту ми визначали первинні ключі, що динамічно генерувалися за допомогою SGBD. Це можна було б зробити й тут. Але ми використаємо іншу техніку, за якою сама програма визначає значення первинного ключа таблиці. У даному випадку рядок [p1,a1] вказує на те, що особа p1 займається діяльністю a1. Цей самий рядок не може повторюватися в таблиці. Отже, пара (p, a) є гарним кандидатом на роль первинного ключа. Такий ключ називають складеним первинним ключем.
- рядки 30–31: складений первинний ключ. Анотація @EmbeddedId (зазвичай це було @Id) аналогічна нотації @Embedded, застосованій до поля Adresse особи. В останньому випадку це означало, що поле Adresse було об’єктом зовнішнього класу, але мало бути вставлене в ту саму таблицю, що й особа. Тут значення те саме, але щоб вказати, що мова йде про первинний ключ, нотація стає @EmbeddedId.
- рядок 31: порожній об’єкт, що представляє первинний ключ id, створюється одразу після створення об’єкта [PersonneActivite]. Клас, що представляє первинний ключ, визначено в рядках 7–26 як публічний статичний клас, внутрішній щодо класу [PersonneActivite]. Те, що він є публічним і статичним, диктується вимогами Hibernate. Якщо замінити public static на private,, виникає виняток, і в відповідному повідомленні про помилку видно, що Hibernate намагався виконати оператор new PersonneActivite$Id. Отже, клас Id має бути одночасно статичним і публічним.
- рядок 6: клас Id первинного ключа оголошено як @Embeddable. Нагадаємо, що первинний ключ id у рядку 31 було оголошено як @EmbeddedId. Відповідний клас повинен мати анотацію @Embeddable.
- Ми вже зазначали, що первинний ключ таблиці [personne_activite] складається з пари (p, a), де p є первинним ключем особи, а a — первинним ключем діяльності. Обидва елементи (p, a) складеного ключа містяться у рядку 11 (personneId) та рядку 15 (activiteId). Столбці, пов’язані з цими двома полями, мають такі назви: PERSONNE_ID для особи, ACTIVITE_ID для діяльності.
- рядок 31: первинний ключ було визначено з двома стовпцями (PERSONNE_ID, ACTIVITE_ID). Інших стовпців у таблиці [personne_activite] немає. Залишилося лише визначити зв’язки, що існують між @Entity PersonneActivite, яку ми зараз описуємо, та іншими @Entity реляційної схеми. Ці зв’язки відображають обмеження зовнішніх ключів, які має таблиця [personne_activite] щодо інших таблиць.
- рядки 33–39: визначають зовнішній ключ, який має таблиця [personne_activite] щодо таблиці [personne]
- рядок 37: відношення має тип @ManyToOne: на один (One) запис таблиці [personne] посилаються кілька (Many) записів таблиці [personne_activite].
- рядок 38: стовпцю надано ім’я зовнішнього ключа. Використовується те саме ім’я, що й для компонента «personne» зовнішнього ключа (рядок 10). Атрибути insertable=false, updatable=false призначені для того, щоб запобігти управлінню зовнішнім ключем з боку Hibernate. Адже цей ключ є складовою первинного ключа, що обчислюється додатком, і Hibernate не повинен втручатися.
- рядки 41–47: визначають зовнішній ключ, який таблиця [personne_activite] має до таблиці [activite]. Пояснення такі самі, як наведені вище.
- рядки 54–63: конструктор об’єкта PersonneActivite на основі особи p та діяльності a. Нагадаємо, що під час створення об’єкта PersonneActivite первинний ключ id у рядку 31 вказував на порожній об’єкт Id. У рядках 56–57 кожному з полів (personneId, activiteId) об’єкта Id присвоюється значення. Ці значення є, відповідно, первинними ключами особи p та діяльності a, переданими як параметри конструктора. Отже, первинний ключ id (рядок 31) тепер має значення.
- рядок 59: поле personne у рядку 39 отримує значення p
- рядок 60: поле activite у рядку 47 отримує значення a
- Об'єкт [PersonneActivite] створено та ініціалізовано. Оновлюються зворотні зв’язки між @Entity Personne (рядок 61) та Activite (рядок 62) з щойно створеним @Entity PersonneActivite.
Ми завершили опис сутностей бази даних. Ми опинилися в складній, але, на жаль, типовій ситуації. Ми побачимо, що існує інша можлива конфігурація шару JPA, яка приховує частину цієї складності: таблиця з’єднання стає неявним елементом, що створюється та управляється шаром JPA. Тут ми обрали найскладніше рішення, яке, однак, дає змогу реляційній схемі розвиватися. Воно дозволяє додавати стовпці до таблиці з’єднання, чого не дозволяє конфігурація, в якій таблиця з’єднання не є явною @Entity. [ref1] рекомендує саме те рішення, яке ми зараз розглядаємо. Саме в [ref1] було знайдено інформацію, яка дозволила розробити це рішення.
2.5.3. Проєкт Eclipse / Hibernate
Реалізація JPA, яка використовується тут, належить до Hibernate. Проєкт Eclipse для тестування має такий вигляд:

У файлі [1] — проект Eclipse, у файлі [2] — Java-код. Проект міститься у файлі [3] у папці прикладів [4]. Його потрібно імпортувати.
2.5.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій з розділу 2.1.7, отриманий файл DDL для SGBD MySQL5 має такий вигляд:
alter table jpa07_hb_personne
drop
foreign key FKB5C817D45FE379D0;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B06CD852024;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B0668C7A284;
drop table if exists jpa07_hb_activite;
drop table if exists jpa07_hb_adresse;
drop table if exists jpa07_hb_personne;
drop table if exists jpa07_hb_personne_activite;
create table jpa07_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa07_hb_personne
add index FKB5C817D45FE379D0 (adresse_id),
add constraint FKB5C817D45FE379D0
foreign key (adresse_id)
references jpa07_hb_adresse (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B06CD852024 (ACTIVITE_ID),
add constraint FKD3E49B06CD852024
foreign key (ACTIVITE_ID)
references jpa07_hb_activite (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B0668C7A284 (PERSONNE_ID),
add constraint FKD3E49B0668C7A284
foreign key (PERSONNE_ID)
references jpa07_hb_personne (id);
- рядки 21–26: таблиця [activite]
- рядки 28–39: таблиця [adresse]
- рядки 41–51: таблиця [personne]
- рядки 53–57: таблиця з’єднання [personne_activite]. Зверніть увагу на складений ключ (рядок 56)
- рядки 59–63: зовнішній ключ з таблиці [personne] до таблиці [adresse]
- рядки 65–69: зовнішній ключ таблиці [personne_activite] до таблиці [activite]
- рядки 71–75: зовнішній ключ таблиці [personne_activite] до таблиці [personne]
2.5.5. InitDB
Код [InitDB] такий:
package tests;
...
public class InitDB {
// константи
private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";
private final static String TABLE_PERSONNE = "jpa07_hb_personne";
private final static String TABLE_ACTIVITE = "jpa07_hb_activite";
private final static String TABLE_ADRESSE = "jpa07_hb_adresse";
public static void main(String[] args) throws ParseException {
// Контекст збереження
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// отримуємо EntityManager з EntityManagerFactory
// попередній
em = emf.createEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// запит
Query sql1;
// видалити елементи з таблиці PERSONNE_ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
sql1.executeUpdate();
// видалити елементи з таблиці PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// видалити елементи з таблиці ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
sql1.executeUpdate();
// видалити елементи з таблиці ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// створення завдань
Activite act1 = new Activite();
act1.setNom("act1");
Activite act2 = new Activite();
act2.setNom("act2");
Activite act3 = new Activite();
act3.setNom("act3");
// збереження активностей
em.persist(act1);
em.persist(act2);
em.persist(act3);
// створення осіб
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// створення адрес
Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
// взаємозв'язки між особами та адресами
p1.setAdresse(adr1);
adr1.setPersonne(p1);
p2.setAdresse(adr2);
adr2.setPersonne(p2);
p3.setAdresse(adr3);
adr3.setPersonne(p3);
// збереження осіб і, відповідно, пов'язаних з ними адрес
em.persist(p1);
em.persist(p2);
em.persist(p3);
// збереження адреси a4, не пов'язаної з особою
em.persist(adr4);
// відображення осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// відображення адрес
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
// зв’язки «особа» <--> «діяльність»
PersonneActivite p1act1 = new PersonneActivite(p1, act1);
PersonneActivite p1act2 = new PersonneActivite(p1, act2);
PersonneActivite p2act1 = new PersonneActivite(p2, act1);
PersonneActivite p2act3 = new PersonneActivite(p2, act3);
// збереження зв'язків «особа <--> діяльність»
em.persist(p1act1);
em.persist(p1act2);
em.persist(p2act1);
em.persist(p2act3);
// перегляд осіб
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// перегляд адрес
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
System.out.println("[personnes/activites]");
for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
System.out.println(pa);
}
// завершення транзакції
tx.commit();
// завершення EntityManager
em.close();
// завершення EntityManagerFactory
emf.close();
// журнал
System.out.println("terminé...");
}
}
- рядки 27–38: таблиці [personne_activite], [personne], [adresse] та [activite] очищаються. Слід зауважити, що спочатку необхідно очистити таблиці, які містять зовнішні ключі.
- рядки 40–45: створюються три операції act1, act2 та act3
- рядки 47–49: вони додаються до контексту збереження.
- рядки 51–53: створюються три особи p1, p2 та p3.
- рядки 55–58: створюються чотири адреси — від adr1 до adr4.
- рядки 60–65: адреси adri пов’язуються з особами pi. Кожного разу потрібно виконати дві операції, оскільки відношення «Особа <-> Адреса» є двонаправленим.
- рядки 67–69: особи від p1 до p3 поміщаються в контекст збереження. Через каскад «Особа» → «Адреса» це також стосуватиметься адрес від adr1 до adr3.
- рядок 71: четверта адреса adr4, яка не пов’язана з жодною особою, явно поміщається в контекст збереження.
- рядки 73–85: до контексту збереження надсилається запит для отримання списку сутностей типу [Personne], [Adresse] та [Activite]. Відомо, що ці запити спричинять синхронізацію контексту з базою даних: створені сутності будуть вставлені в базу даних і отримають свої первинні ключі. Це важливо розуміти для подальшого розуміння.
- рядки 87–90: створюються 4 асоціації «Особа» <-> «Діяльність». Їхні назви вказують, яка особа пов’язана з якою діяльністю. Можливо, ви пам’ятаєте, що первинний ключ сутності PersonneActivite є складеним ключем, що складається з первинного ключа особи та первинного ключа діяльності. Отже, ця операція можлива саме тому, що сутності Personne та Activite отримали свої первинні ключі під час попередньої синхронізації.
- рядки 92–95: ці 4 асоціації поміщаються в контекст персистентності.
- рядки 87–86: до контексту збереження надсилається запит на отримання списку сутностей типів [Personne], [Adresse], [Activite] та [PersonneActivite]. Відомо, що ці запити спричинять синхронізацію контексту з базою даних: створені сутності PersonneActivite будуть вставлені в базу даних.
Виконання [InitDB] разом із MySQL5 призводить до появи такого виводу в консолі:
Може викликати подив те, що в рядках 15–16 особи p1 та p2 мають номер версії 1, і те саме стосується трьох видів діяльності в рядках 24–26. Спробуємо розібратися.
У рядках 2–4 номери версій осіб дорівнюють 0, а в рядках 11–13 номери версій дій дорівнюють 0. Ці відображення відбуваються до створення зв’язків «Особа <-> Дія». У рядках 87–90 коду Java створюються зв’язки між особами p1 та p2 і діями act1, act2, act3. Вони реалізуються за допомогою конструктора @Entity PersonneActivite (див. розділ 2.5.2). Аналіз коду цього конструктора показує, що коли особа p пов’язана з діяльністю a:
- діяльність a додається до множини p.activites
- особа p додається до множини a.personnes
Отже, коли записується new PersonneActivite(p,a), особа p та діяльність a зазнають змін у пам’яті. У рядках 97–113 файлу [InitDB] контекст персистентності синхронізується з базою даних, JPA / Hibernate виявляє, що персистентні елементи p1, p2, act1, act2 та act3 були змінені. Ці зміни мають бути внесені в базу даних. Фактично вони записані в таблиці з’єднання [personne_activite], але JPA / Hibernate все одно збільшує номер версії кожного зі змінених персистентних елементів.
У вікні SQL Explorer результати такі:
![]() |
- [2]: таблиці [jpa07_hb_*]
- [3]: таблиця осіб
- [4]: таблиця адрес.
- [5]: таблиця видів діяльності
- [6]: таблиця зв’язку «особа» <-> «діяльність»
2.5.6. Головна
Клас [Main] об’єднує тести, які ми розглядаємо, за винятком тесту 1, який використовує код з [InitDB] для ініціалізації бази даних.
2.5.6.1. Тест 2
Цей тест виглядає так:
// видалення особи p1
public static void test2() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалення залежностей від p1: не потрібно для Hibernate, але
// необхідні для TopLink
act1.getPersonnes().remove(p1act1);
act2.getPersonnes().remove(p1act2);
// видалення особи p1
em.remove(p1);
// завершення транзакції
tx.commit();
// відображаються нові таблиці
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- рядок 4: використовується контекст збереження даних з test1, де особа p1 є об’єктом цього контексту.
- рядок 13: видалення особи p1. Через атрибут:
- cascadeType.ALL на Adresse, адреса особи p1 буде видалена
- cascadeType.REMOVE щодо PersonneActivite, діяльність особи p1 буде видалена.
- рядки 10–11: видаляються залежності інших сутностей від особи p1, яка буде видалена в рядку 13. Діяльності act1 та act2 здійснюються особою p1. Зв’язки були створені конструктором сутності PersonneActivite, код якої такий:
public PersonneActivite(Personne p, Activite a) {
// зовнішні ключі встановлюються додатком
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// двосторонні зв’язки
setPersonne(p);
setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
у рядку 9 діяльність a отримує додатковий елемент типу PersonneActivite у своєму наборі personnes. Цей елемент має тип (p,a), що вказує на те, що особа p здійснює діяльність a. У test1 з [Main] таким чином було створено два зв’язки: (p1,act1) та (p1,act2). Рядки 10 і 11 у test2 видаляють ці залежності. Слід зазначити, що Hibernate працює без видалення цих залежностей для особи p1, на відміну від Toplink.
- рядки 17–20: виводимо всі таблиці
Результати такі:
- особа p1, яка фігурує в записі test1 (рядок 3), більше не фігурує після завершення запису test2 (рядки 22–23)
- адреса adr1 особи p1, яка присутня в test1 (рядок 11) більше не є такою після виконання test2 (рядки 29–31)
- діяльності (p1,act1) (рядок 16) та (p1,act2) (рядок 18) особи p1, що містяться в test1, більше не є такими після завершення test2 (рядки 33–34)
2.5.6.2. Тест 3
Цей тест полягає в наступному:
// видалення операції act1
public static void test3() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалення залежностей від act1: не потрібно для Hibernate, але
// необхідні для TopLink
p2.getActivites().remove(p2act1);
// видалення активності act1
em.remove(act1);
// завершення транзакції
tx.commit();
// відображення нових таблиць
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- рядок 4: використовується контекст збереження даних з test2
- рядок 12: видалення операції act1. Через атрибут:
- cascadeType.REMOVE для PersonneActivite, рядки (p, act1) таблиці [personne_activite] будуть видалені.
- рядок 10: перед тим, як вивести act1 за межі контексту збереження, видаляються залежності, які можуть мати інші сутності від цього об’єкта, що зберігається. Після видалення особи p1 у попередньому тесті лише особа p2 здійснює діяльність act1.
- рядки 13–16: виводяться всі таблиці
Результати такі:
- у test2 існує діяльність act1 (рядок 6). У test3 вона вже не існує (рядки 21–22)
- у test2 існує зв’язок (p2,act1)) (рядок 14). У test3 його вже немає (рядок 28)
2.5.6.3. Test4
Цей тест виглядає так:
// отримання активності особи
public static void test4() {
// контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// отримано особу p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
// сканування її дій
for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
System.out.println(pa);
}
// перехід через зворотний зв'язок p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
// скануємо її дії
for (PersonneActivite pa : p2.getActivites()) {
System.out.println(pa.getActivite().getNom());
}
// кінець транзакції
tx.commit();
}
- тест 4 відображає дії особи p2.
- рядок 4: починаємо з нового, порожнього контексту
- рядки 12–14: відображаються назви видів діяльності, якими займається особа p2, за допомогою запиту JPQL.
- виконується з'єднання Activite (a) / PersonneActivite (pa) (join a.personnes)
- у рядках цього з'єднання (a,pa) відображається назва діяльності (a.nom) для особи p2 (pa.personne.nom='p2').
- рядки 16–21: виконується те саме, що й раніше, але за допомогою зв’язку OneToMany p2.activites особи p2. Запит JPQL буде згенеровано запитом JPA. Тут стає очевидною користь зворотного зв’язку OneToMany: він дозволяє уникнути запиту JPQL.
Результати такі:
2.5.6.4. Test5
Цей тест виглядає так:
// отримання осіб, які виконують певну діяльність
public static void test5() {
// контекст збереження даних
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// запит щодо дій p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
System.out.println(pa);
}
// перехід через зворотний зв'язок act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (PersonneActivite pa : act3.getPersonnes()) {
System.out.println(pa.getPersonne().getNom());
}
// завершення транзакції
tx.commit();
}
- тест 6 відображає осіб, які виконують діяльність act3. Підхід аналогічний до того, що використовувався в тесті 6. Ми залишаємо читачеві можливість самостійно встановити зв’язок між цими двома кодами.
Результати такі:
Тести 4 і 5 мали на меті ще раз продемонструвати, що зворотний зв’язок ніколи не є обов’язковим і завжди може бути замінений запитом JPQL.
2.5.7. Реалізація JPA / Toplink
Зараз ми використовуємо реалізацію JPA / Toplink:
![]() |
Проєкт Eclipse з Toplink є копією проєкту Eclipse з Hibernate:
![]() |
Код Java ідентичний коду попереднього проекту з Hibernate, за винятком кількох деталей, про які ми розповімо далі. Середовище (бібліотеки — persistence.xml — СКБД — папки conf, ddl — скрипт ant) є тим самим, що розглядалося в розділі 2.1.15.2. Проєкт Eclipse знаходиться у файлі [3] у папці прикладів [4]. Його потрібно імпортувати.
Файл <persistence.xml> [2] змінено в одному місці, а саме в частині оголошених об’єктів:
<!-- персистентні класи -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
<class>entites.PersonneActivite</class>
- рядки 2–5: чотири керовані об’єкти
Виконання [InitDB] разом із SGBD MySQL5 дає такі результати:
![]() |
У [1], вивід на консоль; у [2] — згенеровані таблиці [jpa07_tl]; у [3] — згенеровані скрипти SQL. Їхній вміст такий:
create.sql
CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
Виконання [InitDB] та [Main] відбувається без помилок.
2.6. Приклад 6: відношення «багато-до-багатьох» з неявним об’єднувальним столом
Повернемося до прикладу 4, але тепер розглянемо його з неявним з’єднувальним стовпцем, згенерованим самим шаром JPA.
2.6.1. Схема бази даних
![]() |
- у [1], база даних MySQL5 — у [2]: таблиця [personne] — у [3]; пов’язана таблиця [adresse] — у [4]; таблиця [activite] з діяльністю — у [5]: таблиця з'єднання [personne_activite], яка пов'язує осіб та діяльність.
2.6.2. Об’єкти @Entity, що представляють базу даних
Вищезазначені таблиці будуть представлені такими @Entity:
- @Entity Personne представлятиме таблицю [personne]
- @Entity Adresse представлятиме таблицю [adresse]
- @Entity Activite представлятиме таблицю [activite]
- таблиця [personne_activite] більше не представлена @Entity
Відносини між цими сутностями такі:
- відношення «один до одного» пов’язує суть Personne із сутністю Adresse: особа p має адресу a. Суть Personne, яка містить зовнішній ключ, матиме головний зв’язок, а суть Adresse — зворотний зв’язок.
- Відношення «багато-до-багатьох» пов’язує сутності Personne та Activite: одна особа має кілька видів діяльності, а одним видом діяльності займаються кілька осіб. Цей зв’язок буде реалізовано за допомогою анотації @ManyToMany у кожній із двох сутностей, причому одна з них оголошується оберненою до іншої.
@Entity Personne має такий вигляд:
@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver :@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// головна реляція «Особа» (one) -> «Адреса» (one)
// реалізована за допомогою зовнішнього ключа «Особа» (adresse_id) -> «Адреса»
// каскадне вставлення «Особа» -> вставлення «Адреса»
// каскадне оновлення «Особа» -> оновлення «Адреса»
// каскадне видалення «Особа» -> видалення «Адреса»
// одна особа повинна мати 1 адресу (nullable=false)
// 1 адреса належить лише 1 особі (unique=true)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// зв’язок «Особа» (many) -> «Діяльність» (many) через таблицю з’єднання personne_activite
// personne_activite(PERSONNE_ID) є зовнішнім ключем для «Особа» (id)
// personne_activite(ACTIVITE_ID) — це зовнішній ключ для «Дія» (id)
// cascade=CascadeType.PERSIST : збереження 1 особи зумовлює збереження її діяльності
@ManyToMany(cascade={CascadeType.PERSIST})
@JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
// будівельники
public Personne() {
}
Ми коментуємо лише відношення @ManyToMany у рядках 46–48, яке пов’язує @Entity Personne з @Entity Activite:
- рядок 48: особа має види діяльності. Поле activites буде їх представляти. У попередній версії типом елементів множини activites був PersonneActivite. Тут це Activite. Отже, доступ до видів діяльності особи здійснюється безпосередньо, тоді як у попередній версії потрібно було проходити через проміжну суть PersonneActivite.
- рядок 46: відношення, яке пов’язує @Entity Personne, що ми розглядаємо, з @Entity Activite із множини activites у рядку 48, є відношенням «багато-до-багатьох» (ManyToMany):
- одна особа (One) має кілька видів діяльності (Many)
- одним видом діяльності (One) займаються кілька осіб (Many)
- Зрештою, @Entity Personne та Activite пов’язані відношенням ManyToMany. Як і у відношенні OneToOne, у цьому відношенні спостерігається симетрія сутностей. Можна вільно обирати, яка @Entity буде мати головний зв’язок, а яка — зворотний. У цьому випадку ми вирішуємо, що @Entity Personne матиме головний зв’язок.
- Як ми бачили в попередньому прикладі, відношення @ManyToMany потребує таблиці з’єднання. Якщо раніше ми визначали її за допомогою @Entity, то тут таблиця з’єднання визначається за допомогою анотації @JoinTable у рядку 47.
- Атрибут name присвоює таблиці ім’я.
- Таблиця з'єднання складається з зовнішніх ключів у таблицях, які вона з'єднує. Тут є два зовнішні ключі: один у таблиці [personne], інший — у таблиці [activite]. Ці стовпці зовнішніх ключів визначаються атрибутами joinColumns та inverseJoinColumns.
- Анотація @JoinColumn атрибута joinColumns визначає зовнішній ключ у таблиці @Entity, що містить головну реляцію @ManyToMany, у даному випадку — таблиці [personne]. Цей стовпець із зовнішнім ключем матиме назву PERSONNE_ID.
- Анотація @JoinColumn атрибута inverseJoinColumns визначає зовнішній ключ у таблиці @Entity, що містить зворотний зв'язок @ManyToMany, у даному випадку таблиці [activite]. Цей стовпець із зовнішнім ключем матиме назву ACTIVITE_ID.
@Entity Adresse має такий вигляд:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// поля
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- рядки 28–29: відношення @OneToOne є оберненим до відношення @OneToOne, що вказує на @Entity Personne (рядки 37–38 з Personne).
@Entity Activite має такий вигляд
@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {
// поля
@Id()
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver: @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// зворотний зв'язок «Діяльність» → «Особа»
@ManyToMany(mappedBy = "activites")
private Set<Personne> personnes = new HashSet<Personne>();
...
- рядки 20–21: відношення «багато-до-багатьох», яке пов’язує @Entity Activite з @Entity Personne. Це відношення вже було визначено в @Entity Personne. Тому тут достатньо зазначити, що це відношення є оберненим (mappedBy) до відношення @ManyToMany, яке існує для поля activites (mappedBy = «activites») @Entity Personne.
- Нагадаємо, що зворотний зв'язок завжди є необов'язковим. У цьому випадку ми використовуємо його для отримання осіб, які займаються поточним видом діяльності. Саме множина Set<Personne> «особи» дозволить отримати цих осіб. Режим завантаження залежностей Personne від @Entity Activite не вказано. Ми також не вказували його в попередньому прикладі. За замовчуванням цей режим дорівнює fetch=FetchType.LAZY.
Ми завершили опис сутностей бази даних. Це було простіше, ніж у випадку, коли таблиця з’єднання [personne_activite] є окремою таблицею. Це простіше рішення може мати недоліки з часом: воно не дозволяє додавати стовпці до таблиці з’єднання. Однак це може виявитися необхідним для задоволення нових потреб, наприклад, щоб додати до таблиці [personne_activite] стовпець, що вказує дату реєстрації особи на заході.
2.6.3. Проєкт Eclipse / Hibernate
Реалізація JPA, яка використовується тут, належить до Hibernate. Проєкт Eclipse для тестування має такий вигляд:
![]() |
У [1] — проект Eclipse, у [2] — Java-код. Проект міститься у [3] у папці прикладів [4]. Його потрібно імпортувати.
2.6.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій з розділу 2.1.7, отриманий файл DDL для SGBD та MySQL5 має такий вигляд:
alter table jpa08_hb_personne
drop
foreign key FKA44B1E555FE379D0;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A5CD852024;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A568C7A284;
drop table if exists jpa08_hb_activite;
drop table if exists jpa08_hb_adresse;
drop table if exists jpa08_hb_personne;
drop table if exists jpa08_hb_personne_activite;
create table jpa08_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa08_hb_personne
add index FKA44B1E555FE379D0 (adresse_id),
add constraint FKA44B1E555FE379D0
foreign key (adresse_id)
references jpa08_hb_adresse (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A5CD852024 (ACTIVITE_ID),
add constraint FK5A6A55A5CD852024
foreign key (ACTIVITE_ID)
references jpa08_hb_activite (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A568C7A284 (PERSONNE_ID),
add constraint FK5A6A55A568C7A284
foreign key (PERSONNE_ID)
references jpa08_hb_personne (id);
Цей код DDL аналогічний тому, що отримано за допомогою таблиці явного з’єднання, і відповідає схемі, яка вже була представлена:
![]() |
2.6.5. InitDB
Ми не будемо детально зупинятися на класі [InitDB], оскільки він ідентичний попередній версії та дає ті самі результати. Просто звернемо увагу на наступний код, який відображає з’єднання Personne <-> Activite:
// перегляд осіб/діяльностей
System.out.println("[personnes/activites]");
Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
- рядок 3: команда JPQL, яка виконує з'єднання. Результат команди select повертає ідентифікатори сутностей Personne та Activite, пов'язаних між собою таблицею з'єднання. Список, що повертається select, складається з рядків, що містять два об’єкти типу Long. Щоб пройти цей список, у рядку 3 запитується об’єкт Iterator зі списку.
- рядки 4–7: за допомогою попереднього об’єкта типу Iterator здійснюється перебір списку.
- рядок 5: кожен елемент списку є масивом, що містить рядок, отриманий у результаті виконання select
- рядок 6: витягуються елементи поточного рядка-результату функції select із внесенням відповідних змін типів.
Результат функції [InitDB] є таким:
2.6.6. Main
Клас [Main] об’єднує низку тестів, деякі з яких ми розглянемо.
2.6.6.1. Тест3
Цей тест має такий вигляд:
// видалення діяльності act1
public static void test3() {
// контекст збереження
EntityManager em = getEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалення діяльності act1 з p2
p2.getActivites().remove(act1);
// вилучення act1 з контексту збереження
em.remove(act1);
// завершення транзакції
tx.commit();
// відображення нових таблиць
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- рядок 11: активність act1 вилучається з контексту збереження
- рядок 9: активність act1 входить до переліку активностей єдиної особи, що залишилася в контексті, — особи p2. У рядку 9 дія act1 вилучається зі списку дій особи p2. Ми робимо це для забезпечення узгодженості контексту збереження, оскільки ми зберігаємо його для подальшого використання.
Результати такі:
- діяльність act1, що міститься у рядку 26 у test2, зникла з діяльності test3 (рядки 40–41)
- особа p2 мала в test2 дію act1 (рядок 33). Після завершення test3 вона її більше не має (рядок 47)
2.6.6.2. Тест 6
Цей тест виглядає так:
// зміна активностей особи
public static void test6() {
// контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// отримано особу p2
p2 = em.find(Personne.class, p2.getId());
// отримано діяльність act2
act2 = em.find(Activite.class, act2.getId());
// p2 виконує лише діяльність act2
p2.getActivites().clear();
p2.getActivites().add(act2);
// завершення транзакції
tx.commit();
// відображаються нові таблиці
dumpPersonne();
dumpActivite();
dumpPersonne_Activite();
}
- рядок 4: використовується новий і порожній контекст збереження
- рядок 9: об’єкт p2 переноситься з бази даних у контекст збереження
- рядок 11: активність act2 переноситься з бази даних у контекст збереження
- рядок 13: дії особи p2 (act3) переносяться з бази даних у контекст (fetchType.LAZY). Саме виклик [getActivites] ініціює це завантаження. Видаляються види діяльності особи p2. Це не є фактичним видаленням видів діяльності (remove), а зміною статусу особи p2. Вона більше не займається жодними видами діяльності.
- рядок 14: до особи p2 додається діяльність act2. У підсумку, множина нових видів діяльності особи p2 дорівнює множині {act2}.
- рядок 16: кінець транзакції. Система синхронізації проаналізує об’єкти контексту (p2, act2, act3) і виявить, що стан p2 змінився. Будуть виконані команди SQL, які відображають цю зміну в базі даних.
- рядки 18–20: відображаються всі таблиці
Результати такі:
- за підсумками тесту 4 особа p2 виконувала діяльність act3 (рядок 3).
- за результатами тесту 6 (рядок 19) особа p2 більше не займається діяльністю act3 (рядок 3) і займається діяльністю act2.
2.6.7. Реалізація JPA / Toplink
Зараз ми використовуємо реалізацію JPA / Toplink:
![]() |
Проєкт Eclipse з Toplink є копією проєкту Eclipse з Hibernate:
![]() |
Файл <persistence.xml> [2] змінено в одному місці, а саме в частині оголошених сутностей:
<!-- провайдер -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- персистентні класи -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
...
- рядки 4–6: керовані сутності
Виконання [InitDB] разом із SGBD MySQL5 дає такі результати:
![]() |
У [1], вивід на консоль, у [2] — згенеровані таблиці [jpa07_tl], у [3] — згенеровані скрипти SQL. Їхній вміст такий:
create.sql
CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
Виконання [InitDB] та [Main] проходить без помилок.
2.6.8. Проєкт Eclipse / Hibernate 2
Ми створюємо проект Eclipse, скопіювавши попередній:
![]() |
У файлі [1] — проект Eclipse, у файлі [2] — Java-код. Проект міститься у файлі [3] у папці з прикладами [4]. Ми імпортуємо його.
Змінимо зв’язок між Personne та Activité наступним чином:
Особа
// зв’язок «Особа» (many) -> «Діяльність» (many) через таблицю з’єднання personne_activite
// personne_activite(PERSONNE_ID) є зовнішнім ключем для «Особа» (id)
// personne_activite(ACTIVITE_ID) є зовнішнім ключем для «Дія» (id)
// додаткова каскадна обробка для активностей
// @ManyToMany(каскад={CascadeType.PERSIST})
@ManyToMany()
@JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
- рядок 6: головний зв’язок @ManyToMany більше не має каскаду збереження «Особа» → «Дія» (див. попередню версію, рядок 5)
Діяльність
// більше зворотних зв’язків з «Особою»
// @ManyToMany(mappedBy = "activites")
// private Set<Personne> personnes = new HashSet<Personne>();
- рядки 2–3: зворотний зв’язок @ManyToMany «Дія» → «Особа» видалено
Ми прагнемо показати, що видалені атрибути (каскад і зворотний зв’язок) не є обов’язковими. Перша зміна, спричинена цією новою конфігурацією, міститься в [InitDB]:
// зв’язки «особи» <--> «діяльності»
p1.getActivites().add(act1);
p1.getActivites().add(act2);
p2.getActivites().add(act1);
p2.getActivites().add(act3);
// збереження активностей
em.persist(act1);
em.persist(act2);
em.persist(act3);
// збереження даних про осіб
em.persist(p1);
em.persist(p2);
em.persist(p3);
// та адреси a4, не пов'язаної з особою
em.persist(adr4);
- рядки 7–9: ми змушені явно вказати дії act1 до act3 у контексті збереження. Коли існувала каскадна схема збереження «Особа» -> Дія існувала, рядки 11–13 зберігали одночасно як осіб від p1 до p3, так і дії цих осіб від act1 до act3.
Друга зміна помітна в записі [Main]:
// пошук осіб, які здійснюють певну діяльність
public static void test5() {
// контекст збереження
EntityManager em = getNewEntityManager();
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// запит щодо дій p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
System.out.println(pa);
}
// завершення транзакції
tx.commit();
}
- рядки 9–12: запит JPQL, що виводить осіб, які займаються діяльністю act3
- у попередній версії той самий результат також отримувався через зворотний зв’язок «Діяльність» → «Особа», який тепер видалено:
// здійснюється перехід через зворотний зв'язок act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (Personne p : act3.getPersonnes()) {
System.out.println(p.getNom());
}
2.6.9. Проєкт Eclipse / Toplink 2
Ми створюємо проект Eclipse на основі попереднього проекту Eclipse / Toplink шляхом копіювання:
![]() |
У файлі [1] — проект Eclipse, у файлі [2] — Java-код. Проект знаходиться у файлі [3] у папці з прикладами [4]. Ми імпортуємо його.
Код Java ідентичний коду у версії Hibernate.
2.7. Приклад 7: використання іменованих запитів
Ми завершуємо цю довгу презентацію сутностей JPA, розпочату в параграфі 2, останнім прикладом, який демонструє використання запитів JPQL, винесених у файл конфігурації. Цей приклад походить із такого джерела:
[ref2]: «Getting started With JPA in Spring 2.0» від Марка Фішера за адресою
[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].
2.7.1. Приклад бази даних
База даних має такий вигляд:
![]() |
- у [1]: список ресторанів із їхніми назвами та адресами
- у [2]: таблиця адрес ресторанів, що містить лише номер будинку та назву вулиці. Між таблицями restaurant та adresse існує відношення «один до одного»: кожен ресторан має лише одну адресу.
- у [3]: таблиця страв із їхніми назвами та показником «true»/«false», що вказує, чи є страва вегетаріанською чи ні
- у [4]: таблиця з'єднання «ресторани» / «страви»: один ресторан подає кілька страв, а одна й та сама страва може подаватися в кількох ресторанах. Між таблицями restaurant та plat існує багато-до-багатьох відношення.
2.7.2. Об’єкти @Entity, що представляють базу даних
Вищезазначені таблиці будуть представлені такими @Entity:
- @Entity Restaurant представлятиме таблицю [restaurant]
- @Entity Adresse представлятиме таблицю [adresse]
- @Entity Plat представлятиме таблицю [plat]
Відносини між цими сутностями такі:
- відношення «один до одного» пов’язує суть Restaurant із сутністю Adresse: ресторан r має адресу a. Суть Restaurant, яка містить зовнішній ключ, матиме головний зв’язок. Суть Adresse не матиме зворотного зв’язку.
- Відношення «багато-до-багатьох» пов’язує сутності Restaurant та Plat: один ресторан подає кілька страв, а одна й та сама страва може подаватися в кількох ресторанах. Цей зв’язок буде реалізовано за допомогою анотації @ManyToMany в сутності Restaurant. Сутність Plat не матиме зворотного зв’язку.
@Entity Restaurant має такий вигляд:
package entites;
...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique = true, length = 30, nullable = false)
private String nom;
@OneToOne(cascade = CascadeType.ALL)
private Adresse adresse;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
private Set<Plat> plats = new HashSet<Plat>();
// конструктори
public Restaurant() {
}
public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
}
// гетери та сеттери
...
// toString
public String toString() {
String signature = "R[" + getNom() + "," + getAdresse();
for (Plat e : getPlats()) {
signature += "," + e;
}
return signature + "]";
}
}
- рядок 17: відношення «один до одного», яке має суть Restaurant із сутністю Adresse. Усі операції збереження даних щодо ресторану поширюються на його адресу.
- рядок 20: відношення, що пов’язує @Entity Restaurant з @Entity Plat із набору plats у рядку 22, є відношенням «багато-до-багатьох» (ManyToMany):
- один ресторан (One) має кілька страв (Many)
- одна страва (One) може подаватися в кількох ресторанах (Many)
- у підсумку @Entity Restaurant та Plat пов’язані відношенням ManyToMany. Ми вирішуємо, що @Entity Restaurant матиме головний зв’язок, а @Entity Plat не матиме зворотного зв’язку.
- Відношення @ManyToMany потребує таблиці з'єднання. Вона визначається за допомогою анотації @JoinTable у рядку 47.
- Атрибут name присвоює таблиці ім’я.
- Таблиця з'єднання складається з зовнішніх ключів таблиць, які вона з'єднує. Тут є два зовнішні ключі: один у таблиці [restaurant], інший — у таблиці [plat]. Ці стовпці зовнішніх ключів визначаються атрибутами joinColumns та inverseJoinColumns.
- Атрибут joinColumns визначає зовнішній ключ у таблиці @Entity, що містить головну реляцію @ManyToMany, у даному випадку — таблиці [restaurant]. Атрибут joinColumns тут відсутній. У цьому випадку JPA має значення за замовчуванням: [table]_[clé_primaire_de_table], у даному випадку — [jpa10_hb_restaurant_id].
- Анотація @JoinColumn атрибута inverseJoinColumns визначає зовнішній ключ у таблиці @Entity, що містить зворотний зв’язок @ManyToMany, у даному випадку — таблиці [plat]. Цей стовпець із зовнішнім ключем матиме назву plat_id.
@Entity Adresse має такий вигляд:
package entites;
...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(name = "NUMERO_RUE")
private int numeroRue;
@Column(name = "NOM_RUE", length=30, nullable=false)
private String nomRue;
// геттери та сеттери
...
// конструктори
public Adresse(int streetNumber, String streetName){
...
}
public Adresse(){
}
// toString
public String toString(){
return "A["+getNumeroRue()+","+getNomRue()+"]";
}
}
- @Entity «Adresse» — це суть, яка не має прямого зв’язку з іншими сутями. Її можна зберегти лише через суть Restaurant.
- Адреса визначається назвою вулиці (рядок 16) та номером будинку на цій вулиці (рядок 13).
@Entity Plat має такий вигляд
package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique=true, length=50, nullable=false)
private String nom;
private boolean vegetarien;
// конструктори
public Plat() {
}
public Plat(String name, boolean vegetarian) {
...
}
// геттери та сеттери
...
// toString
public String toString() {
return "E[" + getNom() + "," + isVegetarien() + "]";
}
}
- @Entity Plat — це суть, яка не має прямого зв’язку з іншими сутями. Її можна зберегти лише через суть Restaurant.
- Страва визначається назвою (рядок 12) та типом — вегетаріанська чи ні (рядок 14).
2.7.3. Проєкт Eclipse / Hibernate
Реалізація JPA, що використовується тут, належить до Hibernate. Проєкт Eclipse для тестування має такий вигляд:
![]() |
У [1] — проект Eclipse, у [2] — Java-код, а у JPA — конфігурація. Зверніть увагу на наявність файлу [orm.xml], з яким ми ще не стикалися. Цей проект міститься у файлі [3] у папці прикладів [4]. Ми імпортуємо його.
2.7.4. Створення файлу DDL з бази даних
Дотримуючись інструкцій з розділу 2.1.7, файл DDL, отриманий для SGBD та MySQL5, має такий вигляд:
alter table jpa10_hb_restaurant
drop
foreign key FK3E8E4F5D5FE379D0;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D11F0F78A4;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D1AFAC3E44;
drop table if exists jpa10_hb_adresse;
drop table if exists jpa10_hb_plat;
drop table if exists jpa10_hb_restaurant;
drop table if exists jpa10_hb_restaurant_plat;
create table jpa10_hb_adresse (
id bigint not null auto_increment,
NUMERO_RUE integer,
NOM_RUE varchar(30) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_plat (
id bigint not null auto_increment,
nom varchar(50) not null unique,
vegetarien bit not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant (
id bigint not null auto_increment,
nom varchar(30) not null unique,
adresse_id bigint,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant_plat (
jpa10_hb_restaurant_id bigint not null,
plat_id bigint not null,
primary key (jpa10_hb_restaurant_id, plat_id)
) ENGINE=InnoDB;
alter table jpa10_hb_restaurant
add index FK3E8E4F5D5FE379D0 (adresse_id),
add constraint FK3E8E4F5D5FE379D0
foreign key (adresse_id)
references jpa10_hb_adresse (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D11F0F78A4 (plat_id),
add constraint FK1D2D06D11F0F78A4
foreign key (plat_id)
references jpa10_hb_plat (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id),
add constraint FK1D2D06D1AFAC3E44
foreign key (jpa10_hb_restaurant_id)
references jpa10_hb_restaurant (id);
- рядки 21–26: таблиця [adresse]
- рядки 28–33: таблиця [plat]
- рядки 35–40: таблиця [restaurant]
- рядки 42–46: таблиця з’єднання [restaurant_plat]. Зверніть увагу на складений ключ (рядок 45)
- рядки 48–52: зовнішній ключ з таблиці [restaurant] до таблиці [adresse]
- рядки 54–58: зовнішній ключ таблиці [restaurant_plat] до таблиці [plat]
- рядки 60–64: зовнішній ключ таблиці [restaurant_plat] до таблиці [restaurant]
Ця таблиця DDL відповідає схемі, що вже була представлена:
![]() |
У перспективі SQL Explorer база даних виглядає наступним чином:
![]() |
- у [1]: 4 таблиці бази даних
- у [2]: адреси
- у [3]: страви
- у [4]: ресторани. [adresse_id] посилається на адреси з [2].
- у [5]: таблиця з'єднання [restaurant,plat]. [jpa10_hb_restaurant_id] посилається на ресторани з [4], а [plat_id] — на страви з [3]. Отже, [1,1] означає, що ресторан «Burger Barn» подає страву «CheeseBurger».
Для отримання наведених вище даних було запущено програму [QueryDB] з проєкту Eclipse.
2.7.5. Запити JPQL за допомогою консолі Hibernate
Ми створюємо консоль Hibernate, пов’язану з попереднім проєктом Eclipse. Ми дотримуватимемося підходу, який уже двічі висвітлювався, зокрема в розділі 2.1.12.
![]() |
- у [1] та [2]: налаштування консолі Hibernate
![]() |
- у [3]: запит JPQL, а в [4] — результат.
- у [5]: еквівалентне завдання SQL
Тепер ми наводимо серію запитів JPQL. Пропонуємо читачеві запустити їх і виявити порядок SQL, згенерований Hibernate для їх виконання.
Отримати всі ресторани разом із їхніми стравами:
![]() | ![]() |
Отримати ресторани, які пропонують хоча б одну вегетаріанську страву:
![]() | ![]() |
Отримати назви ресторанів, які подають виключно вегетаріанські страви:
![]() | ![]() |
Отримати ресторани, які подають бургери:
![]() | ![]() |
2.7.6. QueryDB
Тепер розглянемо програму [QueryDB] з проєкту Eclipse, яка:
- заповнює базу даних
- надсилає до неї певну кількість запитів JPQL. Ці запити записуються у файл [META-INF/orm.xml] проекту Eclipse:
![]() |
Файл [orm.xml] можна використовувати для налаштування шару JPA замість анотацій Java. Це забезпечує гнучкість у налаштуванні шару JPA. Її можна змінювати без перекомпіляції Java-коду. Можна використовувати обидва методи одночасно: Java-анотації та файл [orm.xml]. Налаштування JPA спочатку виконується за допомогою Java-анотацій, а потім — за допомогою файлу [orm.xml]. Отже, якщо потрібно змінити конфігурацію, створену за допомогою Java-анотації, без перекомпіляції, достатньо внести цю конфігурацію у файл [orm.xml]. Саме вона матиме пріоритет.
У нашому прикладі файл [orm.xml] використовується для збереження текстів запитів JPQL. Його вміст такий:
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
<description>Restaurants</description>
<named-query name="supprimer le contenu de la table restaurant">
<query>delete from Restaurant</query>
</named-query>
<named-query name="supprimer le contenu de la table plat">
<query>delete from Plat</query>
</named-query>
<named-query name="obtenir tous les restaurants">
<query>select r from Restaurant r order by r.nom asc</query>
</named-query>
<named-query name="obtenir toutes les adresses">
<query>select a from Adresse a order by a.nomRue asc</query>
</named-query>
<named-query name="obtenir tous les plats">
<query>select p from Plat p order by p.nom asc</query>
</named-query>
<named-query name="obtenir tous les restaurants avec leurs plats">
<query>select r.nom,p.nom from Restaurant r join r.plats p</query>
</named-query>
<named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
<query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
</named-query>
<named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
<query>
select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
p1.vegetarien=false)
</query>
</named-query>
<named-query name="obtenir les restaurants d'une certaine rue">
<query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
</named-query>
<named-query name="obtenir les restaurants qui servent des burgers">
<query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
</named-query>
<named-query name="obtenir les plats du restaurant untel">
<query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
</named-query>
</entity-mappings>
- кореневим елементом файлу [orm.xml] є <entity-mappings> (рядок 2).
- рядки 5–7: іменовані запити JPQL об’єктом тегів <named-query name= «...»>текст</namedquery>.
- Атрибут name тегу — це назва запиту.
- Зміст тегу texte — це текст запиту.
QueryDB виконає попередні запити. Його код такий:
package tests;
...
public class QueryDB {
// Контекст збереження даних
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = emf.createEntityManager();
public static void main(String[] args) {
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// видалити елементи з таблиці [restaurant]
em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
// видалити елементи з таблиці [plat]
em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
// створення об’єктів Address
Adresse adr1 = new Adresse(10, "Main Street");
Adresse adr2 = new Adresse(20, "Main Street");
Adresse adr3 = new Adresse(123, "Dover Street");
// створення об’єктів «Entree»
Plat ent1 = new Plat("Hamburger", false);
Plat ent2 = new Plat("Cheeseburger", false);
Plat ent3 = new Plat("Tofu Stir Fry", true);
Plat ent4 = new Plat("Vegetable Soup", true);
// створення об’єктів «Restaurant»
Restaurant restaurant1 = new Restaurant();
restaurant1.setNom("Burger Barn");
restaurant1.setAdresse(adr1);
restaurant1.getPlats().add(ent1);
restaurant1.getPlats().add(ent2);
Restaurant restaurant2 = new Restaurant();
restaurant2.setNom("Veggie Village");
restaurant2.setAdresse(adr2);
restaurant2.getPlats().add(ent3);
restaurant2.getPlats().add(ent4);
Restaurant restaurant3 = new Restaurant();
restaurant3.setNom("Dover Diner");
restaurant3.setAdresse(adr3);
restaurant3.getPlats().add(ent1);
restaurant3.getPlats().add(ent2);
restaurant3.getPlats().add(ent4);
// збереження об’єктів «Restaurant» (та інших об’єктів за каскадним принципом)
em.persist(restaurant1);
em.persist(restaurant2);
em.persist(restaurant3);
// завершення транзакції
tx.commit();
// зливання бази даних
dumpDataBase();
// завершення EntityManager
em.close();
// завершення EntityManagerFactory
emf.close();
}
// перегляд вмісту бази даних
@SuppressWarnings("unchecked")
private static void dumpDataBase() {
// тест2
log("données de la base");
// початок транзакції
EntityTransaction tx = em.getTransaction();
tx.begin();
// перегляд ресторанів
log("[restaurants]");
for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
System.out.println(restaurant);
}
// перегляд адрес
log("[adresses]");
for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
System.out.println(adresse);
}
// перегляди страв
log("[plats]");
for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
System.out.println(plat);
}
// перегляди зв'язків «ресторани» <--> «страви»
log("[restaurants/plats]");
Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
}
log("[Liste des restaurants avec au moins un plat végétarien]");
for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
System.out.println(r);
}
// запит
log("[Liste des restaurants avec seulement des plats végétariens]");
for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
System.out.println(r);
}
// запит
log("[Liste des restaurants dans Dover Street]");
for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
System.out.println(r);
}
// запит
log("[Liste des restaurants ayant un plat de type burger]");
record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
}
// запит
log("[Plats de Veggie Village]");
for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
System.out.println(r);
}
// завершення транзакції
tx.commit();
}
// журнали
private static void log(String message) {
System.out.println(" -----------" + message);
}
}
Результат виконання [QueryDB] такий:
Ми залишаємо читачеві можливість самостійно встановити зв’язок між кодом і результатами. Для цього радимо запустити запити JPQL у консолі Hibernate та проаналізувати відповідний код SQL.
2.7.7. Проєкт Eclipse / Toplink
Зацікавлений читач знайде серед прикладів, які можна завантажити разом із цим посібником, попередній проект, реалізований за допомогою Toplink:
![]() |
Проєкт Eclipse з Toplink є копією проєкту Eclipse з Hibernate:
![]() |
Файл <persistence.xml> [2] містить опис керованих сутностей:
<!-- постачальник -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- постійні класи -->
<class>entites.Restaurant</class>
<class>entites.Adresse</class>
<class>entites.Plat</class>
...
- рядки 4–6: керовані сутності
Запити JPQL, записані у [orm.xml], коректно виконуються Toplink. Для цього в попередньому проєкті ми подбали про те, щоб не використовувати запити HQL (Hibernate Query Language), які фактично є надмножиною JPQL і деякі синтаксичні конструкції яких не підтримуються JPQL.
2.8. Conclusion
На цьому ми завершуємо наше вивчення сутностей JPA. Це зайняло багато часу, проте деякі важливі (для досвідченого розробника) аспекти залишилися нерозглянутими. Знову ж таки, рекомендується прочитати довідкову книгу, подібну до тієї, що використовувалася для цього посібника:
[ref1]: «Java Persistence with Hibernate», авторства Крістіана Бауера та Гевіна Кінга, видавництво Manning.


















































































































