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

Что такое Many-to-Many?

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

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

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

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

# Many-to-Many отношения в базах данных

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

1. Концепция Many-to-Many

Пример: Студенты и курсы

  • Один студент может быть записан на несколько курсов
  • Один курс может иметь несколько студентов
┌──────────────┐         ┌──────────────────┐         ┌────────────┐
│   Student    │─────────│ StudentCourse    │─────────│   Course   │
├──────────────┤         ├──────────────────┤         ├────────────┤
│ id (PK)      │         │ student_id (FK)  │         │ id (PK)    │
│ name         │         │ course_id (FK)   │         │ name       │
│              │         │ grade            │         │            │
└──────────────┘         └──────────────────┘         └────────────┘

2. Реализация в SQL

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

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

-- Промежуточная таблица (junction table / association table)
CREATE TABLE student_courses (
    student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
    course_id INT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
    grade VARCHAR(2),
    enrolled_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (student_id, course_id)
);

3. SQLAlchemy ORM

Вариант 1: Без дополнительных полей (простой M2M)

from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    pass

# Association table
association_table = Table(
    'student_courses',
    Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id', ondelete='CASCADE'), primary_key=True),
    Column('course_id', Integer, ForeignKey('courses.id', ondelete='CASCADE'), primary_key=True)
)

class Student(Base):
    __tablename__ = 'students'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    
    # Many-to-Many отношение
    courses: Mapped[list['Course']] = relationship(
        'Course',
        secondary=association_table,
        back_populates='students'
    )

class Course(Base):
    __tablename__ = 'courses'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    
    students: Mapped[list['Student']] = relationship(
        'Student',
        secondary=association_table,
        back_populates='courses'
    )

Вариант 2: С дополнительными полями (ассоциативная таблица как модель)

from datetime import datetime

class StudentCourse(Base):
    __tablename__ = 'student_courses'
    
    student_id: Mapped[int] = mapped_column(ForeignKey('students.id', ondelete='CASCADE'), primary_key=True)
    course_id: Mapped[int] = mapped_column(ForeignKey('courses.id', ondelete='CASCADE'), primary_key=True)
    grade: Mapped[str | None]
    enrolled_date: Mapped[datetime] = mapped_column(default=datetime.now)
    
    # Обратные отношения
    student: Mapped['Student'] = relationship(back_populates='course_registrations')
    course: Mapped['Course'] = relationship(back_populates='student_registrations')

class Student(Base):
    __tablename__ = 'students'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    
    course_registrations: Mapped[list['StudentCourse']] = relationship(
        back_populates='student',
        cascade='all, delete-orphan'
    )
    
    @property
    def courses(self):
        return [reg.course for reg in self.course_registrations]

class Course(Base):
    __tablename__ = 'courses'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    
    student_registrations: Mapped[list['StudentCourse']] = relationship(
        back_populates='course',
        cascade='all, delete-orphan'
    )
    
    @property
    def students(self):
        return [reg.student for reg in self.student_registrations]

4. Практические операции

from sqlalchemy.orm import Session

# Добавление связи
def enroll_student(session: Session, student_id: int, course_id: int):
    student = session.query(Student).get(student_id)
    course = session.query(Course).get(course_id)
    
    if course not in student.courses:
        student.courses.append(course)
        session.commit()

# Получение всех курсов студента
def get_student_courses(session: Session, student_id: int):
    student = session.query(Student).get(student_id)
    return student.courses

# Удаление связи
def unenroll_student(session: Session, student_id: int, course_id: int):
    student = session.query(Student).get(student_id)
    course = session.query(Course).get(course_id)
    
    if course in student.courses:
        student.courses.remove(course)
        session.commit()

# Запрос студентов конкретного курса
def get_course_students(session: Session, course_id: int):
    course = session.query(Course).get(course_id)
    return course.students

5. Сложные запросы

-- Студенты, записанные на курс ID=1
SELECT s.* 
FROM students s
JOIN student_courses sc ON s.id = sc.student_id
WHERE sc.course_id = 1;

-- Курсы студента ID=5
SELECT c.*
FROM courses c
JOIN student_courses sc ON c.id = sc.course_id
WHERE sc.student_id = 5;

-- Студенты на более чем 3 курсах
SELECT s.id, s.name, COUNT(sc.course_id) as course_count
FROM students s
LEFT JOIN student_courses sc ON s.id = sc.student_id
GROUP BY s.id
HAVING COUNT(sc.course_id) > 3;

6. Другие примеры Many-to-Many

  • Теги и статьи: Статья может иметь несколько тегов, тег относится к разным статьям
  • Пользователи и группы: Пользователь в нескольких группах, группа содержит несколько пользователей
  • Рецепты и ингредиенты: Рецепт требует множество ингредиентов, ингредиент используется в разных рецептах

Ключевые выводы:

  • M2M требует промежуточной таблицы для хранения связей
  • Junction table содержит иностранные ключи обеих таблиц
  • При дополнительных данных создавай модель для ассоциативной таблицы
  • Каскадное удаление автоматизирует очистку связей
  • ORM значительно упрощает работу с M2M отношениями