1. Службова таблиця

Тепер розберемо ще один частий випадок — many-to-many. Давай уявимо, що в нас відношення між завданнями і співробітниками багато-до-багатьох:

  • Один співробітник у таблиці employee може виконувати багато завдань із таблиці task.
  • Одне завдання в таблиці task може бути призначена на кілька співробітників.

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

Таблиця employee_task міститиме всього дві колонки:

  • employee_id
  • task_id

Кожен раз, коли ми будемо призначати певне завдання певному користувачеві, до цієї таблиці буде додаватися новий рядок. Приклад:

employee_id task_id
1 1
1 2
2 3

Ну а таблиця task повинна втратити колонку employee_id. У ній є сенс лише якщо завдання можна призначити тільки на одного співробітника. Якщо ж завдання можна призначити на кількох співробітників, цю інформацію потрібно зберігати у службовій таблиці employee_task.

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

Ось як виглядатимуть наші нові таблиці:

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
  • name VARCHAR
  • occupation VARCHAR
  • salary INT
  • age INT
  • join_date DATE

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

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)

У цій таблиці тепер є лише 3 колонки:

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

Також у нас є службова таблиця employee_task, куди перекочували дані про employee_id з таблиці task:

employee_id task_id
1 1
2 2
5 3
5 4
5 5
(NULL) 6
4 7
6 8

Я спеціально тимчасово зберіг віддалену колонку у таблиці task, щоб ти міг побачити, що дані з неї переїхали до таблиці employee_task.

Ще один важливий момент — червоний рядок "(NULL) 6" у таблиці employee_task. Я відзначив його червоним, оскільки його не буде в таблиці employee_task.

Якщо таск 7 призначений на користувача 4, то в таблиці employee_task має бути рядок (4, 7).

Якщо таск 6 ні на кого не призначений, то просто в таблиці employee_task для нього не буде жодного запису. Ось як виглядатимуть фінальні версії цих таблиць:

Таблиця task:

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

Таблиця employee_task:

employee_id task_id
1 1
2 2
5 3
5 4
5 5
4 7
6 8

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

Зате зі зв'язком на рівні Entity-класів ми маємо повний порядок. Почнемо з добрих новин.

По-перше, Hibernate має спеціальну анотацію @ManyToMany, яка дозволяє добре описати випадок відношення таблиць many-to-many.

По-друге, нам, як і раніше, достатньо двох Entity-класів. Клас для службової таблиці нам не потрібний.

Ось як виглядатимуть наші класи. Клас Employee у первісному вигляді:


@Entity
@Table(name="user")
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;
}

І клас EmployeeTask у його первісному вигляді:


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

4. Аннотація @ManyToMany

Я пропущу в прикладах існуючі поля, але натомість додам нові. Ось як вони виглядатимуть. Клас Employee:


@Entity
@Table(name="employee")
class Employee {
   @Column(name="id")
   public Integer id;
 
   @ManyToMany(cascade = CascadeType.ALL)
   @JoinTable(name="employee_task",
joinColumns= @JoinColumn(name="employee_id", referencedColumnName="id"),
           inverseJoinColumns= @JoinColumn(name="task_id", referencedColumnName="id") )
   private Set<EmployeeTask> tasks = new HashSet<EmployeeTask>();
 
}

І клас EmployeeTask:


@Entity
@Table(name="task")
class EmployeeTask {
   @Column(name="id")
   public Integer id;
 
   @ManyToMany(cascade = CascadeType.ALL)
   @JoinTable(name="employee_task",
       joinColumns= @JoinColumn(name="task_id", referencedColumnName="id"),
       inverseJoinColumns= @JoinColumn(name="employee_id", referencedColumnName="id") )
   private Set<Employee> employees = new HashSet<Employee>();
 
}

Здається, що все складно, але насправді там все просто.

По-перше, там використовується анотація @JoinTable (не плутати з @JoinColumn), яка описує службову таблицю employee_task.

По-друге, там описується, що колонка task_id таблиці employee_task посилається на колонку ID таблиці task.

По-третє, там говориться, що колонка employee_id таблиці employee_task посилається на колонку id таблиці employee.

Фактично ми за допомогою анотацій описали, які дані містяться в таблиці employee_task і як Hibernate повинен їх інтерпретувати.

Зате ми тепер дуже просто можемо додати (і видалити) завдання для будь-якого співробітника. А також додати будь-якого виконавця до будь-якого завдання.

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

Давай напишемо кілька цікавих запитів, щоб краще зрозуміти, як працюють ці ManyToMany поля. А працюють вони абсолютно так, як очікується.

По-перше, наш старий код буде працювати без змін, оскільки директор і раніше мав поле tasks:


EmployeeTask task1 = новий EmployeeTask();
task1.description = "Зробити щось важливе";
session.persist(task1);
 
EmployeeTask task2 = новий EmployeeTask();
task2.description = "Нічого не робити";
session.persist(task2);
session.flush();
 
Employee director = session.find(Employee.class, 4);
director.tasks.add(task1);
director.tasks.add(task2);
 
session.update(director);
session.flush();

По-друге, якщо ми захочемо призначити якомусь завданню ще одного виконавця, то зробити це ще простіше:


Employee director = session.find(Employee.class, 4);
EmployeeTask task = session.find(EmployeeTask.class, 101);
task.employees.add(director);
 
session.update(task);
session.flush();

Важливо! В результаті виконання цього запиту не тільки у завдання з'явиться виконавець-директор, а ще й у директора з'явиться завдання № 101.

По-перше, факт зв'язку директора та завдання у таблиці employee_task буде збережено у вигляді рядка: (4,101).

По-друге, поля, позначені анотаціями @ManyToMany, є proxy-об'єктами, і під час звернення до них завжди виконується запит до бази даних.

Отже, якщо додати завдання до співробітника та зберегти інформацію про співробітника до бази, після цього у завдання в списку виконавців з'явиться новий виконавець.