Учебное пособие по аннотациям Hibernate «многие ко многим»
В этом кратком руководстве мы кратко рассмотрим, как аннотацию @ManyToMany можно использовать для указания этого типа отношений в Hibernate.
2. Типичный пример
Давайте начнем с простой диаграммы отношений сущностей, которая показывает связь «многие ко многим» между двумя сущностями , сотрудником и проектом:
В этом сценарии любой конкретный сотрудник может быть назначен на несколько проектов, и над проектом может работать несколько сотрудников, что приводит к ассоциации «многие ко многим» между ними.
У нас есть таблица сотрудников с employee_id в качестве первичного ключа и таблица проекта с project_id в качестве первичного ключа. Здесь требуется таблица соединений employee_project для соединения обеих сторон.
3. Настройка базы данных
Предположим, у нас есть уже созданная база данных с именем spring_hibernate_many_to_many.
Нам также необходимо создать таблицы employee и project вместе с таблицей соединения employee_project с employee_id и project_id в качестве внешних ключей:
CREATE TABLE `employee` ( `employee_id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(50) DEFAULT NULL, `last_name` varchar(50) DEFAULT NULL, PRIMARY KEY (`employee_id`) ) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8; CREATE TABLE `project` ( `project_id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(50) DEFAULT NULL, PRIMARY KEY (`project_id`) ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8; CREATE TABLE `employee_project` ( `employee_id` int(11) NOT NULL, `project_id` int(11) NOT NULL, PRIMARY KEY (`employee_id`,`project_id`), KEY `project_id` (`project_id`), CONSTRAINT `employee_project_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `employee` (`employee_id`), CONSTRAINT `employee_project_ibfk_2` FOREIGN KEY (`project_id`) REFERENCES `project` (`project_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
После настройки базы данных следующим шагом будет подготовка зависимостей Maven и конфигурации Hibernate. Для получения информации об этом, пожалуйста, обратитесь к статье Руководство по Hibernate4 с Spring
4. Классы моделей
Классы модели Employee и Project должны быть созданы с аннотациями JPA:
@Entity @Table(name = "Employee") public class Employee // . @ManyToMany(cascade = CascadeType.ALL >) @JoinTable( name = "Employee_Project", joinColumns = @JoinColumn(name = "employee_id") >, inverseJoinColumns = @JoinColumn(name = "project_id") > ) SetProject> projects = new HashSet>(); // standard constructor/getters/setters >
@Entity @Table(name = "Project") public class Project // . @ManyToMany(mappedBy = "projects") private SetEmployee> employees = new HashSet>(); // standard constructors/getters/setters >
Как мы видим, и класс Employee , и классы Project ссылаются друг на друга, а это означает, что связь между ними является двунаправленной.
Чтобы отобразить ассоциацию «многие ко многим», мы используем аннотации @ManyToMany , @JoinTable и @JoinColumn . Давайте посмотрим на них поближе.
Аннотация @ManyToMany используется в обоих классах для создания отношения «многие ко многим» между сущностями.
Эта ассоциация имеет две стороны, т.е. владеющую сторону и обратную сторону. В нашем примере стороной-владельцем является Employee , поэтому таблица соединения указывается на стороне-владельце с помощью аннотации @JoinTable в классе Employee . @JoinTable используется для определения таблицы соединений/связей . В данном случае это Employee_Project.
Аннотация @JoinColumn используется для указания столбца соединения/связывания с основной таблицей. Здесь столбец соединения — employee_id , а project_id — обратный столбец соединения, поскольку Project находится на обратной стороне отношения.
В классе Project атрибут mappedBy используется в аннотации @ManyToMany , чтобы указать, что коллекция сотрудников сопоставляется с коллекцией проектов на стороне владельца.
5. Исполнение
Чтобы увидеть аннотацию «многие ко многим» в действии, мы можем написать следующий тест JUnit:
public class HibernateManyToManyAnnotationMainIntegrationTest private static SessionFactory sessionFactory; private Session session; //. @Test public void givenSession_whenRead_thenReturnsMtoMdata() prepareData(); @SuppressWarnings("unchecked") ListEmployee> employeeList = session.createQuery("FROM Employee").list(); @SuppressWarnings("unchecked") ListProject> projectList = session.createQuery("FROM Project").list(); assertNotNull(employeeList); assertNotNull(projectList); assertEquals(2, employeeList.size()); assertEquals(2, projectList.size()); for(Employee employee : employeeList) assertNotNull(employee.getProjects()); assertEquals(2, employee.getProjects().size()); > for(Project project : projectList) assertNotNull(project.getEmployees()); assertEquals(2, project.getEmployees().size()); > > private void prepareData() String[] employeeData = "Peter Oven", "Allan Norman" >; String[] projectData = "IT Project", "Networking Project" >; SetProject> projects = new HashSetProject>(); for (String proj : projectData) projects.add(new Project(proj)); > for (String emp : employeeData) Employee employee = new Employee(emp.split(" ")[0], emp.split(" ")[1]); employee.setProjects(projects); for (Project proj : projects) proj.getEmployees().add(employee); > session.persist(employee); > > //. >
Мы можем видеть отношение «многие ко многим» между двумя сущностями, созданными в базе данных: таблицами employee , project и employee_project с образцами данных, представляющими отношения.
6. Заключение
В этом руководстве мы увидели, как создавать сопоставления с использованием аннотаций Hibernate «многие ко многим», что является более удобным аналогом по сравнению с созданием файлов сопоставления XML.
Исходный код этого руководства можно найти на GitHub .
@ManyToMany
Теперь разберем еще один часто встречающийся случай – many-to-many. Давай представим, что у нас отношение между задачами и сотрудниками многие-ко-многим:
- Один сотрудник в таблице employee может делать много задач из таблицы task.
- Одна задача в таблице task может быть назначена на несколько сотрудников.
Такая связь между сущностями называется многие-ко-многим. И чтобы ее реализовать на уровне SQL, нам понадобится дополнительная служебная таблица. Назовем ее, например, employee_task.
Таблица employee_task будет содержать всего две колонки:
Каждый раз, когда мы будем назначать определенную задачу определенному пользователю, в эту таблицу будет добавляться новая строка. Пример:
Ну, а таблица 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 ( не изменилась ):
В этой таблице есть такие колонки:
А вот так выглядит таблица 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:
Я специально временно сохранил удаленную колонку в таблице 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) |
Связь на уровне Java-классов
Зато со связью на уровне Entity-классов у нас полный порядок. Начнем с хороших новостей.
Во-первых, у Hibernate есть специальная аннотация @ManyToMany , которая позволяет хорошо описать случай отношения таблиц many-to-many.
Во-вторых, нам по-прежнему достаточно двух Entity-классов. Класс для служебной таблицы нам не нужен.
Вот как будут выглядеть наши классы. Класс Employee в изначальном виде:
@Entity @Table(name="user") class Employee
И класс EmployeeTask в его изначальном виде:
@Entity @Table(name="task") class EmployeeTask
Аннотация @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 Settasks = new HashSet(); >
@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 Setemployees = new HashSet(); >
Кажется, что все сложно, но на самом деле там все просто.
Во-первых, там используется аннотация @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-объектами и при обращении к ним всегда выполняется запрос к базе данных.
Так что если добавить задачу к сотруднику и сохранить информацию о сотруднике в базу, то после этого у задачи в списке исполнителей появится новый исполнитель.