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-об'єктами, і під час звернення до них завжди виконується запит до бази даних.
Отже, якщо додати завдання до співробітника та зберегти інформацію про співробітника до бази, після цього у завдання в списку виконавців з'явиться новий виконавець.