Всем нам хорошо известен ответ на вопрос, какими могут быть отношения между сущностями в 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 List<Contact> contacts;
// Конструктор по умолчанию, гетеры, сеттеры и т.д.
}
Теперь владельцем отношения является сущность пользователя, что более логично, но если запустить данный код и посмотреть на созданную 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<Contact> 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 List<Role> roles;
// Конструктор по умолчанию, гетеры, сеттеры и т.д.
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@ManyToMany
private List<User> users;
// Конструктор по умолчанию, гетеры, сеттеры и т.д.
}
Запускаем код и смотрим на структуру таблиц. Помимо таблиц для пользователей и ролей 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<User> 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 List<Contact> contacts;
// Конструктор по умолчанию, гетеры, сеттеры и т.д.
}
Для такого кода Hibernate создаст привычную нам структуру из двух таблиц со ссылкой на пользователя в таблице контактов.
На этом все на этот раз! Благодарю, что дочитали до конца и надеюсь, что статья была полезной! Разумеется, очень жду от вас обратную связь в виде голосов и комментариев!
Возможно, будет продолжение!
1nt3g3r
Из интересных моментов, которые я сталкивался — это работа с каскадным удалением связанных сущностей.
Например, по дефолту hibernate при удалении связанных сущностей вначалей делает update связанной сущности, присваивая внешнему ключу значение null, а лишь потом удаляет связанную сущность. Фиксится поведение указанием updatable=false в аннотации @JoinColumn.
Ещё один момент — если мы указываем каскадное удаление связанных сущностей, то мы отдаем это на откуп hibernate, в БД ограничения указываются без on delete cascade. Как результат, мы не можем удалить главную сущность, не удалив всех родителей. Не всегда это нужно, и часто удобней чтобы БД контролировала ссылочную целостностность. Лечится это поведение прописыванием явным constraints в аннотации @ForeignKey.
usharik Автор
Спасибо! Это интересно. У меня есть идея второй части этого материала с описанием подобных проблем/странностей. А не попадалось ли вам объяснения, почему создается таблица связей для OneToMany? Мне нигде не попадалось объяснения почему именно такое поведение выбрано дефолтным.
poxvuibr
Может быть потому что для того, чтобы сделать однонаправленную связь на OneToMany придётся снять ограничение not null на внешний ключ? По крайней мере я не раз встречался с ситуациями, когда из-за этого разработчики делали связь один ко многим на дополнительной таблице
usharik Автор
Попробовал `@JoinColumn(name = «user_id», nullable = false)` для односторонней связи. В результате поле user_id становится not null, но связь остается через ссылочное поле.