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

Какие знаешь связи с моделями в БД?

1.8 Middle🔥 251 комментариев
#Базы данных (NoSQL)#Базы данных (SQL)

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

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

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

Типы связей в базах данных

В реляционных БД используются несколько основных типов связей между таблицами. Расскажу о каждом с примерами на SQLAlchemy.

One-to-Many (Один ко многим)

Это самая распространённая связь. Один пользователь может иметь много постов.

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

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    posts = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    user_id = Column(Integer, ForeignKey("users.id"))  # Foreign Key
    author = relationship("User", back_populates="posts")

Как работает:

  • В таблице posts есть столбец user_id, который ссылается на users.id
  • Один пользователь → много постов
  • На уровне БД это FOREIGN KEY ограничение

Преимущества:

  • Простая реализация
  • Минимальное дублирование данных
  • Легко добавлять новые посты

Many-to-Many (Много ко многим)

Например, студенты и курсы — студент может учиться на многих курсах, курс может иметь многих студентов.

# Промежуточная таблица (association table)
student_course = Table(
    "student_course",
    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)
    courses = relationship("Course", secondary=student_course, back_populates="students")

class Course(Base):
    __tablename__ = "courses"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    students = relationship("Student", secondary=student_course, back_populates="courses")

Как работает:

  • Нужна промежуточная таблица student_course
  • Она хранит пары (student_id, course_id)
  • Каждая строка означает "этот студент учится на этом курсе"

Когда использовать:

  • Когда связь неоднозначна (не ясно, кто владелец)
  • Когда нужны дополнительные данные о связи

One-to-One (Один к одному)

Например, каждый пользователь имеет один профиль.

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    profile = relationship("Profile", back_populates="user", uselist=False)

class Profile(Base):
    __tablename__ = "profiles"
    id = Column(Integer, primary_key=True)
    bio = Column(String)
    user_id = Column(Integer, ForeignKey("users.id"), unique=True)  # Уникальный FK!
    user = relationship("User", back_populates="profile")

Ключевое отличие:

  • uselist=False говорит, что вернётся один объект, а не список
  • unique=True на foreign key гарантирует, что на одного пользователя может быть только один профиль

Self-Referential (Самоссылающаяся связь)

Это когда таблица ссылается на саму себя. Например, иерархия сотрудников (сотрудник → его менеджер).

class Employee(Base):
    __tablename__ = "employees"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    manager_id = Column(Integer, ForeignKey("employees.id"), nullable=True)
    
    # Сотрудники, которых я менеджирую
    subordinates = relationship("Employee", remote_side=[id], back_populates="manager")
    # Мой менеджер
    manager = relationship("Employee", remote_side=[manager_id], back_populates="subordinates")

Когда использовать:

  • Организационные иерархии
  • Древовидные структуры (папки в папках, категории в категориях)
  • Графы (граф знакомств, сеть друзей)

Polymorphic связи (продвинутые)

Обычно используются для наследования в ORM. Одна таблица может содержать разные типы сущностей.

class Content(Base):
    __tablename__ = "content"
    id = Column(Integer, primary_key=True)
    type = Column(String)  # Дискриминатор
    title = Column(String)
    
    __mapper_args__ = {
        "polymorphic_identity": "content",
        "polymorphic_on": type,
    }

class Article(Content):
    __tablename__ = "articles"
    id = Column(Integer, ForeignKey("content.id"), primary_key=True)
    body = Column(String)
    
    __mapper_args__ = {
        "polymorphic_identity": "article",
    }

class Video(Content):
    __tablename__ = "videos"
    id = Column(Integer, ForeignKey("content.id"), primary_key=True)
    duration = Column(Integer)
    
    __mapper_args__ = {
        "polymorphic_identity": "video",
    }

Каскадное удаление

Очень важно правильно настроить поведение при удалении.

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    user_id = Column(
        Integer,
        ForeignKey("users.id", ondelete="CASCADE"),  # Удалить посты при удалении юзера
    )
    author = relationship("User", cascade="all, delete-orphan")  # На уровне ORM

Варианты каскада:

  • CASCADE → удалить связанные записи
  • SET NULL → установить NULL (для nullable FK)
  • RESTRICT → не удалять, если есть связанные записи (безопасно!)

Практическое использование

# One-to-Many
user = session.query(User).first()
user.posts  # List[Post] - все посты этого пользователя

# Many-to-Many
course = session.query(Course).first()
for student in course.students:  # Итерируемся по студентам
    print(student.name)

# One-to-One
user = session.query(User).first()
user.profile  # Profile | None - один профиль

# Self-Referential
employee = session.query(Employee).first()
employee.manager  # его менеджер
employee.subordinates  # список его подчинённых

Лучшие практики

✅ Выбирай правильный тип связи:

  • One-to-Many когда есть чёткий владелец
  • Many-to-Many когда связь равноправна
  • One-to-One когда нужна оптимизация или разделение таблиц

✅ Используй back_populates:

post = relationship("Post", back_populates="author")  # Двусторонняя связь

✅ Правильно настраивай каскад:

relationship("Post", cascade="all, delete-orphan")

❌ Не забывай про lazy loading проблемы:

# Это может вызвать N+1 запросы!
for user in users:
    print(user.posts)  # Запрос для каждого пользователя

# Используй eager loading
users = session.query(User).options(joinedload(User.posts)).all()

В итоге: я хорошо знаю все основные типы связей в БД и умею правильно их применять для оптимальной структуры данных.

Какие знаешь связи с моделями в БД? | PrepBro