Связи между таблицами: 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 держит обе стороны в согласии. Это завершает блок про базы данных; дальше — практика: собираем настоящее приложение.