Как реализовать связь «многие-ко-многим» в реляционной базе данных?

Ответ

Связь "многие-ко-многим" (Many-to-Many) напрямую в реляционных БД не реализуется. Для нее требуется промежуточная таблица-связка (join table, junction table), которая хранит пары внешних ключей, связывающих записи из двух основных таблиц.

Схема на примере Students и Courses:

-- Основные таблицы
CREATE TABLE Students (
    StudentId INT PRIMARY KEY,
    Name NVARCHAR(100)
);

CREATE TABLE Courses (
    CourseId INT PRIMARY KEY,
    Title NVARCHAR(100)
);

-- Таблица-связка
CREATE TABLE StudentCourses (
    StudentId INT,
    CourseId INT,
    EnrollmentDate DATE, -- Можно добавлять дополнительные атрибуты связи
    PRIMARY KEY (StudentId, CourseId),
    FOREIGN KEY (StudentId) REFERENCES Students(StudentId),
    FOREIGN KEY (CourseId) REFERENCES Courses(CourseId)
);

Реализация в Entity Framework Core:

Способ 1: Явная сущность-связка (рекомендуется при наличии дополнительных данных в связи).

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    // Навигационное свойство к связующей сущности
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

// Явная сущность-связка
public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }

    public int CourseId { get; set; }
    public Course Course { get; set; }

    public DateTime EnrollmentDate { get; set; } // Доп. поле
}

Способ 2: Неявная связь (EF Core 5.0+). EF Core автоматически создаст таблицу-связку.

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Course> Courses { get; set; } // Прямая навигация
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<Student> Students { get; set; } // Прямая навигация
}

// Настройка в DbContext (Fluent API):
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
        .HasMany(s => s.Courses)
        .WithMany(c => c.Students)
        .UsingEntity(j => j.ToTable("StudentCourses")); // Опционально: имя таблицы
}

Запросы:

// Найти всех студентов на курсе
var studentsInMath = context.Courses
    .Where(c => c.Title == "Math")
    .SelectMany(c => c.Students)
    .ToList();

// Найти все курсы студента
var johnsCourses = context.Students
    .Where(s => s.Name == "John")
    .SelectMany(s => s.Courses)
    .ToList();

Ответ 18+ 🔞

А, ну это же классика, блядь! Связь "многие-ко-многим" — это как два друга, которые хотят нажраться, но у них нет посредника, который бы купил бухло. Без третьего — нихуя не выйдет.

Вот смотри, в реляционных базах напрямую эту хуйню не сделаешь. Ты не можешь просто так взять и запихнуть кучу айдишников из одной таблицы в другую. Это пиздец как неудобно. Поэтому нужна промежуточная таблица-связка, она же join table. По сути, это такой сводник, который говорит: «Смотри, вот этот студент (StudentId) ебёт мозг на вот этом курсе (CourseId)».

Вот как это выглядит на SQL, если ты вдруг решил, что ORM — это для слабаков:

-- Таблицы с основными данными
CREATE TABLE Students (
    StudentId INT PRIMARY KEY,
    Name NVARCHAR(100)
);

CREATE TABLE Courses (
    CourseId INT PRIMARY KEY,
    Title NVARCHAR(100)
);

-- А вот и наша сводница, сука!
CREATE TABLE StudentCourses (
    StudentId INT,
    CourseId INT,
    EnrollmentDate DATE, -- Можешь тут ещё что угодно прилепить, типа оценки или посещаемости
    PRIMARY KEY (StudentId, CourseId), -- Составной ключ, чтобы один студент не записался на один курс дважды
    FOREIGN KEY (StudentId) REFERENCES Students(StudentId),
    FOREIGN KEY (CourseId) REFERENCES Courses(CourseId)
);

Теперь про Entity Framework Core, потому что вручную писать SQL — это уже мазохизм.

Способ 1: Делаем всё по-взрослому, с явной сущностью-связкой.
Этот способ — твой выбор, если в самой связи есть какие-то дополнительные данные. Ну там, дата зачисления, оценка, а может, и причина, по которой студент решил, что ему нужен этот курс в жопу.

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    // Ссылаемся не сразу на курсы, а на таблицу-посредника
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

// А вот и наша героиня — сущность-связка!
public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }

    public int CourseId { get; set; }
    public Course Course { get; set; }

    public DateTime EnrollmentDate { get; set; } // Дополнительное поле, ради которого весь сыр-бор
}

Способ 2: Магия EF Core 5.0 и выше (неявная связь).
Тут ты делаешь вид, что промежуточной таблицы не существует, а EF Core сам, как хитрая жопа, создаст её за кулисами. Удобно, если тебе от связи нужны только айдишники, без дополнительных полей.

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    // Прямо так и говоришь: у меня есть курсы, нахрен!
    public ICollection<Course> Courses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    // И курсы отвечают: да, и у нас есть студенты, блядь!
    public ICollection<Student> Students { get; set; }
}

// А в DbContext пишешь вот такую портянку:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
        .HasMany(s => s.Courses) // Студент имеет много курсов
        .WithMany(c => c.Students) // Курс имеет много студентов
        .UsingEntity(j => j.ToTable("StudentCourses")); // И всё это безобразие складываем в таблицу с именем "StudentCourses"
}

Ну и как с этим работать, спросишь ты? Да так же, как и всегда, только теперь через коллекции.

// Найти всех студентов, которые мучаются на курсе "Математика"
var studentsInMath = context.Courses
    .Where(c => c.Title == "Math")
    .SelectMany(c => c.Students) // Разворачиваем коллекцию студентов
    .ToList(); // И вуаля — список страдальцев

// Найти все курсы, которые терпит студент по имени "Васян"
var johnsCourses = context.Students
    .Where(s => s.Name == "John")
    .SelectMany(s => s.Courses) // Разворачиваем коллекцию курсов
    .ToList(); // Получаем список его мучений

Вот и вся магия, ёпта. Главное — не забывай, что без промежуточной таблицы твоя «многие-ко-многим» превратится в «многие-к-хуям».