6. Spring Data JPA Hibernate
6.1. Introduction
Ми візьмемо базу даних [dbproduitscategories], що управляється проектом [spring-jdbc-04], і реалізуємо обидва інтерфейси [IDao<Categorie>, IDao<Produit>], визначені в цьому проекті. Це дасть нам кілька можливостей:
- порівняти код реалізації;
- використовувати один і той самий набір тестів;
- порівняти продуктивність двох реалізацій;
![]() |
- рівень [JDBC] реалізовано в рамках проєкту [mysql-config-jdbc], розглянутого в розділі 3.3;
Тепер перейдемо до інших рівнів.
6.2. Налаштування робочого середовища
За допомогою STS імпортуйте проект [mysl-config-jpa-hibernate] [1], який знаходиться у папці [<exemples>/spring-database-config/mysql/eclipse] [2]:
![]() |
Цей проект налаштовує рівень [Spring JPA Hibernate] проекту. Кожна реалізація JPA має власний проект налаштування.
Потім імпортуйте проект [spring-jpa-generic] [1], який знаходиться в папці [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Після цього скиньте налаштування середовища Maven (Alt-F5) для всіх проектів, що містяться в [Package Explorer]:
![]() |
Потім, щоб перевірити робоче середовище, запустіть конфігурацію виконання з назвою [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Ця конфігурація запускає тест [JUnitTestDao]. Цей тест має завершитися успішно:
![]() |
6.3. Проект конфігурації шару JPA
![]() |
Цей проект призначений для конфігурації шару JPA у наведеній нижче архітектурі:
![]() |
6.3.1. Конфігурація Maven
Проєкт є проєктом Maven і налаштовується за допомогою такого файлу [pom.xml]:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- змінні залежності ********************************************** -->
<!-- JPA — постачальник -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- постійні залежності ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- успадкована конфігурація JDBC -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 5–7: артефакт Maven, згенерований цим проєктом. Проєкти конфігурації інших реалізацій JPA (Eclipselink та OpenJpa) використовуватимуть цей самий артефакт. Це означає, що в будь-який момент часу активним може бути лише один із цих проєктів. Тому слід уникати їх одночасного існування в [Package Explorer]. Потрібен лише один;
- рядки 10–14: батьківський проект Maven, який визначає версії більшості залежностей, необхідних для проекту;
- рядки 19–22: бібліотека Hibernate;
- рядки 25–28: бібліотека Spring Data;
- рядки 32–34: проект конфігурації шару JPA базується на проекті конфігурації шару JDBC, який, серед іншого, визначає драйвер JDBC для використовуваного SGBD та координати бази даних, що має використовуватися;
- рядки 35–39: проект конфігурації шару JDBC включає бібліотеку [Spring JDBC], яка тут замінена на бібліотеку [Spring Data JPA]. Тому рекомендується не включати її до залежностей проєкту. Однак якщо її залишити, це не спричинить помилок;
У підсумку залежності проекту виглядають наступним чином:
![]() |
6.3.2. Конфігурація Spring
![]() |
Клас [ConfigJpa] налаштовує проект Spring:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// провайдер JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// пакети сутностей JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// джерело даних
@Bean
public DataSource dataSource() {
// джерело даних TomcatJdbc
DataSource dataSource = new DataSource();
// конфігурація доступу JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// спочатку відкриті з'єднання
dataSource.setInitialSize(5);
// результат
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Менеджер транзакцій
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- рядок 18: клас є класом конфігурації Spring;
- рядок 19: він імпортує біни, визначені класом конфігурації [ConfigJdbc], який використовувався для конфігурації проекту Spring [mysql-config-jdbc]. Це фільтри jSON;
- рядки 23–30: визначають реалізацію JPA, що використовується, у даному випадку реалізацію Hibernate (рядок 25);
- рядок 26: можна ввімкнути або вимкнути відображення операцій SQL, що виконуються реалізацією Hibernate;
- рядок 27: вказується Hibernate підключений SGBD. Ця конфігурація є важливою. Вона дозволяє Hibernate використовувати діалект SQL з SGBD MySQL, включаючи його пропрієтарну частину. Крім того, це надає йому інформацію про типи SQL та об’єкти SGBD, якими він зможе користуватися. Саме ця здатність реалізації JPA адаптуватися до конкретного SGBD забезпечує їй високу переносимість між SGBD;
- рядок 28: Hibernate може генерувати або не генерувати таблиці цільової бази даних на основі сутностей JPA, які він знайде. Це генерування відбувається лише в тому випадку, якщо таблиці відсутні. Якщо вони вже присутні, ніяких дій не виконується. Ми скористаємося цією можливістю генерування таблиць, коли розповімо, як були створені скрипти SQL для генерації різних баз даних, що використовуються в цьому документі;
- рядок 33: пакет, у якому містяться сутності JPA з бази [dbproduitscategories];
- рядки 36–49: джерело даних [tomcat-jdbc], пов’язане з базою даних [dbproduitscategories];
- рядки 52–60: бін із назвою [entityManagerFactory] (він має називатися саме так) — це бін, який створить об’єкт [EntityManager], що керує контекстом персистентності JPA. Усі операції JPA проходять через нього. Використання [Spring Data JPA] означає, що ми самі ніколи не будемо використовувати цей об’єкт. Однак нам потрібно його налаштувати. Йому потрібно знати наступне:
- використовувану реалізацію JPA (рядок 55);
- джерело даних, що використовується (рядок 57);
- ентітети JPA з цього джерела (рядок 56);
- рядок 58: ініціалізує EntityManager за допомогою цієї інформації;
- рядок 59: повертає синготон [entityManagerFactory];
- рядки 63–68: визначають менеджер транзакцій. Він повинен мати назву [transactionManager];
- рядок 65: створюється обробник транзакцій JPA;
- рядок 66: він пов’язаний із джерелом даних із рядка 37 за допомогою біна [entityManagerFactory] (рядки 53 та 57);
Лише bean із рядків 23–30 залежить від використовуваної реалізації JPA. Інші bean потім спираються на нього.
6.3.3. Елементи шару [JPA]
![]() |
![]() |
Цільовою базою даних є база [dbproduitscategories] з двома таблицями: [CATEGORIES] та [PRODUITS]. Ми бачили, що вона також містить три інші таблиці [USERS, ROLES, USERS_ROLES], які будуть використовуватися для забезпечення безпеки веб-сервісу, що буде розгорнуто в Інтернеті. Поки що ми проігноруємо ці таблиці. Нагадаємо для довідки структуру таблиць [CATEGORIES] та [PRODUITS]:
Таблиця [PRODUITS] має такий вигляд:
![]() |
- [ID]: автоінкрементний первинний ключ таблиці [2];
- [NOM]: унікальна назва товару [4];
- [PRIX]: ціна товару;
- [DESCRIPTION] — опис товару;
- [VERSIONING] — номер версії товару. Його початкова версія — 1 [3]. Кожного разу, коли товар буде змінено, його номер версії збільшуватиметься на одиницю за допомогою коду, що використовує цю таблицю;
- [CATEGORIE_ID]: зовнішній ключ у таблиці [CATEGORIES] для позначення категорії, до якої належить продукт;
![]() |
- у [1-3] — зовнішній ключ [CATEGORIE_ID] з таблиці [PRODUITS]. Вона стосується стовпця [ID] таблиці [CATEGORIES] [4-5];
- коли категорія видаляється, видаляються також усі пов’язані з нею товари [6]. Цей момент важливо відзначити, оскільки він використовується при побудові шару [DAO], що використовує базу [dbproduitscategories];
Таблиця категорій [CATEGORIES] має такий вигляд:
![]() |
- [ID]: первинний ключ з автоінкрементом;
- [VERSIONING]: номер версії категорії;
- [NOM]: унікальна назва категорії;
Тепер ми опишемо сутності JPA, [Produit] та [Categorie] — зображення з таблиць [PRODUITS] та [CATEGORIES].
![]() |
6.3.3.1. Інтерфейс [AbstractCoreEntity]
Інтерфейс [AbstractCoreEntity] реалізовано об’єктами JPA, [Categorie] та [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// методи отримання та встановлення значень полів [id], [version], [entityType]
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
Цей інтерфейс, реалізований двома сутностями JPA, слугує лише для переліку методів читання/запису полів [id], [version] та [entityType] цих сутностей. Роль поля [entityType] буде пояснено пізніше;
6.3.3.2. Ентітет JPA [Produit]
Клас [Produit] — це суть JPA, пов’язана з рядком таблиці [PRODUITS]:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// властивості
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// властивості
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// категорія
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// виробники
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// підпис
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// ------------------------------------------------------------
// перевизначення [equals] та [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// гетери та сеттери
...
public void setCategorie(Categorie categorie) {
// тип сутності
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- рядок 21: анотація [@Entity] робить клас [Produit] об’єктом, що керується шаром [JPA]. Також можна написати [@Entity(name="MonProduit")], що надає об’єкту ім’я [MonProduit]. За відсутності цієї інформації ім’я об’єкта дорівнює імені класу, у даному випадку — [Produit]. Таке іменування стає необхідним, коли серед об’єктів є два класи з різних пакетів, що мають однакову назву;
- рядок 22: анотація [@Table(name = "PRODUITS")] вказує, що клас [Produit] є об’єктним зображенням рядка таблиці [PRODUITS] у базі даних;
- рядок 23: ім’я фільтра jSON, який слід застосувати до об’єкта. Ми побачимо, що властивість [categorie] у рядку 58 не завжди доступна. Тоді його потрібно виключити з представлення об’єкта jSON. Для цього нам потрібен фільтр. Отже, саме у фільтрі з назвою [jsonFilterCategorie] ми вкажемо, чи хочемо ми мати властивість [categorie] чи ні;
- рядок 26: анотація [@Id] робить анотоване поле полем, пов’язаним із первинним ключем таблиці з рядка 19;
- рядок 27: анотація [@GeneratedValue(strategy = GenerationType.IDENTITY)] визначає режим автоматичного формування первинного ключа в таблиці [PRODUITS]. Це визначається атрибутом [strategy]. Існують різні режими:

Стратегія [IDENTITY] доступна не для всіх SGBD. Серед шести протестованих SGBD вона була доступна для SGBD та [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. Для двох інших [Oracle Express 11g Release 2, Firebird 2.5.4] довелося використовувати стратегію [SEQUENCE]. Для забезпечення сумісності між реалізаціями JPA не слід використовувати стратегію [AUTO], яка залишає вибір стратегії генерації первинного ключа на розсуд реалізації JPA. Отже, з MySQL 5 та стратегією [AUTO]:
- Hibernate обирає стратегію [IDENTITY] із режимом [AUTO_INCREMENT] для первинного ключа;
- EclipseLink обирає стратегію [TABLE], яка створює таблицю, що за замовчуванням називається [SEQUENCE], до якої потрібно звернутися, щоб отримати первинні ключі.
У підсумку структура бази даних, що керується цими двома реалізаціями JPA, не є однаковою. Якщо вона була згенерована Hibernate, її не можна буде використовувати з EclipseLink і навпаки.
- рядок 28: анотація [@Column(name="ID"] визначає ім’я стовпця таблиці [PRODUITS], який слід пов’язати з полем [id];
- рядок 29: для первинного ключа використовується тип [Long] замість [long]. Справа в тому, що первинні ключі [null] мають особливе значення для JPA. Тому тут краще використовувати об’єктний тип, а не простий тип;
- рядок 31: анотація [@Version] вказує, що поле [version] пов’язане зі стовпцем версій. Реалізація JPA буде збільшувати цей номер версії щоразу, коли суть буде змінена. Цей номер слугує для запобігання одночасному оновленню сутності двома різними користувачами: два користувачі U1 та U2 читають сутність E з номером версії, що дорівнює V1. U1 змінює E та зберігає цю зміну в базі даних: номер версії тоді змінюється на V1+1. U2, у свою чергу, змінює E та зберігає цю зміну в базі даних: він отримає виняток, оскільки має версію (V1), відмінну від тієї, що міститься в базі даних (V1+1);
- рядок 36: тип об’єкта. Їх буде два: POJO та PROXY. За замовчуванням екземпляр, що генерується, матиме тип POJO (Plain Old Java Object). У деяких випадках екземпляри [Produit], отримані з бази даних, матимуть тип [PROXY]. Це трапиться, якщо властивість [Categorie categorie] у рядку 58 не буде ініціалізована з категорією через атрибут [fetch = FetchType.LAZY] у рядку 56. У цьому випадку реалізації JPA, які будуть тестуватися, відрізняються:
- [Hibernate, OpenJPA]: доступ до категорії товару типу [PROXY] викликає виняток. У Hibernate термін «проксі» використовується для позначення екземпляра JPA, отриманого в режимі [LAZY]. Саме тому я використав цей термін для позначення цього типу сутності;
- [EclipseLink]: доступ до категорії товару типу [PROXY] призводить до пошуку цієї категорії в базі даних, і винятку не виникає;
Оскільки я хотів мати рівень тестування, незалежний від використовуваної реалізації JPA, мені потрібно було знати тип кожної сутності: POJO чи PROXY. Саме тому я додав поле [entityType] до сутностей JPA;
- рядок 35: примітка [@Transient] вказує, що реалізація JPA повинна ігнорувати це поле. Адже воно не існує в таблицях SGBD;
- рядок 40: клас [Produit] генерує виняток типу [ProxyException], якому потрібна назва класу;
- рядок 38: як і раніше, вказано, що реалізація JPA повинна ігнорувати це поле;
- рядок 39: анотація [@JsonIgnore] вказує, що серіалізатор/десеріалізатор jSON екземпляра [Produit] повинен ігнорувати це поле;
- рядок 43: анотація [@Column] пов’язує поле [nom] зі стовпцем [NOM] таблиці [PRODUITS]. Коли поле має таку саму назву, як і пов’язаний стовпець (без урахування регістру), анотацію [@Column] можна опустити. Саме так і є в даному випадку. Атрибути [unique = true, length = 30, nullable = false] використовуються лише тоді, коли реалізація JPA має згенерувати таблицю [CATEGORIES] на основі сутності [Produit]. Вони будуть перетворені на атрибути SQL та [UNIQUE, VARCHAR(30), NOT NULL], завдяки яким стовпець [NOM] матиме не більше 30 символів, буде унікальним у таблиці та не зможе мати значення NULL;
- рядки 46–47: поле [idCategorie] пов’язане зі стовпцем [CATEGORIE_ID]. До його атрибутів ми повернемося трохи пізніше;
- рядки 49–50: поле [prix] пов’язане зі стовпцем [PRIX];
- рядки 52–53: поле [description] пов’язане зі стовпцем [DESCRIPTION];
- рядки 56–58: категорія товару;
- рядок 56: анотація [@ManyToOne] вказує, що стовпець анотації рядка 57 [@JoinColumn(name = "CATEGORIE_ID")] є зовнішнім ключем таблиці [PRODUITS]сутності [Produit] у таблиці [CATEGORIES], пов’язаній із сутністю у рядку 58. Ця анотація має анотувати сутність JPA. Отже, клас у рядку 58 має бути об’єктом JPA;
- рядок 56: анотація [fetch = FetchType.LAZY] вимагає, щоб під час вилучення продукту з таблиці [PRODUITS] його категорія (рядок 58) не вилучалася одразу (lazy loading). Вона отримується під час першого виклику методу [getCategorie]. Для цього під час виконання шар JPA доповнює початковий метод [getCategorie] (який обмежується поверненням поля categorie) викликом методу SGBD для отримання категорії — техніка, що називається «проксінг». Реалізації JPA відрізняються у втіленні цієї характеристики, як ми вже зазначали вище. Цей атрибут не є обов’язковим. Використана реалізація JPA має право ігнорувати його. Саме тому, що властивість [categorie] може бути присутньою або відсутньою, ми ввели фільтр jSON у рядку 23. Стовпець з'єднання [CATEGORIE_ID] таблиці [PRODUITS] оновлюється автоматично під час вставки або оновлення товару. Вона отримує значення з [categorie.getId()], де [categorie] — це поле рядка 58. Специфікація JPA передбачає, що цей стовпець з’єднання не може оновлюватися іншим способом. Також вона визначає атрибути [insertable = false, updatable = false] у рядку 46, завдяки яким стовпець [CATEGORIE_ID] (тобто стовпець з’єднання), пов’язаний з полем [idCategorie], не може бути змінений полем [idCategorie]. Можливим буде лише перенесення стовпця [CATEGORIE_ID] у поле [idCategorie];
- рядки 91–104: рівність між сутностями [Produit] визначається як рівність між їхніми первинними ключами [id];
- рядки 108–115: щоб зробити наш тестовий шар переносимим, ми будемо однаково обробляти сутності [PROXY] у трьох реалізаціях JPA та [Hibernate, EclipseLink, OpenJpa]. Для об’єкта типу [Produit], що належить до типу [PROXY], заборонено змінювати значення поля [categorie]. Клас [ProxyException] має такий вигляд:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
На завершення розгляду цієї сутності слід зазначити, що анотації та їхні атрибути використовуються у двох чітко відмінних випадках:
- для створення таблиць бази даних;
- для їх використання. У цьому випадку реалізація JPA очікує знайти таблиці такими, якими вона б їх сама згенерувала. Тому до попередньої сутності [Produit] не можна прив’язати будь-яку таблицю [PRODUITS]. Вона повинна мати принаймні (може мати й інші) характеристики таблиці [PRODUITS], яку б вона сама згенерувала. Під час роботи з JPA найкраще починати з порожньої бази даних, у якій JPA самостійно генерує таблиці. Ми розглянемо цей процес генерації трохи пізніше. Скрипт SQL, наданий для SGBD та MySQL, було згенеровано на основі таблиць, створених JPA.
Усі атрибути об’єкта [Produit] використовуються для генерації таблиці [PRODUITS]. Після цього атрибути генерації, такі як [unique = true, length = 30, nullable = false], більше не використовуються під час обробки таблиць.
6.3.3.3. Об’єкт JPA [Categorie]
Клас [Categorie] є сутністю JPA, пов’язаною з рядком таблиці [CATEGORIES]:
![]() |
Її код такий:
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// властивості
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// пов'язані продукти
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// конструктори
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// сигнатура
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// методи
public void addProduit(Produit produit) {
// тип об'єкта
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// додавання товару
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// додаємо товар
produits.add(produit);
// визначаємо його категорію
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// перевизначення [equals] та [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// методи getter та setter
...
}
- рядок 24: клас є об’єктом JPA;
- рядок 25: пов’язаний з таблицею [CATEGORIES];
- рядок 26: представлення jSON сутності [Categorie] керується фільтром з назвою [jsonFilterCategorie]. Його слід налаштувати перед будь-яким запитом на представлення сутності jSON. Фільтр [jsonFilterCategorie] використовуватиметься для виключення або включення поля [produits] із рядка 40 з представлення jSON сутності [Categorie];
- рядки 29–32: поле [id] пов’язане з первинним ключем [ID] таблиці [CATEGORIES]. Обрано режим генерації [IDENTITY], отже, для MySQL — режим [AUTO_INCREMENT];
- рядки 34–36: поле [version] пов’язане зі стовпцем версій [VERSIONING] таблиці [CATEGORIES];
- рядки 38–39: тип сутності [Categorie];
- рядки 41–43: просте ім’я класу [Categorie];
- рядки 46–47: поле [nom] пов’язане зі стовпцем [NOM] таблиці [CATEGORIES]. Йому присвоюються атрибути JPA та [unique = true, length = 30, nullable=false], щоб під час формування таблиці [CATEGORIES] стовпець [NOM] мав атрибути SQL та [UNIQUE, VARCHAR(30), NOT NULL];
- рядки 50–51: товари, що належать до категорії;
- рядок 50: анотація [@OneToMany] є оберненим відношенням до відношення [@ManyToOne], яке ми зустрічали в сутності [Produit]. Атрибут [mappedBy = "categorie"] вказує на поле сутності [Produit], позначене зворотним зв’язком [@ManyToOne]. Атрибут [cascade = { CascadeType.ALL }] вимагає, щоб операції (persist, merge, remove), виконані над @Entity [Categorie], поширювалися каскадно на [produits] у рядку 51. Можна вказати часткові каскадні дії за допомогою констант [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- рядок 50: атрибут [fetch = FetchType.LAZY] вимагає, щоб при витягуванні категорії з таблиці [CATEGORIES] її товари не витягувалися одразу. Вони витягуються під час першого виклику методу [getProduits]. Для цього під час виконання шар JPA доповнює початковий метод [getProduits] (який лише повертає поле produits) викликом методу SGBD для отримання товарів цієї категорії. Цей атрибут є обов’язковим. Реалізація JPA не може його ігнорувати. Оскільки властивість [produits] може бути ініціалізована або ні, ми ввели фільтр jSON у рядку 26, який дозволить вказати, чи потрібна ця властивість, а також тип сутності у рядку 39;
- рядки 71–88: метод [addProduit] дозволяє додати товар до категорії;
- рядки 73–76: щоб уніфікувати управління проксі-серверами між різними реалізаціями JPA, було вирішено, що до сутності [Categorie] типу PROXY не можна додавати товари;
- рядки 92–112: дві сутності [Categorie] вважатимуться рівними, якщо вони мають однаковий первинний ключ [id];
6.3.4. Файл [persistence.xml]
![]() |
Додатки JPA повинні визначити певні властивості використовуваного постачальника JPA, а також сутності JPA, які слід використовувати, у файлі [META-INF/persistence.xml], що знаходиться у Classpath додатка. У наведеному вище прикладі цей файл розміщено в папці [src/main/resources], яка фактично входить до Classpath проекту Eclipse. При використанні JPA разом із Spring певна інформація, яка мала б міститися у файлі [persistence.xml], розміщується в інших місцях — у класах конфігурації Spring. У додатку Spring JPA саме Spring керує JPA. З Spring JPA Hibernate файл [persistence.xml] можна звести до найпростішого вигляду:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- рядки 1–5: файл [persistence.xml] повинен містити кореневий тег <persistence>. Атрибути тегу в рядку 2 у цьому додатку не використовуються;
- файл персистентності може визначати одну або кілька одиниць персистентності за допомогою тегу <persistence-unit> (рядок 4). Одиниця персистентності керує доступом до певної бази даних. Якщо додаток одночасно керує двома базами даних, він матиме дві одиниці персистентності;
- рядок 4: одиниця персистентності має ім’я [attribut name], підтримує тип транзакції [attribut transaction-type], має властивості та визначає сутності, пов’язані з таблицями бази даних, що керується цією одиницею персистентності. Оскільки в даному випадку доступ до бази даних буде керуватися модулем [Spring JPA Hibernate], ці дві останні відомості можна розмістити в іншому місці. Існує два типи транзакцій:
- [RESOURCE_LOCAL]: транзакції керуються самою програмою. Саме так і відбувається в даному випадку, де транзакції керуватиме Spring;
- [JTA] (Java Transaction API): контейнер EJB (Enterprise Java Bean), який виконує додаток, автоматично керуватиме транзакціями на основі анотацій Java, знайдених у коді. У даній конфігурації це не так;
Пізніше ми побачимо, що вміст цього файлу [persistence.xml] залежить від використовуваної реалізації JPA.
6.4. Проєкт [spring-jpa-generic]
Нагадаємо, що ми хочемо зробити. Ми хочемо реалізувати таку архітектуру:
![]() |
у якій рівень [DAO] реалізовуватиме інтерфейс [IDao<Produit>, IDao<Categorie>], розглянутий у розділі 4. Йдеться про порівняння двох реалізацій цього інтерфейсу:
- одну, побудовану за допомогою Spring JDBC;
- інша — створена за допомогою Spring JPA;
У наведеній вище архітектурі:
- рівень [JDBC] реалізовано проектом [mysql-config-jdbc], розглянутим у параграфі 3.3;
- шар [JPA] реалізовано проектом [mysql-config-jpa-hibernate], розглянутим у розділі 6.3;
Проєкт [spring-jpa-generic] забезпечує реалізацію рівнів [DAO] та [Spring Data].
![]() |
6.4.1. Конфігурація Maven
Проєкт [spring-jpa-generic] — це проєкт Maven, налаштований за допомогою такого файлу [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- конфігурація JPA для SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 22–26: проект має лише одну залежність — від проекту, що налаштовує рівень [JPA] додатка, який ми щойно розглянули. Це універсальний додаток:
- змінюємо SGBD, змінюючи проект конфігурації шару [JDBC];
- зміна реалізації JPA здійснюється шляхом зміни конфігураційного проєкту шару [JPA];
У підсумку залежності виглядають так:
![]() |
6.4.2. Конфігурація Spring
![]() |
Клас [AppConfig] налаштовує проект Spring:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- рядок 11: клас є класом конфігурації Spring;
- рядок 10: анотація [@EnableJpaRepositories] використовується для позначення пакетів, що містять інтерфейси [CrudRepository] з Spring Data. Це робить їх компонентами Spring, які можуть бути введені в інші компоненти Spring;
- рядок 12: анотація [@ComponentScan] вказує, що пакет [spring.data.dao] потрібно просканувати на наявність компонентів Spring. Будуть знайдені компоненти [DaoCategorie] та [DaoProduit];
- рядок 13: імпортуються біни з класу конфігурації [ConfigJpa]. Серед них буде знайдено бін реалізації JPA, що використовується (Hibernate, Eclipselink, OpenJpa), джерело даних, яке буде використовуватися, EntityManager, що керуватиме операціями JPA, та менеджер транзакцій;
6.4.3. Рівень [Spring Data]
![]() |
![]() |
6.4.3.1. Інтерфейс [CategoriesRepository]
Інтерфейс [CategoriesRepository] керує доступом до таблиці [CATEGORIES]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// категорія з її товарами
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- рядок 10: інтерфейс [CrudRepository] було використано та пояснено в розділі 5.1.3. Нагадуємо, що:
- перший тип параметра інтерфейсу — це об’єкт JPA, який керується для операцій доступу CRUD (findOne, findAll, збереження, видалення, deleteAll),
- другий тип параметра інтерфейсу — це первинний ключ сутності JPA, у даному випадку ціле число [Long];
Методи інтерфейсу реалізуються за допомогою запитів JPQL (Java Persistence Query Language). Цей запит стосується сутностей JPA. У такому запиті:
- таблиці замінюються відповідними сутностями JPA;
- стовпці замінюються на поля сутностей JPA, що використовуються у запиті;
Розглянемо приклад рядків 31–32: метод у рядку 32 повертає всі категорії з бази даних у скороченому вигляді. Він реалізований за допомогою запиту JPQL (Java Persistence Query Language) у рядку 31, який дуже схожий на свій аналог SQL. Щоб детальніше ознайомитися з JPQL, можна прочитати [ref2] (див. параграф 1.2).
Методи інтерфейсу [CategoriesRepository] такі:
- рядки 13–14: метод [getLongCategorieById] повертає розгорнуту версію категорії, на яку посилається її первинний ключ [id], тобто категорію разом із її товарами. Нагадаємо, що в сутності [Categorie] поле [produits] мало атрибут [fetch = FetchType.LAZY] (відкладене завантаження). У запиті JPQL ми примусово завантажуємо товари за ключовим словом [fetch]. Параметр ?1 запиту під час виконання буде замінено значенням першого параметра методу в рядку 12, тобто параметром [Long id];
- рядки 16–17: метод [getLongCategorieByName] повертає повну версію категорії, на яку посилається її назва [nom];
- рядки 19–20: метод [getShortCategoriesByName] повертає короткі версії категорій, на які посилаються за їхніми іменами. Поле [produits] цих категорій не дорівнює null. Воно містить посилання на проксі (клас, створений реалізацією JPA), роль якого полягає в тому, щоб повертати товари категорії під час виклику. Його виклик поза контекстом персистентності JPA спричиняє виняток (Hibernate та OpenJpa, але не EclipseLink). З цієї причини ми не будемо використовувати поле [produits] у скороченій версії категорії;
- рядки 22–23: метод [getShortCategoriesById] повертає короткі версії категорій, на які посилаються їхні первинні ключі [id];
- рядки 25–26: метод [getLongCategoriesById] повертає повні версії категорій, на які посилаються їхні первинні ключі [id];
- рядки [28-29]: метод [getLongCategoriesByName] повертає повні версії категорій, на які посилаються їхні назви;
- рядки 31–32: метод [getAllShortCategories] повертає короткі версії всіх категорій;
- рядки 34–35: метод [getAllLongCategories] повертає повні назви всіх категорій;
Примітка: не всі реалізації JPA підтримують той самий синтаксис, що й JPQL. Так, наступний синтаксис приймається Hibernate та EclipseLink, але не OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa не підтримує вищезазначений псевдонім [p].
6.4.3.2. Інтерфейс [ProduitsRepository]
Інтерфейс [ProduitsRepository] керує доступом до таблиці [PRODUITS]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// товар із його категорією
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- записи [15-16]: метод [getLongProduitById] повертає розгорнуту версію товару, ідентифікованого за його первинним ключем [id], тобто разом із його категорією. Нагадаємо, що в сутності [Produit] поле [categorie] мало атрибут [fetch = FetchType.LAZY] (відкладене завантаження). У запиті JPQL ми примусово завантажуємо категорію за допомогою ключового слова [fetch];
- рядки 18–19: метод [getLongProduitByName] повертає повну версію товару, ідентифікованого за назвою;
- рядки 21–22: метод [getShortProduitsById] повертає коротку версію товарів, ідентифікованих за їхнім первинним ключем [id]. У цій короткій версії поле [categorie] не має значення null. Воно містить посилання на проксі, згенерований реалізацією JPA, який, у разі виклику, отримає категорію товару. Цей виклик можна здійснити лише в контексті персистенції JPA. Виконання його в іншому місці спричиняє виняток (Hibernate та OpenJpa, але не EclipseLink). Отже, у шарі [DAO] або деінде ми не будемо використовувати поле [categorie] товару в його скороченій версії. У скороченій версії товару ініціалізується поле [idCategorie]. Його значенням є первинний ключ категорії, до якої належить товар. Це дозволяє згодом отримати цю категорію з шару [DAO] за допомогою методу [DaoCategorie. getShortCategoriesById(idCategorie)];
- рядки 24–25: метод [getShortProduitsByName] повертає скорочену версію товарів, ідентифікованих за їхніми назвами;
- рядки 27–28: метод [getLongProduitsById] повертає повну версію продуктів, ідентифікованих за їхніми первинними ключами;
- рядки 30–31: метод [getLongProduitsByName] повертає повну версію продуктів, ідентифікованих за їхніми назвами;
- рядки 33–34: метод [getAllLongProduits] повертає повну версію всіх продуктів;
- рядки 36–37: метод [getAllShortProduits] повертає коротку версію всіх продуктів;
Ці інтерфейси будуть реалізовані класами, що генеруються реалізацією JPA під час виконання проекту. Такі класи називаються класами [proxy]. За замовчуванням методи інтерфейсу [CrudRepository] виконуються в транзакції. Те, що інтерфейси [ProduitsRepository, CategoriesRepository] успадковують клас [CrudRepository], робить їх компонентами Spring. Таким чином, їх можна ін’єктувати в інші компоненти Spring.
6.4.4. Рівень [DAO]
![]() |
![]() |
6.4.4.1. Інтерфейс [IDao<T>]
Інтерфейс [IDao<T>] — це той самий, що вже розглядався при реалізації шару [DAO], виконаної за допомогою Spring JDBC (див. параграф 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// перелік усіх об’єктів T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// окремих об’єктів — коротка версія
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// окремих об’єктів — повна версія
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// оновлення декількох об’єктів
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// видалення всіх об’єктів
public void deleteAllEntities();
// видалення декількох об’єктів
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
6.4.4.2. Абстрактний клас [AbstractDao]
![]() |
Абстрактний клас [AbstractDao] є батьківським класом для класів, що реалізують рівень [DAO]:
- клас [DaoProduit], який реалізує інтерфейс [IDao<Produit>] та керує доступом до таблиці [PRODUITS];
- клас [DaoCategorie], який реалізує інтерфейс [IDao<Categorie>] та керує доступом до таблиці [CATEGORIES];
Його код відповідає описаному в розділі 4.8, за винятком наступної деталі: жоден метод не має атрибута [@Transactional], який забезпечує виконання методу в транзакції. Тут використовується той факт, що інтерфейси [CrudRepository] з Spring Data за замовчуванням виконуються в транзакції.
6.4.4.3. Клас [DaoCategorie]
![]() |
Клас [DaoCategorie] реалізує інтерфейс [IDao<Categorie>] наступним чином:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- рядок 17: анотація [@Component] робить клас [DaoCategorie] компонентом Spring;
- рядок 18: клас [DaoCategorie] успадковує клас [AbstractDao<Categorie>], завдяки чому він реалізує інтерфейс [IDao<Categorie>];
- рядки 20–24: ін’єкція посилань на обидва інтерфейси [CrudRepository] та [Spring Data]. Ця ін’єкція відбуватиметься під час інстанціювання об’єктів Spring, зазвичай на початку виконання проекту Spring;
- усі методи класу делегують роботу методам з такими самими іменами в інтерфейсах [CrudRepository];
- усі методи, які перетворюють сутності у їх скорочену версію, вказують на це, встановлюючи тип сутності як [EntityType.PROXY] (рядки 29, 63, 72);
Метод [saveEntities] заслуговує на пояснення:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// відзначаємо товари, які будуть додані
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// скористаємося нагодою, щоб відновити (за потреби) зв’язок «товар» → «категорія»
produit.setCategorie(categorie);
}
}
}
// зберігаємо категорії / товари
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// оновлюємо поле [idCategorie] для доданих товарів
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// результат
return categories;
}
- рядок 2: категорії, передані як параметри, є як категоріями для вставки ([id==null]), так і категоріями для редагування ([id!=null]);
- рядок 20: категорії зберігаються за допомогою методу [categoriesRepository.save(entities)]. Під час тестування було виявлено, що поле [idCategorie] збережених товарів (id==null) не заповнене. Щоб вирішити цю проблему, у рядках 4–17 зазначаються товари, які будуть вставлені, і після їх збереження заповнюється їхнє поле [idCategorie] (рядки 25–27);
- рядки 5–17: проглядаємо список категорій;
- рядки 8–16: для кожної категорії переглядається її список товарів. Тут виникає складність. Метод [saveEntities] використовується як для збереження, так і для редагування категорії. В останньому випадку категорія могла бути отримана у скороченій версії, тобто з посиланням на проксі-метод у полі [produits]. Використання цього методу з Hibernate призводить до виникнення винятку, оскільки категорія, що обробляється, більше не перебуває в контексті збереження JPA, який було закрито після завершення транзакції методу, що повернув скорочені версії категорій. Тоді використовується поле [EntityType] сутності [Categorie] у рядку 8, щоб визначити, чи можна отримати доступ до списку товарів цієї категорії;
- рядок 14: товар пов’язується зі своєю категорією. Зазвичай це вже має бути зроблено. Однак невідомо, як саме було створено цей товар і чи був він пов’язаний зі своєю категорією. Тож, щоб уникнути будь-яких проблем (для управління сутністю [Produit] сутності JPA потрібно, щоб вона посилалася на сутність [Categorie], з якою вона пов’язана), ми встановлюємо це зв’язування самостійно.
Порівнюючи цей код із кодом класу [DaoProduit] у реалізації Spring JDBC (див. розділ 4.9) можна побачити, що бібліотека Spring Data JPA значно полегшує написання шару [DAO].
6.4.4.4. Клас [DaoProduit]
![]() |
Клас [DaoProduit] реалізує інтерфейс [IDao<Produit>] наступним чином:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// відновлюємо (за потреби) зв’язок між товаром та його категорією
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// зберігаються товари
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
Код аналогічний коду класу [DaoCategorie]:
- щодо розгорнутих назв категорій, під час тестування було виявлено, що поле [idCategorie] товарів не заповнене. Метод [linkLongProduitsToCategories] у рядках 96–105 усуває цю проблему;
- метод [saveEntities] у рядках 108–121 додає нові товари або змінює існуючі. Шар JPA вимагає, щоб кожна суть [Produit] була пов’язана з сутністю [Categorie]. Оскільки невідомо, чи зробив це користувач, ми робимо це самостійно у рядках 110–113. Достатньо пов’язати [Produit] з об’єктом [Categorie], первинний ключ якого дорівнює полю [idCategorie] об’єкта [Produit]. Під час тестування ми бачимо, що виникає помилка, якщо вказати null як версію категорії. Тому тут ми вказуємо значення 0, але можна вказати будь-яке значення. Окрім первинного ключа, жодне поле сутності [Categorie] не є обов’язковим для рівня JPA для вставки/редагування сутності [Produit];
6.4.5. Тестовий рівень
![]() |
![]() |
Вищезазначені тести ідентичні тестам у реалізації Spring JDBC. У разі потреби див. такі сторінки:
- [JUnitTestCheckArguments]: параграф 4.11.1;
- [JUnitTestDao]: параграф 4.11.2;
- [JUnitTestPushTheLimits]: параграф 4.11.3;
Ми використовуємо такі конфігурації виконання:
![]() | ![]() |
![]() | ![]() |
Результати, отримані в ході різних тестів, такі:
![]() | ![]() |
![]() |
У [1] тест [JUnitTestPushTheLimits] з реалізацією Spring Data JPA Hibernate, а в [2] — з реалізацією Spring JDBC. Видно, що остання є більш продуктивною. Отже, ми приходимо до першого висновку: розробляти шар [DAO] із Spring Data JPA значно простіше, але він менш продуктивний, ніж реалізація Spring JDBC.
Тест [JUnitTestProxies] є фіктивним тестом JUnit. Він призначений для демонстрації поведінки кожної реалізації JPA при роботі з проксі, тобто з короткими версіями сутностей:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// шар [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// очищаємо базу даних перед кожним тестом
log("Vidage de la base de données", 1);
// очищається таблиця [CATEGORIES] і, відповідно, таблиця [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// заповнюються таблиці
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
categorie.setProduits(new ArrayList<Produit>());
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// додавання категорії — каскадно також будуть додані товари
// додані
daoCategorie.saveEntities(categories);
// результат
return categories;
}
@Test
public void getShortCategoriesByName1() {
// заповнення
fill(1, 1);
// тест
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// заповнення
fill(1, 1);
// тест
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// заповнення
fill(1, 1);
// тест
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// заповнення
fill(1, 1);
// тест
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private void log(String message, int mode) {
// вивести повідомлення
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
Отримано такі результати:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
Тут видно, що при доступі до поля [Categorie.produits] категорії типу PROXY та до поля [Produit.categorie] товару типу PROXY в обох випадках (рядки 7 і 17) виникає виняток типу [org.hibernate.LazyInitializationException].



































