Как в реляционных базах данных реализуется связь ‘многие-ко-многим’ (Many-to-Many)?

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

Ответ

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

Ключевые принципы:

  1. Создание связующей таблицы: Она содержит как минимум два поля — внешние ключи (FOREIGN KEY) для каждой из основных таблиц.
  2. Составной первичный ключ: Для обеспечения уникальности пар связей (например, чтобы один студент не был записан на один и тот же курс дважды), для этих двух внешних ключей часто создается составной первичный ключ (PRIMARY KEY).

Пример на SQL (PostgreSQL)

Предположим, у нас есть студенты и курсы. Один студент может посещать много курсов, и на одном курсе может быть много студентов.

-- Таблица студентов
CREATE TABLE Students (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

-- Таблица курсов
CREATE TABLE Courses (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100) NOT NULL
);

-- Промежуточная таблица для связи Students и Courses
CREATE TABLE StudentCourses (
    student_id INT REFERENCES Students(id) ON DELETE CASCADE,
    course_id INT REFERENCES Courses(id) ON DELETE CASCADE,
    -- Составной первичный ключ для уникальности пары (студент, курс)
    PRIMARY KEY (student_id, course_id)
);

Пример в ORM (SQLAlchemy для Python)

В ORM-системах эта связь настраивается через специальный атрибут (secondary), который указывает на связующую таблицу.

from sqlalchemy import Column, Integer, String, Table, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

# Описание промежуточной таблицы без создания отдельного класса модели
student_courses_table = Table('student_courses',
    Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id'), primary_key=True),
    Column('course_id', Integer, ForeignKey('courses.id'), primary_key=True)
)

class Student(Base):
    __tablename__ = 'students'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    # Связь с Course через таблицу student_courses_table
    courses = relationship("Course", secondary=student_courses_table, back_populates="students")

class Course(Base):
    __tablename__ = 'courses'
    id = Column(Integer, primary_key=True)
    title = Column(String)
    # Обратная связь для удобства запросов
    students = relationship("Student", secondary=student_courses_table, back_populates="courses")