В этой статье рассмотрим, как поддерживать согласованность ссылок между двунаправленными отношениями.
Как известно, отношения между @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 используется, но как множитель, а как константа оно совсем бессмысленное)
Спасибо!)