В этой статье рассмотрим, как поддерживать согласованность ссылок между двунаправленными отношениями.
Как известно, отношения между @Entity-классами бывают:
- однонаправленные (unidirectional): когда только у одного из двух классов есть ссылка на другой класс. А обратной ссылки нет.
- двунаправленные (bidirectional): когда классы ссылаются друг на друга взаимно.
В двунаправленных отношениях возникает проблема согласованности ссылок. Допустим, конкретный объект Comment ссылается на конкретный Topic. В то время как этот топик не содержит в своей коллекции этот Comment (по ошибке программиста). Что же должен делать Hibernate при сохранении Comment? Сохранить его в данном топике (физически это будет означать внешний ключ в таблице comment, ссылающийся на данную запись в таблице topic)? Или же нет?
Вообще для разрешения подобных противоречий в Hibernate есть понятие owning side (главная сторона). Неглавная сторона помечается свойством mappedBy, ее ссылки имеют второстепенную важность. А главная сторона имеет приоритет.
Но несмотря на некоторую способность Hibernate разруливать противоречия с помощью owning side, все же можно столкнуться с ошибками, если не поддерживать синхронизацию вручную.
Поэтому ниже приведем методы синхронизации, которые всегда надо добавлять в двусторонние (bidirectional) отношения и пользоваться ими во избежание ошибок. Рассмотрим все отношения:
- ManyToOne (+OneToMany)
- OneToOne
- ManyToMany
ManyToOne и OneToMany
Это топик с комментариями. Каждый комментарий относится ровно к одному топику, а топик содержит коллекцию комментариев.
Сначала структура в базе:

