Как реализуется связь Many-to-Many в реляционной базе данных?

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

Ответ

Связь «многие-ко-многим» (Many-to-Many) всегда реализуется через промежуточную таблицу (junction/join table), что фактически разбивает её на две связи «один-ко-многим» (One-to-Many).

Структура на примере Student и Course:

-- Основные таблицы
CREATE TABLE student (id SERIAL PRIMARY KEY, name TEXT);
CREATE TABLE course (id SERIAL PRIMARY KEY, title TEXT);

-- Промежуточная таблица (реализует Many-to-Many)
CREATE TABLE student_course (
    student_id INT REFERENCES student(id) ON DELETE CASCADE,
    course_id INT REFERENCES course(id) ON DELETE CASCADE,
    enrolled_at TIMESTAMP DEFAULT NOW(), -- Дополнительные данные связи
    PRIMARY KEY (student_id, course_id) -- Составной первичный ключ
);

Соответствующая модель в JPA/Hibernate:

@Entity
public class Student {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

@Entity
public class Course {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

Ключевое преимущество промежуточной таблицы: Она позволяет хранить атрибуты связи (например, enrolled_at, grade), что невозможно при прямой связи Many-to-Many. Для работы с такими атрибутами промежуточную таблицу часто мапят на отдельную сущность (как в исходном ответе), что даёт полный контроль над связью.