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

«Как реализовать связь «многие-ко-многим» в реляционной базе данных?» — вопрос из категории Базы данных, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Связь "многие-ко-многим" (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();