
Когда два объекта могут иметь множественные связи друг с другом, мы говорим о отношении "многие ко многим". В базах данных это достигается с помощью "промежуточной" или "ассоциативной" таблицы. В SQLAlchemy это также может быть эффективно реализовано благодаря ORM.
Давайте представим, что у нас есть две сущности: Student и Course. Один студент может записаться на несколько курсов, и один курс может иметь несколько студентов.
Для реализации такого отношения нам понадобится промежуточная таблица:
from sqlalchemy import Table, Column, Integer, ForeignKey
association_table = Table('student_course_association', Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id')),
    Column('course_id', Integer, ForeignKey('courses.id'))
)Далее определим модели Student и Course:
from sqlalchemy.orm import relationship
class Student(Base):
    __tablename__ = 'students'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    courses = relationship("Course", secondary=association_table, back_populates="students")
class Course(Base):
    __tablename__ = 'courses'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    students = relationship("Student", secondary=association_table, back_populates="courses")Обратите внимание на параметр secondary, который указывает на промежуточную таблицу.
Теперь, когда у нас есть модели, можно добавлять данные:
math = Course(name="Mathematics")
john = Student(name="John Doe", courses=[math])
session.add(john)
session.commit()И запросить их:
john = session.query(Student).filter_by(name="John Doe").first()
for course in john.courses:
    print(course.name)Если вы хотите, чтобы записи из промежуточной таблицы автоматически удалялись, когда вы удаляете объект (например, студента или курс), вы можете добавить параметр cascade:
students = relationship("Student", secondary=association_table, back_populates="courses", cascade="all, delete-orphan")Иногда вам может потребоваться добавить дополнительные атрибуты в промежуточную таблицу. В таком случае вы можете превратить промежуточную таблицу в полноценную модель:
class Enrollment(Base):
    __tablename__ = 'student_course_association'
    student_id = Column(Integer, ForeignKey('students.id'), primary_key=True)
    course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True)
    grade = Column(String)
    student = relationship("Student", back_populates="course_enrollments")
    course = relationship("Course", back_populates="student_enrollments")
class Student(Base):
    # ...
    course_enrollments = relationship("Enrollment", back_populates="student")
class Course(Base):
    # ...
    student_enrollments = relationship("Enrollment", back_populates="course")При выполнении запросов в базу данных с отношением Many-to-Many, часто используется оператор JOIN. SQLAlchemy делает это автоматически за вас:
# Получение всех студентов, записанных на определенный курс
course = session.query(Course).filter_by(name="Mathematics").first()
students_on_course = session.query(Student).join(association_table).filter(association_table.c.course_id == course.id).all()В реальной жизни, чтобы избежать дублирования записей в ассоциативной таблице, полезно устанавливать уникальные ограничения на комбинации значений. Например:
association_table = Table('student_course_association', Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id')),
    Column('course_id', Integer, ForeignKey('courses.id')),
    UniqueConstraint('student_id', 'course_id', name='uix_1')
)Одна из частых проблем, с которой сталкиваются разработчики при работе с ORM - это проблема N+1 запросов. Она возникает, когда для получения связанных данных требуется выполнить отдельный запрос для каждого объекта в коллекции. Чтобы избежать этой проблемы, используйте joinedload:
from sqlalchemy.orm import joinedload
courses = session.query(Course).options(joinedload(Course.students)).all()Мы рассматривали различные стратегии загрузки с One-to-Many отношением, и они также применимы к Many-to-Many. Например, вы можете использовать lazy="dynamic", чтобы иметь возможность дополнительно фильтровать и сортировать связанные объекты перед их загрузкой.
В зависимости от объема данных и частоты доступа к связанным объектам, Many-to-Many отношение может стать причиной замедления работы вашего приложения. Регулярно анализируйте производительность своих запросов и используйте инструменты, такие как индексы базы данных, для оптимизации запросов.
Отношение Many-to-Many в SQLAlchemy – это мощный инструмент для представления сложных связей в вашей базе данных. С помощью промежуточных таблиц и ORM SQLAlchemy вы можете эффективно управлять этими связями в вашем Python-коде.
Содержание: