Служебная таблица

Теперь разберем еще один часто встречающийся случай – 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-объектами и при обращении к ним всегда выполняется запрос к базе данных.

Так что если добавить задачу к сотруднику и сохранить информацию о сотруднике в базу, то после этого у задачи в списке исполнителей появится новый исполнитель.

undefined
1
Задача
Модуль 4. Работа с БД, 13 уровень, 3 лекция
Недоступна
Создаём таблицу связи ManyToMany
Представим, что у нас есть две таблицы: - таблица author с колонками id, first_name, last_name, full_name; - таблица book с колонками id, title, publication_year, isbn.
undefined
1
Задача
Модуль 4. Работа с БД, 13 уровень, 3 лекция
Недоступна
Соавторство
Есть два класса-энтити: Author и Book. Таблицы, которые им соответствуют: - таблица author с колонками id, first_name, last_name, full_name; - таблица book с колонками id, title, publication_year, isbn.