1. Зв'язок на рівні таблиць

Ми познайомилися з тим, як Hibernate зберігає колекції у допоміжних таблицях. Тепер розберемося, як організувати зв'язки між повноцінними таблицями, які зберігають справжні Entity-класи.

Усього між Entity-класами в Hibernate є чотири види відносин:

  • one-to-one
  • one-to-many
  • many-to-one
  • many-to-many

І почнемо ми розбір із найпростішого варіанту — many-to-one.

З таким відношенням між таблицями в SQL ти вже стикався. Ось як воно зазвичай виглядає:

id name occupation salary age join_date
1 Шевченко Ігор Програміст 100000 25 2012-06-30
2 Коваленко Максим Програміст 80000 23 2013-08-12
3 Шевченко Данило Тестувальник 40000 30 2014-01-01
4 Мельник Степан Директор 200000 35 2015-05-12
5 Кірієнко Анастасія Офіс-менеджер 40000 25 2015-10-10
6 Пончик Кіт 1000 3 2018-11-11

Таблиця employee:

У цій таблиці є такі колонки:

  • id INT
  • nameVARCHAR
  • occupationVARCHAR
  • salaryINT
  • ageINT
  • join_dateDATE

А так виглядає таблиця task, яка містить завдання для співробітників:

id employee_id name deadline
1 1 Виправити багу на фронтенді 2022-06-01
2 2 Виправити багу на бекенді 2022-06-15
3 5 Купити каву 2022-07-01
4 5 Купити каву 2022-08-01
5 5 Купити каву 2022-09-01
6 (NULL) Прибрати офіс (NULL)
7 4 Насолоджуватися життям (NULL)
8 6 Насолоджуватися життям (NULL)

У цій таблиці є всього 4 колонки:

  • id — унікальний номер завдання (та рядки в таблиці);
  • employee_id — ID співробітника з таблиці employee, на якого призначено завдання;
  • name — назва та опис завдання;
  • deadline — час, до якого потрібно виконати завдання.

Ми бачимо, що багато рядків таблиці task можуть посилатися на один запис employee таблиці. Такий зв'язок на рівні таблиць називається багато до одного (many-to-one).

2. Зв'язок рівня Java-класів

Крім зв'язку лише на рівні таблиць, можна ще організувати зв'язок лише на рівні Entity-класів в Hibernate. Це робиться за допомогою анотації @ManyToOne.

Але для початку просто створимо два класи: Employee та EmployeeTask:


@Entity
@Table(name="employee")
class Employee {
   @Column(name="id")
   public Integer id;
 
   @Column(name="name")
   public String name;
 
   @Column(name="occupation")
   public String occupation;
 
   @Column(name="salary")
   public Integer salary;
 
   @Column(name="join_date")
   public Date join;
}

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


@Entity
@Table(name="task")
class EmployeeTask {
   @Column(name="id")
   public Integer id;
 
   @Column(name="name")
   public String description;
 
  @Column(name="employee_id")
   public Integer employeeId;
 
   @Column(name="deadline")
   public Date deadline;
}

З цими класами все добре, але між ними немає жодного зв'язку, який би відображав той факт, що поле employeeId класу EmployeeTask посилається на поле id класу Employee. Настав час це виправити

3. Анотація @ManyToOne.

По-перше, в Java ми звикли оперувати об'єктами (і посиланнями на об'єкти), а не їхніми id. Так що насамперед давайте замість поля employeeId у класі EmployeeTask просто вкажемо посилання на об'єкт типу Employee. Ось як виглядатиме наш новий клас:


@Entity
@Table(name="task")
class EmployeeTask {
   @Column(name="id")
   public Integer id;
 
   @Column(name="name")
   public String description;
 
   @ManyToOne
   @JoinColumn(name = "employee_id")
   public Employee employee;
 
   @Column(name="deadline")
   public Date deadline;
}

За допомогою анотації @ManyToOne ми вказали, що багато об'єктів EmployeeTask можуть посилатися на один об'єкт типу Employee. Також за допомогою анотації @JoinColumn ми вказали, в якій колонці нашої таблиці зберігається id об'єкта Employee.

4. Приклади запитів

А тепер розглянемо кілька прикладів того, як Hibernate вміє працювати з такими зв'язаними класами.

Сценарій перший

Давай напишемо запит, щоб дізнатися про всі завдання, які були призначені на певного користувача. Ось як цей запит буде виглядати на HQL:

from EmployeeTask where employee.name = "Шевченко Ігор"

Ти можеш просто звертатися до полів залежних класів через точку. Це дуже зручно. Але давай все ж запишемо цей запит у вигляді Java-коду:


String hql = "from EmployeeTask where employee.name = :username";
Query<EmployeeTask> query = session.createQuery( hql, EmployeeTask . class);
query.setParameter("username", "Шевченко Ігор" );
List<EmployeeTask> resultLIst = query.list();

Сценарій другий

Давай напишемо запит, який повертає список співробітників, які мають прострочені завдання. Завдання прострочено, якщо його deadline вже у минулому. Ось як виглядатиме цей запит на SQL:


SELECT DISTINCT employee.*
FROM task JOIN employee ON task.employee_id = employee.id
WHERE task.deadline < CURDATE();

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

А тепер запишемо цей же запит на HQL:

select distinct employee from EmployeeTask where  deadline < CURDATE();

Employee у цьому запиті — це поле класу EmployeeTask

Ситуація третя

Призначимо всі непризначені завдання на директора. Запит на SQL виглядатиме так:


UPDATE task SET employee_id = 4 WHERE employee_id IS NULL

А тепер запишемо цей же запит на HQL:

update EmployeeTask set employee = :user where employee is null

Останній запит найскладніший. Нам потрібно передати ID директора, але клас EmployeeTask не містить поля, куди можна записати id, натомість він містить поле Employee, куди потрібно присвоїти посилання на об'єкт типу Employee.


Employee director = session.get(Employee.class, 4);
 
String hql = "update EmployeeTask set employee = :user where employee is null";
Query<EmployeeTask> query = session.createQuery(hql, EmployeeTask.class);
query.setParameter("user", director);
query.executeUpdate();