Ниже показаны классы. При этом owning side — Comment, ссылки в классе Comment имеют приоритет. А коллекция в topic помечена с помощью mappedBy.
Класс Comment:
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String text;
@ManyToOne(fetch = FetchType.LAZY)
private Topic topic;
// getters/setters/constructors
}
Класс Topic:
@Entity
public class Topic {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String title;
@OneToMany(mappedBy = "topic", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
public void addComment(Comment comment){
this.comments.add(comment);
comment.setTopic(this);
}
public void removeComment(Comment comment){
this.comments.remove(comment);
comment.setTopic(null);
}
// getters/setters/constructors
}
Как видите, класс Topic ссылается на Comment, а Comment — на Topic, что означает двунаправленное отношение.
Методы синхронизации находятся в классе Topic: это addComment() и removeComment().
Этими методами нужно пользоваться при добавлении/удалении комментариев в коллекцию. Они не только регулируют состав коллекции, но и присваивают комментарию правильную ссылку на топик.
Пример использования — создание топика с комментариями:
Topic topic = new Topic("Topic1");
Comment comment1 = new Comment("comment1");
Comment comment2 = new Comment("comment1");
topic.addComment(comment1);
topic.addComment(comment2);
topicRepository.save(topic);
Удаление комментария из топика:
Topic topic = topicRepository.getOne(-1l); topic.removeComment(topic.getComments().get(0));
С помощью метода removeComment() мы удалили его из коллекции и обнулили его ссылку на топик.
OneToOne
Рассмотрим классы User и UseDetails, которые имеют отношение OneToOne.

Класс User:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToOne(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private UserDetails details;
public void setDetails(UserDetails details) {
if (details == null) {
if (this.details != null) {
this.details.setUser(null);
}
}
else {
details.setUser(this);
}
this.details = details;
}
// getters/setters/constructors
Класс UserDetails:
@Entity
public class UserDetails {
@Id
private long id;
private String phone;
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "id")
private User user;
// getters/setters/constructors
}
Пример использования методов
Добавление User (и UserDetails):
User user = new User();
user.setName("Vasya");
UserDetails details = new UserDetails();
details.setPhone("345345");
user.setDetails(details);
userRepository.save(user);
Выше мы устанавливаем user-у UserDetails. Поскольку setDetails() — синхронизирующий метод, то в нем UserDetails-у также присваивается наш user.
В базе сохраняется как user, так и details, потому что (о каскадах) cascade = CascadeType.ALL, а значит в том числе CascadeType.PERSIST.
Отсоединение UserDetails (и удаление UserDetails):
User user = userRepository.findByName("Petr");
user.setDetails(null);
userRepository.save(user);
Тут тоже null устанавливается взаимно, details перестает ссылаться на user и подлежит удалению (благодаря orphanRemoval=true).
ManyToMany
Теперь рассмотрим отношение ManyToMany. А именно авторов и книги — у одного автора может быть много книг, и у каждой книги может быть несколько авторов.

Главной стороной (owning side) будет Author. Здесь же и методы синхронизации addBook(), removeBook():
Класс Author:
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
public Author(String name){
this.name=name;
}
@ManyToMany (cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "author_book",
joinColumns = @JoinColumn(name = "author_id"),
inverseJoinColumns = @JoinColumn(name = "book_id")
)
private Set<Book> books=new HashSet<>();
public void addBook(Book book){
this.books.add(book);
book.getAuthors().add(this);
}
public void removeBook(Book book){
this.books.remove(book);
book.getAuthors().remove(this);
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Author)) return false;
return id != null && id.equals(((Author) o).getId());
}
@Override
public int hashCode() {
return 31;
}
// getters/setters/constructors
}
В методах addBook(), removeBook() не только добавляется (удаляется) книга в коллекцию, но и на противоположной стороне в книгу добавляется (удаляется) автор.
CascadeType.REMOVE уже не используется, он противопоказан в ManyToMany отношениях.
Класс Book:
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
@ManyToMany(mappedBy = "books")
private Set<Author> authors=new HashSet<>();
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Author)) return false;
return id != null && id.equals(((Book) o).getId());
}
@Override
public int hashCode() {
return 31;
}
// getters/setters/constructors
}
Пример использования
Добавим автора с книгами в базу:
Author author = new Author("a1");
author.addBook(new Book("b1"));
author.addBook(new Book("b2"));
authorRepository.save(author);
Отсоединим книгу от автора и удалим ее из базы:
Book book = bookRepository.findByName("b1");
author.removeBook(book);
bookRepository.delete(book);
Итоги
Все двунаправленные отношения необходимо синхронизировать.
Исходный код примеров есть на GitHub.
А что будет если их не синхронизировать, я просто никогда не видел каких либо проблем если нет синхронизации. По моему hibernate сам справляется с подобной синхронизацией и проставляет противоположную связь.
может где-то и срабатывает, но отсутствие проблем не гарантируется, как я понимаю.
Не могли бы Вы прокомментировать следующие вопросы:
1) в ч.3 (ManyToMany), почему методы синхронизации добавлены только на главной стороне — для Author, а для Book их нет?
2) в ч.3 (ManyToMany), с чем связано, что переопределенный метод hashCode() возвращает 31? Т.е. понятно, что все экземпляры данного класса попадут в область памяти под одним и тем же индексом, вычисленной данной хеш-функцией (поправьте, если ошибаюсь). Но почему именно число 31, а не какое-либо другое простое целое или функция? Встречал такое несколько раз, но пояснения не нашел 🙁
3) в ч.1 (ManyToOne и OneToMany), почему в классе-сущности Topic, содержащей в качестве поля коллекцию List с комментариями, не переопределены методы equals() и hashCode(), как по аналогии с классами-сущностями в ч.3 (ManyToMany)? Т.е. равны только в том случае, если это тот же самый объект, или по уникальному идентификатору (id), и хеш-функция, возвращающая простое целое?
Заранее признателен.
1. Можно и для Book добавить, если нужно (просто добавить add/remove по аналогии, ничего больше менять не потребуется).
3. equals/hashCode используется, только если помещаем объект в Set/Map, для List это неважно.
2. Случайно, в статье это значения не имеет. Лучше, конечно, иначе задать хэш-функцию. С константой 31 решение неэффективное. (Да, число 31 используется, но как множитель, а как константа оно совсем бессмысленное)
Спасибо!)