Служебная таблица
Теперь разберем еще один часто встречающийся случай – 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.
Связь на уровне таблиц
Вот как будут выглядеть наши новые таблицы:
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 | emploee_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) |
Tаблица employee_task:
employee_id | task_id |
---|---|
1 | 1 |
2 | 2 |
5 | 3 |
5 | 4 |
5 | 5 |
4 | 7 |
6 | 8 |
Связь на уровне 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;
}
Аннотация @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 должен их интерпретировать.
Зато мы теперь очень просто можем добавить (и удалить) задание любому сотруднику. А также добавить любого исполнителя любому заданию.
Примеры запросов
Давай напишем пару интересных запросов, чтобы лучше понять, как работают эти ManyToMany поля. А работают они абсолютно так, как и ожидается.
Во-первых, наш старый код будет работать без изменений, так как у директора и раньше было поле tasks:
EmployeeTask task1 = new EmployeeTask();
task1.description = "Сделать что-то важное";
session.persist(task1);
EmployeeTask task2 = new 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-объектами и при обращении к ним всегда выполняется запрос к базе данных.
Так что если добавить задачу к сотруднику и сохранить информацию о сотруднике в базу, то после этого у задачи в списке исполнителей появится новый исполнитель.