В этой статье рассмотрим, почему в отношении ManyToMany оптимальнее использовать для коллекций Set, а не List.
Модель
Рассмотрим пример: у автора может быть несколько книг, а у книги — несколько авторов. Схема в базе состоит из трех таблиц:
Отношение в классах двунаправленное: то есть и у автора есть ссылка на коллекцию книг, и у книги на коллекцию авторов. Причем автор Author является главной стороной (его ссылки приоритетнее).
Сначала будем изучать поведение List (потом Set), поэтому в классе Author используется коллекция List.
Класс Author:
@NoArgsConstructor @Data @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 List<Book> books=new ArrayList<>(); 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; } }
Как говорилось, в двунаправленные отношения надо добавлять методы синхронизации (addBook() и removeBook()) для поддержания согласованности ссылок. Чтобы не было ситуации, когда у автора есть такая-то книга в коллекции, а у книги нет в коллекции этого автора.
Book является неглавной стороной, поэтому коллекция авторов аннотирована mappedBy:
@NoArgsConstructor @Data @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; private String name; @ManyToMany(mappedBy = "books") private Set<Author> authors=new HashSet<>(); public Book(String name){ this.name=name; } @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; } }
Теперь перейдем к демонстрации неоптимального поведения, которое наблюдается при использовании List.
Удаление элемента коллекции из List
Для этого напишем тест. До его выполнения добавим в базу двух авторов и две книги (книга b1 принадлежит обоим авторам):
@DataJpaTest @Commit public class ManyToManyTest { @Autowired private AuthorRepository authorRepository; @Autowired private BookRepository bookRepository; @BeforeEach public void booksShouldBeAdded() { Author author1 = new Author("a1"); Book b1 = new Book("b1"); Book b2 = new Book("b2"); author1.addBook(b1); author1.addBook(b2); authorRepository.save(author1); Author author2 = new Author("a2"); author2.addBook(b1); authorRepository.save(author2); Assertions.assertEquals(2, authorRepository.count()); Assertions.assertEquals(2, bookRepository.count()); } @Test @DisplayName("отсоединение книги от автора") public void whenDeleteAuthorFromBook_thenOneDeleteStatement() { Author author = authorRepository.findByName("a1"); Book book = bookRepository.findByName("b1"); author.removeBook(book); } }
А в самом тесте просто удалим книгу b1 из коллекции автора a1 (именно из автора, а не из базы вообще. В базе b1 остается (b1 принадлежит еще a2). То есть обновиться должна средняя таблица author_book: из нее должна быть удалена строка b1a1.
В консоли отображаются следующие SQL-операторы:
Hibernate: delete from author_book where author_id=? Hibernate: insert into author_book (author_id, book_id) values (?, ?)
То есть сначала удаляются все записи для автора, а потом обратно вставляются те, что удалять не надо.
И это неоптимально: как говорилось выше, достаточно одного delete.
И его можно получить, если сменить List на Set.
Удаление элемента коллекции из Set
Заменим в классе Book коллекцию List на Set:
@JoinTable(name = "author_book", joinColumns = @JoinColumn(name = "author_id"), inverseJoinColumns = @JoinColumn(name = "book_id") ) private Set books=new HashSet();
Выполним тот же тест и получим один оператор delete:
Hibernate: delete from author_book where author_id=? and book_id=?
Итоги
Таким образом, в отношениях ManyToMany оптимальнее использовать Set, а не List. Так выполняется меньше SQL-операторов.
Исходный код примера есть на GitHub.
Спасибо за статью!