Как хранить книги и авторов в базе данных при связи многие-ко-многим
Что проверяют
На уровне схемы всё сводится к связующей таблице, поэтому для Java-разработчика вопрос быстро уходит в JPA и Hibernate: какая сторона владеет связью, почему Set, а не List, что будет с каскадами. Частый уточняющий вопрос: как добавить в связь своё поле, например порядок авторов.
Как отвечать
В базе три таблицы: book, author и book_author с парой внешних ключей и составным первичным ключом (book_id, author_id). PostgreSQL не индексирует внешние ключи сам, поэтому для выборки «книги автора» добавьте индекс по author_id.
В коде:
- владеющая сторона:
Bookс@ManyToManyи@JoinTable(name = "book_author", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "author_id")). В таблицу Hibernate пишет только её изменения; - обратная сторона:
Authorс@ManyToMany(mappedBy = "authors"); SetвместоList: дляListбез порядка Hibernate при удалении одного элемента стирает все строки связи и вставляет их заново, а два таких списка в одномJOIN FETCHдаютMultipleBagFetchException;- вспомогательные методы:
addAuthorиremoveAuthorменяют обе стороны сразу, иначе объекты в памяти расходятся с базой; - загрузка: по умолчанию связь ленивая, нужные данные подтягиваются через
JOIN FETCHили@EntityGraph, а не обращением к коллекции в цикле.
Когда у связи появляются свои поля (порядок, роль, дата), @ManyToMany заменяют отдельной сущностью BookAuthor с @EmbeddedId и двумя @ManyToOne через @MapsId. Это нормальная эволюция модели, а не усложнение ради красоты.
Где спотыкаются
Ставят CascadeType.ALL или REMOVE на @ManyToMany. Удаление книги тогда удаляет и авторов, у которых есть ещё десяток других книг.
Вешают @Data из Lombok на обе сущности. Сгенерированные hashCode и toString ходят по связи в обе стороны и падают со StackOverflowError.
Считают equals и hashCode по сгенерированному id, который до сохранения равен null. После persist хэш меняется, и объект, положенный в Set заранее, уже не находится и не удаляется.