Комментарии (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 отношениями