Как реализовать связь многие ко многим в базе данных
Что проверяют
Знаете ли вы, что в реляционной модели связь «многие ко многим» напрямую не выражается, и умеете ли довести её до рабочей схемы в EF Core. Следом почти всегда спрашивают: а если у связи появились свои данные, например дата записи на курс?
Как отвечать
Основа: третья, связующая таблица StudentCourses с двумя внешними ключами. Дальше детали, которые отличают продуманную схему от учебной:
- составной первичный ключ
(StudentId, CourseId): он же гарантирует, что одна пара не запишется дважды. СуррогатныйIdбез уникального ограничения на пару пропускает дубли; - индекс по второй колонке: составной ключ ускоряет поиск курсов студента, но не студентов курса. Для обратного направления нужен отдельный индекс по
CourseId; - каскадное удаление: при удалении студента его строки в связующей таблице должны уходить вместе с ним, иначе внешний ключ не даст удалить запись;
- EF Core без явной сущности: начиная с EF Core 5 достаточно коллекций навигации с обеих сторон, фреймворк сам создаст связующую таблицу.
UsingEntityвOnModelCreatingзадаёт её имя и колонки; - явная сущность связи: как только нужны дополнительные поля (
EnrolledAt, роль, статус), заводите классStudentCourseи настраиваете его черезUsingEntity<StudentCourse>или как две связи «один ко многим».
Про чтение стоит сказать отдельно: Include(s => s.Courses) на большой выборке даёт декартов взрыв строк, и тут помогает AsSplitQuery(). Для списков, где нужны только названия, лучше проекция через Select, чем загрузка сущностей целиком.
Где спотыкаются
Хранят список идентификаторов строкой через запятую или JSON-массивом в одной колонке. Ни внешних ключей, ни индекса, ни нормального JOIN после этого нет.
Делают связующую таблицу с суррогатным ключом и забывают про уникальность пары. Двойной клик на «Записаться» оставляет две одинаковые строки.
Не знают, что неявная связь EF Core не даёт доступа к дополнительным колонкам. Когда появляется дата записи, приходится переделывать модель и писать миграцию, хотя это стоило предусмотреть сразу.