JavaRush /Java блог /Random UA /JPA: Знайомство з технологією
Viacheslav
3 рівень

JPA: Знайомство з технологією

Стаття з групи Random UA
Сучасний світ розробки сповнений різних специфікацій, покликаних спростити життя. Знаючи інструменти, можна вибрати відповідний. Не знаючи, можна ускладнити собі життя. Цей огляд відкриє завісу таємниці над поняттям JPA - Java Persistence API. Сподіваюся, після прочитання захочеться поринути у цей таємничий світ ще глибше.
JPA: Знайомство з технологією - 1

Вступ

Як ми знаємо, одне з основних завдань програм - зберігання та обробка даних. У давні часи люди зберігали дані просто у файлух. Але як тільки потрібний одночасний доступ на читання та редагування, коли з'являється навантаження (тобто одночасно надходить кілька звернень), зберігання даних просто у файлух стає проблемою. Докладніше про те, які проблеми вирішують БД і яким чином, раджу прочитати у статті " Як влаштовані бази даних ". Отже, наші дані ми вирішуємо зберігати у базі даних. З давніх-давен Java вміє працювати з базами даних за допомогою JDBC API (The Java Database Connectivity). Детальніше про JDBC можна прочитати тут: JDBC або з чого все починаєтьсяАле час йшов і розробники щоразу стикалися з необхідністю писати однотипний і непотрібний "обслуговуючий" код (так званий Boilerplate code) для тривіальних операцій зі збереження Java об'єктів у БД і навпаки, створення Java об'єктів за даними з БД. І тоді для вирішення цих проблем на світ з'явилося таке поняття, як ORM.ORM - Object-Relational Mapping або в перекладі на російську об'єктно-реляційне відображення.Це технологія програмування, яка пов'язує бази даних з концепціями об'єктно-орієнтованих мов програмування. Java об'єктів та записів у БД: JPA : Знайомство з технологією - 2ORM — це по суті концепція у тому, що Java об'єкт можна як дані у БД (і навпаки). Вона знайшла втілення у вигляді специфікації JPA Java Persistence API. Специфікація - це вже опис Java API, який виражає цю концепцію. Специфікація розповідає, якими засобами ми маємо бути забезпечені (тобто через які інтерфейси ми зможемо працювати), щоб працювати за концепцією ORM. І як використати ці кошти. Реалізацію коштів специфікація не визначає. Це дозволяє використовувати для однієї специфікації різні реалізації. Можна спростити і сказати, що специфікація - це опис API. Текст специфікації JPA можна знайти на сайті Oracle: JSR 338: JavaTM Persistence APIОтже, щоб використовувати JPA нам потрібна деяка реалізацію, за допомогою якої ми будемо користуватися технологією. Реалізації JPA ще називають JPA Provider. Однією з найпомітніших реалізацій JPA є Hibernate. Тому, пропоную її і розглянути .
JPA : Знайомство з технологією - 3

Створення проекту

Оскільки JPA - це про Java, то нам знадобиться Java проект. Ми могли б самі створити вручну структуру каталогів, самі додати потрібні бібліотеки. Але куди зручніше і правильніше використовувати системи автоматизації складання проектів (тобто по суті це просто програма, яка за нас керуватиме складання проектів. Створювати каталоги, підкладати в classpath потрібні бібліотеки тощо). Однією з таких систем є Gradle. Детальніше про Gradle можна прочитати тут: " Коротке знайомство з Gradle ". Як відомо, функціональність Gradle (тобто дії, які може зробити) реалізовані з допомогою різних Gradle Plugins. Скористаємося Gradle та плагіном " Gradle Build Init Plugin'ом ". Виконаємо команду:

