Как реализовать связь многие ко многим в базе данных
Что проверяют
Схему (связующая таблица, составной ключ, индексы) плюс грабли маппинга в Hibernate: неверный тип коллекции или каскад тут стоят дороже, чем кажется на код-ревью.
Как отвечать
SQL. Две таблицы сущностей и связующая с двумя внешними ключами:
create table student_courses (
student_id bigint not null references students(id) on delete cascade,
course_id bigint not null references courses(id) on delete cascade,
primary key (student_id, course_id)
);
create index on student_courses (course_id, student_id);Составной первичный ключ сам по себе запрещает дубли пар и обслуживает выборку «курсы студента». Обратное направление «студенты курса» по нему не идёт, потому что student_id первая колонка индекса, отсюда второй индекс с обратным порядком. Это забывают чаще всего.
Hibernate.
@ManyToMany
@JoinTable(name = "student_courses",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private Set<Course> courses = new HashSet<>();Детали, которые отличают опытный ответ:
- Коллекция должна быть
Set, неList. ДляListбез@OrderColumnHibernate при любом изменении удаляет все строки связи и вставляет их заново. - Владелец связи один, у второй стороны
mappedBy. Записывается только владелец, поэтому нужен вспомогательный метод, обновляющий обе коллекции сразу, иначе объект в памяти разъедется с базой до перезагрузки. - Каскад
REMOVEилиALLна@ManyToManyне ставить: удаление студента снесёт курсы, а не только связи. ДопустимыPERSISTиMERGE. equalsиhashCodeсущности, попадающей вSet, строить на бизнес-ключе, а не на сгенерированном id: до flush id равенnull, и объект «теряется» вHashSetпосле сохранения.- Загрузка ленивая по умолчанию. Выборка списка студентов с курсами без
join fetchили@EntityGraphдаёт N+1. Дваjoin fetchпо коллекциям типаListдаютMultipleBagFetchException.
Когда @ManyToMany заканчивается. Как только у связи появляется собственный атрибут (дата записи, оценка, роль), связующую таблицу мапят отдельной сущностью с двумя @ManyToOne и @EmbeddedId либо суррогатным ключом. Переехать на это позже дорого, поэтому вопрос «будут ли у связи поля» задают до проектирования, а не после.
Где спотыкаются
Рисуют три таблицы и считают ответ завершённым: не помнят про индекс для обратного направления, ставят List вместо Set и получают перезапись всей связи на каждое добавление курса.