Java связь между таблицами

@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-объектами и при обращении к ним всегда выполняется запрос к базе данных.

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

Источник

Односторонние и двусторонние отношения в Hibernate

Всем нам хорошо известен ответ на вопрос, какими могут быть отношения между сущностями в Hibernate и JPA. Вариантов всего четыре:

  • OneToOne — один к одному
  • OneToMany — один ко многим
  • ManyToOne — многие к одному
  • ManyToMany — многие ко многим

Для каждого из отношений есть своя аннотация и, казалось бы, на этом можно закончить разговор, но все не так просто. Да и вообще, может ли быть что-то просто в Hibernate 😉 Каждое из выше перечисленных отношений может быть односторонним (unidirectional) или двусторонним (bidirectional), и если не принимать это во внимание, то можно столкнуться с массой проблем и странностей.

Для примера возьмем две простейшие сущности: пользователь и контакт. Очевидно, что каждый контакт связан с пользователем отношением многие к одному, а пользователь с контактами отношением один ко многим.

Односторонние отношения

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

Давайте попробуем сделать владельцем отношения сторону контакта. При этом сущности будут выглядеть следующим образом.

@Entity @Table(name = "contacts") public class Contact < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String type; @Column private String data; @ManyToOne private User user; // Конструктор по умолчанию, геттеры, сеттеры и т.д. >@Entity @Table(name = "users") public class User < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String username; // Конструктор по умолчанию, гетеры, сеттеры и т.д. >

Если запустить этот код, то Hibernate создаст следующую структуру таблиц, которая выглядит для нас вполне привычно. Отношение между таблицами создается при помощи ссылочного поля user_id в таблице contacts.

create table contacts ( id bigint not null auto_increment, data varchar(255), type varchar(255), user_id bigint, primary key (id) ) engine=InnoDB; create table users ( id bigint not null auto_increment, username varchar(128) not null, primary key (id) ) engine=InnoDB

Но выбор сущности Contact в качестве стороны владельца отношений в данном случае не очень удачен. Очевидно, что нам чаще нужна информация обо всех контактах пользователя чем о том, какому пользователю принадлежит контакт. Попробуем сделать владельцем контакта сущность пользователя. Для этого убираем поле user из класса Contact и добавляем поле со списком контактов в класс User. Получаем следующий код.

@Entity @Table(name = "contacts") public class Contact < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String type; @Column private String data; // Конструктор по умолчанию, геттеры, сеттеры и т.д. >@Entity @Table(name = "users") public class User < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String username; @OneToMany private Listcontacts; // Конструктор по умолчанию, гетеры, сеттеры и т.д. >

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

create table contacts ( id bigint not null auto_increment, data varchar(255), type varchar(255), primary key (id) ) engine=InnoDB; create table users ( id bigint not null auto_increment, username varchar(128) not null, primary key (id) ) engine=InnoDB; create table users_contacts ( User_id bigint not null, contacts_id bigint not null ) engine=InnoDB;

Чтобы связать сущности Hibernate создал дополнительную таблицу связи (join table) с именем users_contacts, хотя сущности вполне можно было бы связать через ссылочное поле в таблице contacts, как в предыдущем случае. Честно говоря, я не совсем понимаю, почему Hibernate поступает именно так. Буду рад, если кто-то поможет с этим разобраться в комментариях к статье.

Проблему можно легко решить добавив аннотацию JoinColumn к полю contacts.

 @OneToMany @JoinColumn(name = "user_id") private List contacts;

При таких настройках связь будет проводиться при помощи колонки user_id в таблице contacts, а таблица связи создаваться не будет.

Двусторонние отношения

У двусторонних отношений помимо стороны — владельца (owning side) имеется ещё и противоположная сторона (inverse side). Т.е. обе стороны отношения обладают информацией о связи. Логично предположить, что из одностороннего отношения можно сделать двустороннее просто добавив поле и аннотацию в класс сущности противоположной стороны, но не все так просто. В чем именно тут проблема очень хорошо видно на примере отношения многие ко многим. Давайте создадим пример такого отношения между сущностями пользователя и роли этого пользователя.

@Entity @Table(name = "users") public class User < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String username; @ManyToMany private Listroles; // Конструктор по умолчанию, гетеры, сеттеры и т.д. > @Entity @Table(name = "roles") public class Role < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String name; @ManyToMany private Listusers; // Конструктор по умолчанию, гетеры, сеттеры и т.д. >

Запускаем код и смотрим на структуру таблиц. Помимо таблиц для пользователей и ролей Hibernate создаст две таблицы связи, хотя нам хватило бы и одной.

create table roles_users ( Role_id bigint not null, users_id bigint not null ) engine=InnoDB; create table users_roles ( User_id bigint not null, roles_id bigint not null ) engine=InnoDB;

Дело в том, что вместо одного двустороннего отношения мы с вами сейчас создали два односторонних. Тоже самое произойдет и для отношения один ко многим. Чтобы Hibernate понял, что мы хотим создать именно двустороннее отношение нам нужно указать, какая из сторон является владельцем отношений, а какая является обратной стороной. Это делается при помощи атрибута mappedBy. Важно отметить, что указывается этот атрибут в аннотации, которая находится на противоположной стороне отношения.

Для отношения многие ко многим любая из сторон может быть владельцем. В случае с ролями и пользователями выберем сущность пользователя в качестве владельца. Для этого изменим описание поля users в классе Role следующим образом.

 // значение атрибута mappedBy - имя поля связи в классе сущности-владельца отношений @ManyToMany(mappedBy = "roles") private List users;

Теперь Hibernate создаст только одну таблицу связи users_roles.

И напоследок давайте сделаем двусторонним отношение между пользователями и контактами. Следует отметить, что в отношении один ко многим стороной-владельцем может быть только сторона многих (many), поэтому атрибут mappedBy есть только в аннотации @OneToMany . В нашем случае владельцем отношения будет сторона контакта (класс Contact).

@Entity @Table(name = "contacts") public class Contact < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String type; @Column private String data; @ManyToOne private User user; // Конструктор по умолчанию, геттеры, сеттеры и т.д. >@Entity @Table(name = "users") public class User < @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String username; @OneToMany(mappedBy = "user") private Listcontacts; // Конструктор по умолчанию, гетеры, сеттеры и т.д. >

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

На этом все на этот раз! Благодарю, что дочитали до конца и надеюсь, что статья была полезной! Разумеется, очень жду от вас обратную связь в виде голосов и комментариев!

Возможно, будет продолжение!

Источник

Читайте также:  Php does dir exists
Оцените статью