← Назад к вопросам

Как работает many-to-many под копотом?

2.3 Middle🔥 211 комментариев
#Django#Архитектура и паттерны#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Many-to-Many отношения в БД и ORM

Many-to-many (M2M) — это отношение, когда запись из одной таблицы может быть связана с несколькими записями в другой таблице, и наоборот. Это одно из самых частых отношений в реальных приложениях.

Как это работает на уровне БД

В реляционных БД many-to-many реализуется с помощью соединительной таблицы (junction table):

-- Таблица студентов
CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

-- Таблица курсов
CREATE TABLE courses (
    id INT PRIMARY KEY,
    title VARCHAR(255)
);

-- Соединительная таблица
CREATE TABLE student_courses (
    student_id INT,
    course_id INT,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

Теперь один студент может быть в нескольких курсах, и один курс может быть у нескольких студентов.

Пример запроса

-- Получить все курсы студента с id=1
SELECT c.* FROM courses c
INNER JOIN student_courses sc ON sc.course_id = c.id
WHERE sc.student_id = 1;

-- Получить всех студентов курса с id=5
SELECT s.* FROM students s
INNER JOIN student_courses sc ON sc.student_id = s.id
WHERE sc.course_id = 5;

-- Получить количество курсов на студента
SELECT s.id, s.name, COUNT(sc.course_id) as course_count
FROM students s
LEFT JOIN student_courses sc ON sc.student_id = s.id
GROUP BY s.id, s.name;

SQLAlchemy реализация

В SQLAlchemy many-to-many описывается очень просто с помощью параметра secondary:

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

Base = declarative_base()

# Вспомогательная таблица (SQLAlchemy создаёт её автоматически)
association_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(255))
    
    # Определяем отношение
    courses = relationship(
        Course,
        secondary=association_table,
        back_populates=students
    )

class Course(Base):
    __tablename__ = courses
    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    
    # Обратное отношение
    students = relationship(
        Student,
        secondary=association_table,
        back_populates=courses
    )

Работа с данными

from sqlalchemy.orm import Session

session = Session()

# Создание
student = Student(name=Alice)
course = Course(title=Python)
session.add(student)
session.add(course)
session.commit()

# Добавление связи
student.courses.append(course)
session.commit()

# Получение данных
print(student.courses)  # [<Course Python>]
print(course.students)  # [<Student Alice>]

# Удаление связи
student.courses.remove(course)
session.commit()

# Удаление с каскадом
session.delete(student)  # Удалит студента и его связи
session.commit()

Более сложный случай: добавление атрибутов связи

Иногда нужно добавить данные в саму связь (например, дату добавления студента на курс):

class StudentCourse(Base):
    __tablename__ = student_courses
    student_id = Column(Integer, ForeignKey(students.id), primary_key=True)
    course_id = Column(Integer, ForeignKey(courses.id), primary_key=True)
    enrolled_date = Column(DateTime, default=datetime.now)  # Доп. атрибут
    grade = Column(String(1))
    
    # Relationship к родительским таблицам
    student = relationship(Student, back_populates=enrollments)
    course = relationship(Course, back_populates=enrollments)

class Student(Base):
    __tablename__ = students
    id = Column(Integer, primary_key=True)
    name = Column(String(255))
    enrollments = relationship(StudentCourse, back_populates=student)

class Course(Base):
    __tablename__ = courses
    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    enrollments = relationship(StudentCourse, back_populates=course)

# Использование
enrollment = StudentCourse(student=student, course=course, grade=A)
session.add(enrollment)
session.commit()

Производительность

При работе с M2M нужно помнить о N+1 проблеме:

# Плохо: N+1 запрос
for student in students:
    print(student.courses)  # Каждая итерация = новый запрос

# Хорошо: использовать eager loading
students = session.query(Student).options(
    joinedload(Student.courses)
).all()  # Один запрос с JOIN

for student in students:
    print(student.courses)  # Данные уже загружены

Many-to-many — это фундаментальная концепция реляционных БД, которая позволяет моделировать сложные связи между сущностями.