gradle init --type java-application
Gradle за нас зробить потрібну структуру каталогів, створить базовий декларативний опис проекту в білд-скрипті build.gradle. Отже, у нас з'явився додаток. Нам треба подумати, що ми хочемо описувати чи моделювати нашим додатком. Давайте скористаємося якимось засобом моделювання, наприклад: app.quickdatabasediagrams.com JPA : Знайомство з технологією - 4Тут варто сказати, що те, що ми описали нашу "доменну модель". Домен - це деяка "предметна область". Взагалі, домен - це "володіння" латиною. У середні віки так називалися області, якими володіли королі чи феодали. А у французькій мові це стало словом "domaine", яке перекладається просто як "область". Таким чином ми описали нашу "доменну модель" = "предметну модель". Кожен елемент цієї моделі - це деяка "сутність", щось із реального життя. У нашому випадку це є сутністю: Категорія ( Category), Тема ( Topic). Створимо для сутностей окремий пакет, наприклад, з ім'ям model. І додамо туди Java класи, що описують сутності. У Java коді такі сутності являють собою звичайний POJO ,
public class Category {
    private Long id;
    private String title;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
Скопіюємо вміст класу та зробимо за аналогією клас Topic. Відрізнятися він буде лише тим, що знає про категорію, до якої належить. Тому, додамо в клас Topicполе категорії та методи роботи з нею:
private Category category;

public Category getCategory() {
	return category;
}

public void setCategory(Category category) {
	this.category = category;
}
Тепер у нас є Java додаток, який має свою доменну модель. Настав час тепер приступати до підключення до проекту JPA.
JPA : Знайомство з технологією - 5

Додавання JPA

Отже, як ми пам'ятаємо, JPA — це про те, що ми зберігатимемо щось у БД. Отже, нам потрібна база даних. Щоб використовувати підключення до БД у своєму проекті, нам потрібно додати в залежності бібліотеку, для підключення до БД. Як ми пам'ятаємо, ми використали Gradle, який створив нам білд скрипт build.gradle. У ньому ми й опишемо залежності, які потрібні нашому проекту. Залежно — це бібліотеки, без яких не може працювати наш код. Почнемо з опису залежності від підключення до БД. Робимо це так само, як би робабо, працюючи просто з JDBC:

dependencies {
	implementation 'com.h2database:h2:1.4.199'
Тепер ми маємо БД. Ми можемо тепер додати до нашої програми рівень або шар (layer), що відповідає за відображення наших Java об'єктів у поняття бази даних (з Java мови на мову SQL). Як ми пам'ятаємо, ми збираємося використати для цього реалізацію специфікації JPA під назвою Hibernate:

dependencies {
	implementation 'com.h2database:h2:1.4.199'
	implementation 'org.hibernate:hibernate-core:5.4.2.Final'
Тепер нам потрібно налаштувати JPA. Якщо ми прочитаємо специфікацію та розділ "8.1 Persistence Unit", то ми дізнаємося, що Persistence Unit - це деяке об'єднання конфігурацій, метаданих і сутностей. І щоб JPA запрацював, потрібно описати хоча б один Persistence Unit у файлі конфігурації, який має назву persistence.xml. Його розташування описано у розділі специфікації "8.2 Persistence Unit Packaging". Відповідно до цього розділу, якщо у нас Java SE оточення, ми повинні покласти його в корінь каталогу META-INF.
JPA : Знайомство з технологією - 6
Зміст скопіюємо з прикладу, наведеного у специфікації JPA у розділі " 8.2.1 persistence.xml file":
<persistence>
	<persistence-unit name="JavaRush">
        <description>Persistence Unit For test</description>
        <class>hibernate.model.Category</class>
        <class>hibernate.model.Topic</class>
    </persistence-unit>
</persistence>
Але цього замало. Потрібно розповісти, хто наш JPA Provider, тобто. той, хто реалізує специфікацію JPA:
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
А тепер додамо налаштування ( properties). Частина (починаються на javax.persistence) є стандартними JPA конфігураціями і описані у специфікації JPA у розділі "8.2.1.9 properties". Частина конфігурацій є провайдер-специфічними (у нашому випадку, впливають на Hibernate як на Jpa Provider'а. Наш блок налаштувань виглядатиме так:
<properties>
    <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
    <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />
    <property name="javax.persistence.jdbc.user" value="sa" />
    <property name="javax.persistence.jdbc.password" value="" />
    <property name="hibernate.show_sql" value="true" />
    <property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
Тепер у нас є JPA-сумісний конфіг persistence.xml, є JPA провайдер Hibernate і є база даних H2, а також є 2 класи, які є нашою доменною моделлю. Давайте змусимо це все відпрацювати. У каталозі /test/javaнаш Gradle люб'язно нам згенерував шаблон для Unit тестів та назвав його AppTest. Давайте використовуємо його. Як говорить глава "7.1 Persistence Contexts" специфікації JPA, сутності у світі JPA живуть у деякому просторі, яке називається "Контекст персистенції" (або Контексті сталості, Persistence Context). Але безпосередньо ми не працюємо з Persistence Context. Для цього ми використовуємо Entity Managerабо менеджер сутностей. Саме він знає про контекст та про те, які там живуть сутності. Ми ж взаємодіємо з Entity ManagerТом. Тоді залишається тільки зрозуміти,Entity Manager? Відповідно до розділу "7.2.2 Obtaining an Application-managed Entity Manager" специфікації JPA ми повинні використовувати EntityManagerFactory. Тому, озброїмося специфікацією JPA і візьмемо приклад з розділу "7.3.2.
@Test
public void shouldStartHibernate() {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
	EntityManager entityManager = emf.createEntityManager();
}
Цей тест покаже помилку "Unrecognized JPA persistence.xml XSD version". Причина — persistence.xmlпотрібно правильно вказати використовувану схему, як це сказано в специфікації JPA в розділі "8.3 persistence.xml Schema":
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
             http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">
Крім того, важливим є порядок елементів. Тому providerповинен бути вказаний до перерахування класів. Після цього тест виконається успішно. Безпосереднє підключення JPA ми здійснабо. Перш ніж ми рухатимемося далі, подумаємо про інші тести. Кожен наш тест вимагатиме EntityManager. Давайте зробимо так, щоб у кожного тесту був свій EntityManagerпочаток виконання. Крім того, ми хочемо, щоб БД щоразу була нова. Завдяки тому, що ми використовуємо inmemoryваріант, достатньо закривати EntityManagerFactory. Створення Factory– дорога операція. Але для тестів це виправдано. JUnit дозволяє задати методи, які будуть виконуватися перед (Before) та після (After) виконанням кожного тесту:
public class AppTest {
    private EntityManager em;

    @Before
    public void init() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
        em = emf.createEntityManager();
    }

    @After
    public void close() {
        em.getEntityManagerFactory().close();
        em.close();
    }
Тепер, перед виконанням будь-якого тесту буде створено нову EntityManagerFactory, що спричинить створення нової БД, т.к. hibernate.hbm2ddl.autoмає значення create. А з нової фабрики отримаємо новий EntityManager.
JPA : Знайомство з технологією - 7

Сутності (Entities)

Ми пам'ятаємо, ми створабо раніше класи, що описують нашу доменну модель. Ми вже говорабо, що це наші "сутності". Це і є Entity, якими ми керуватимемо за допомогою EntityManager. Напишемо простий тест щодо збереження сутності категорії:
@Test
public void shouldPersistCategory() {
	Category cat = new Category();
	cat.setTitle("new category");
	// JUnit обеспечит тест свежим EntityManager'ом
	em.persist(cat);
}
Але це тест не запрацює, т.к. ми отримаємо різні помилки, які нам допоможуть зрозуміти, що таке сутності:
  • Unknown entity: hibernate.model.Category
    Чому ж Hibernate не розуміє, що Categoryце entity? Справа в тому, що сутності повинні бути описані за стандартом JPA.
    Класи сутностей повинні бути анотовані анотацією @Entity, як сказано у розділі "2.1 The Entity Class" специфікації JPA.

  • No identifier specified for entity: hibernate.model.Category
    Для сутностей повинен бути вказаний унікальний ідентифікатор, яким можна відрізнити один запис від іншого.
    Відповідно до глави "2.4 Primary Keys and Entity Identity" специфікації JPA "Every entity must have a primary key", тобто. кожна сутність повинна мати "первинний ключ". Такий первинний ключ має бути вказаний анотацією@Id

  • ids for this class must be manually assigned before calling save()
    Ідентифікатор повинен з'явитися звідкись. Його можна вказати вручну, а можна отримати автоматично.
    Тому, як і зазначено в розділах "11.2.3.3 GeneratedValue" та "11.1.20 GeneratedValue Annotation", ми можемо вказати інструкцію @GeneratedValue.

Таким чином, щоб клас категорії став сутністю, ми повинні виконати наступні зміни:
@Entity
public class Category {
    @Id
    @GeneratedValue
    private Long id;
Крім того, інструкція @Idвказує на те, який використовувати Access Type. Докладніше про тип доступу можна прочитати у специфікації JPA, у розділі "2.3 Access Type". Якщо дуже стисло, то т.к. ми вказали @Idнад полем ( field), тип доступу буде за замовчуванням field-based, а не property-based. Отже, провайдер JPA читатиме і зберігатиме значення безпосередньо з полів. Якби помістабо @Idнад геттером, то використовувався б property-basedдоступ, тобто. через геттер та сетер. При виконанні тесту ми бачимо навіть те, які запити надсилаються до бази (завдяки опції hibernate.show_sql). Але при збереженні ми не бачимо жодних insert'ів. Виходить, що ми насправді нічого не зберегли? JPA дозволяє синхронізувати контекст персистенції та БД за допомогою методу flush:
entityManager.flush();
Але якщо ми його виконаємо, то отримаємо помилку: no transaction is in progress . І тут настає час дізнатися про те, як JPA використовує транзакції.
JPA : Знайомство з технологією - 8

JPA Transactions

Як ми пам'ятаємо, в основі JPA лежить поняття контексту персистенції (Persistence Context). Це місце, де мешкають сутності. А ми керуємо сутностями через EntityManager. Коли ми виконуємо команду persist, то поміщаємо сутність у контекст. Точніше, ми говоримо EntityManager, що це потрібно зробити. Але контекст цей — це просто певна сфера зберігання. Його навіть іноді називають "кешем першого рівня". Але його потрібно з'єднати із базою даних. Команда flush, яка раніше у нас впала з помилкою, синхронізує дані з контексту персистенції з БД. Але для цього потрібний транспорт і цим транспортом є транзакція. Транзакції JPA описані в розділі специфікації "7.5 Controlling Transactions". Для використання транзакцій у JPA є спеціальний API:
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
Необхідно додати керування транзакціями до нашого коду, який виконується до тестів і після:
@Before
public void init() {
	EntityManagerFactory emf = Persistence.createEntityManagerFactory( "JavaRush" );
	em = emf.createEntityManager();
	em.getTransaction().begin();
}
@After
public void close() {
	if (em.getTransaction().isActive()) {
		em.getTransaction().commit();
        }
	em.getEntityManagerFactory().close();
	em.close();
}
Після додавання ми побачимо в лозі insert вираз на мові SQL, яких раніше не було:
JPA : Знайомство з технологією - 9
Зміни, накопичені EntityManagerза допомогою транзакції були закоммічені (підтверджені та збережені) в БД. Спробуймо тепер знайти нашу сутність. Створимо тест на пошук сутності за її ID:
@Test
public void shouldFindCategory() {
	Category cat = new Category();
	cat.setTitle("test");
	em.persist(cat);
	Category result = em.find(Category.class, 1L);
	assertNotNull(result);
}
У цьому випадку ми отримаємо раніше збережену сутність, але в лозі ми не побачимо SELECT запитів. А все по тому, що ми говоримо: "Менеджер сутностей, будь ласка, знайди мені сутність Категорія з ID=1". А менеджер сутностей спочатку дивиться у себе в контексті (використовує його свого роду кеш), і тільки якщо не знаходить, йде шукати в БД. Варто змінити ID на 2 (такого ні, ми зберегли лише 1 екземпляр), як побачимо, що SELECTзапит з'являється. Тому що в контексті не знайдено сутності і EntityManagerнамагається знайти сутність БД. Існують різні команди, якими ми можемо керувати станом сутності в контексті. Перехід сутності з одного стану до іншого називається життєвим циклом сутності — lifecycle.
JPA : Знайомство з технологією - 10

Entity Lifecycle

Життєвий цикл сутностей описаний у специфікації JPA у розділі "3.2 Entity Instance's Life Cycle". Т.к. сутності живуть у тих і ними управляє EntityManager, то кажуть, що сутності керовані, тобто. managed. Давайте подивимося на етапи життя сутності:
// 1. New або Transient (временный)
Category cat = new Category();
cat.setTitle("new category");
// 2. Managed або Persistent
entityManager.persist(cat);
// 3. Транзакция завершена, все сущности в контексте detached
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
// 4. Сущность изымаем из контекста, она становится detached
entityManager.detach(cat);
// 5. Сущность из detached можно снова сделать managed
Category managed = entityManager.merge(cat);
// 6. И можно сделать Removed. Интересно, что cat всё равно detached
entityManager.remove(managed);
І ось для закріплення схема:
JPA : Знайомство з технологією - 11
JPA : Знайомство з технологією - 12

Mapping

У JPA ми можемо описати відносини сутності між собою. Згадаймо, що ми вже розбирали стосунки сутностей між один одним, коли ми розбиралися з нашою доменною моделлю. Тоді ми використовували ресурс quickdatabasediagrams.com :
JPA : Знайомство з технологією - 13
Встановлення зв'язків між сутностями називається мапінг або асоціюванням (Association Mappings). Види асоціацій, які можуть бути встановлені за допомогою JPA, представлені нижче:
JPA : Знайомство з технологією - 14
Давайте подивимося на суть Topic, яка описує тему. Що ми можемо сказати про ставлення Topicдо Category? Багато Topicналежатимуть до однієї категорії. Отже, нам потрібна асоціація ManyToOne. Виразимо цей зв'язок мовою JPA:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
Щоб запам'ятати, які анотації ставити можна запам'ятати, що остання частина відповідає за поле, над яким вказана анотація. ToOne- конкретний екземпляр. ToMany- Колекції. Зараз у нас зв'язок односторонній. Давайте зробимо з неї двосторонній зв'язок. Додамо у Categoryзнання про всі Topic, які входять до цієї категорії. Закінчуватися повинен на ToManyтому, що у нас список Topic. Тобто ставлення "До багатьох" тем. Залишається питання - OneToManyабо ManyToMany:
JPA : Знайомство з технологією - 15
На цю ж тему хорошу відповідь можна прочитати тут: " Explain ORM oneToMany, manyToMany relation like I'm five ". Якщо категорія має зв'язок з ToManyтопіків, то кожен із цих топіків може мати тільки одну категорію, то буде One, а інакше Many. Таким чином, у Categoryсписок усіх тем буде виглядати так:
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
І не забудемо по суті Categoryописати геттер для отримання списку всіх тем:
public Set<Topic> getTopics() {
	return this.topics;
}
Двонаправлені стосунки — дуже складний для автоматичного відстеження момент. Тому JPA перекладає цей обов'язок на розробника. Для нас це означає, що коли ми встановлюємо по суті Topicзв'язок з Category, ми повинні забезпечити несуперечність даних самостійно. Робиться це просто:
public void setCategory(Category category) {
	category.getTopics().add(this);
	this.category = category;
}
Напишемо для перевірки простий тест:
@Test
public void shouldPersistCategoryAndTopics() {
	Category cat = new Category();
	cat.setTitle("test");
	Topic topic = new Topic();
	topic.setTitle("topic");
	topic.setCategory(cat);
 	em.persist(cat);
}
Мапінг це ціла окрема тема. У рамках даного огляду слід зрозуміти, за допомогою яких засобів це досягається. Докладніше про мапінг можна прочитати тут:
JPA : Знайомство з технологією - 16

JPQL

JPA вводить цікавий інструмент – запити мовою Java Persistence Query Language. Ця мова схожа на SQL, але використовує об'єктну модель Java, а не SQL таблиці. Розглянемо приклад:
@Test
public void shouldPerformQuery() {
	Category cat = new Category();
	cat.setTitle("query");
	em.persist(cat);
	Query query = em.createQuery("SELECT c from Category c WHERE c.title = 'query'");
 	assertNotNull(query.getSingleResult());
}
Як бачимо, у запиті ми використовували вказівку на сутність Category, а чи не таблицю. А також на полі цієї сутності title. JPQL надає безліч корисних можливостей та претендує на окрему статтю. Детальніше можна ознайомитись у огляді:
JPA : Знайомство з технологією - 17

Criteria API

І насамкінець хотілося б торкнутися Criteria API. JPA вводить інструмент динамічної побудови запитів. Приклад використання Criteria API:
@Test
public void shouldFindWithCriteriaAPI() {
	Category cat = new Category();
	em.persist(cat);
	CriteriaBuilder cb = em.getCriteriaBuilder();
	CriteriaQuery<Category> query = cb.createQuery(Category.class);
	Root<Category> c = query.from(Category.class);
	query.select(c);
	List<Category> resultList = em.createQuery(query).getResultList();
	assertEquals(1, resultList.size());
}
Цей приклад дорівнює виконанню запиту " SELECT c FROM Category c". Criteria API – потужний інструмент. Докладніше про нього можна прочитати тут:

Висновок

Як ми бачимо, JPA надає величезну кількість можливостей та інструментів. Кожен із них потребує досвіду та знань. Навіть у рамках огляду JPA вийшло згадати не все, не кажучи вже про детальне занурення. Але сподіваюся, що після прочитання стало зрозуміліше, що взагалі таке ORM і JPA, як це працює і що з цим можна зробити. Ну і на закуску пропоную різні матеріали: #Viacheslav
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