Связи между таблицами: relationship

Реальные данные связаны: у пользователя много постов, у поста один автор. Связи описываются внешним ключом и relationship — и тогда ORM сама подтягивает связанные объекты.
Связь «один-ко-многим» — основа моделей данных. Внешний ключ (ForeignKey) в таблице постов указывает на пользователя-автора; relationship даёт навигацию: user.posts вернёт список постов, post.author — их автора. Никакого ручного JOIN.

Данные редко живут в одной таблице. Блог: пользователи и посты. Каждый пост принадлежит одному пользователю, у пользователя — много постов. Это связь один-ко-многим. Технически её задаёт внешний ключ: столбец в постах, хранящий id автора.

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

class User(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(db.String(80))
    posts: Mapped[list["Post"]] = relationship(back_populates="author")

class Post(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(db.String(200))
    user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    author: Mapped["User"] = relationship(back_populates="posts")

Два слоя: user_id = ForeignKey("user.id") — физическая связь в базе (столбец-ссылка); relationship — удобство в Python (навигация по объектам). back_populates связывает обе стороны: добавил пост в user.posts — у поста заполнится author.

user = User(name="Анна")
post = Post(title="Привет", author=user)   # связали объекты
db.session.add(user)
db.session.commit()

print(user.posts)        # [<Post Привет>]
print(post.author.name)  # "Анна"

Связи — это то, ради чего вообще берут реляционную базу. Два уровня связи стоит держать раздельно в голове: ForeignKey обеспечивает целостность на уровне СУБД (нельзя сослаться на несуществующего пользователя), а relationship — это удобство навигации в Python (user.posts, post.author) без ручных JOIN. Параметр back_populates держит обе стороны согласованными: добавил пост в user.posts — и у поста автоматически проставится author. Главная производительная ловушка связей — проблема N+1: перебирая список родителей и обращаясь к их связям в цикле, ты порождаешь по запросу на каждого. Лечится жадной загрузкой (selectinload, joinedload), которая подтягивает связанные строки одним-двумя запросами вместо сотни.

Как работает под капотом

В базе связь — это просто столбец user_id в таблице постов, равный id автора. relationship прячет JOIN: когда ты читаешь user.posts, ORM выполняет SELECT по постам с этим user_id и собирает объекты.

  users                  posts
  ┌────┬───────┐         ┌────┬─────────┬──────────┐
  │ id │ name  │         │ id │ title   │ user_id  │
  ├────┼───────┤         ├────┼─────────┼──────────┤
  │ 1  │ Анна  │◀────────│ 10 │ Привет  │ 1        │
  │ 2  │ Боб   │    ▲    │ 11 │ Заметка │ 1        │
  └────┴───────┘    └────│ 12 │ Идея    │ 2        │
                         └────┴─────────┴──────────┘
  user.posts  → SELECT * FROM posts WHERE user_id = 1

Смоделируем связь по внешнему ключу обычным Python.

users = [{"id": 1, "name": "Анна"}, {"id": 2, "name": "Боб"}]
posts = [
    {"id": 10, "title": "Привет", "user_id": 1},
    {"id": 11, "title": "Заметка", "user_id": 1},
    {"id": 12, "title": "Идея", "user_id": 2},
]

def posts_of(user):                       # как user.posts
    return [p for p in posts if p["user_id"] == user["id"]]

def author_of(post):                      # как post.author
    return next(u for u in users if u["id"] == post["user_id"])

anna = users[0]
print("Посты Анны:", [p["title"] for p in posts_of(anna)])
print("Автор поста 12:", author_of(posts[2])["name"])

Запусти: по user_id мы находим и посты пользователя, и автора поста. relationship делает ровно это, но автоматически и возвращая объекты-модели, а не словари.

Частые ошибки

  • ForeignKey без relationship. Связь в базе будет, но навигации user.posts не появится.
  • Рассогласованный back_populates. Имена на обеих сторонах должны указывать друг на друга.
  • Проблема N+1. Перебирать users и для каждого дёргать posts — это лавина запросов. Используй жадную загрузку (selectinload).

Best practices

  • Описывай обе стороны связи через back_populates для согласованности.
  • Для списков связанных объектов при выборке многих родителей применяй eager loading, избегая N+1.
  • ForeignKey задаёт целостность на уровне базы — не пренебрегай им.

Что запомнить

  • ForeignKey — физическая связь в базе (столбец-ссылка с id).
  • relationship — навигация по объектам в Python (user.posts, post.author).
  • back_populates держит обе стороны связи согласованными.
  • Проблему N+1 решают жадной загрузкой (selectinload/joinedload).

Итог: ForeignKey хранит связь физически, relationship даёт удобную навигацию по объектам, back_populates держит обе стороны в согласии. Это завершает блок про базы данных; дальше — практика: собираем настоящее приложение.

Проверьте себя
1. Чем ForeignKey отличается от relationship?
AЭто синонимы
BForeignKey — физический столбец-ссылка в базе, relationship — навигация по объектам в Python
Crelationship быстрее
DForeignKey только для SQLite
2. Что такое проблема N+1 при работе со связями?
AОшибка переполнения
BЛавина запросов: для каждого родителя отдельный запрос за связанными объектами
CНехватка памяти базы
DДублирование первичных ключей