Подключение базы данных через зависимости
Стандартный паттерн работы с БД: пул/engine создаётся один раз в lifespan, а отдельная сессия на каждый запрос выдаётся зависимостью с yield и гарантированно закрывается.
Два уровня жизни: engine/пул живёт всё время работы приложения (создаётся в lifespan), а сессия живёт ровно один запрос (выдаётся зависимостью). Это разделение — ключ к корректной работе с базой.
FastAPI не навязывает ORM, но самый частый выбор — SQLAlchemy. Важно правильно распределить жизненные циклы. Создание подключения к базе — дорогая операция, поэтому пул соединений (engine) поднимают один раз при старте. А вот сессия — рабочая единица транзакции — должна быть своя у каждого запроса, иначе запросы будут мешать друг другу. Сессию выдают зависимостью с yield, которую вы уже изучили.
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import create_engine
from fastapi import Depends
from typing import Annotated
engine = create_engine("postgresql://...")
SessionLocal = sessionmaker(bind=engine)
def get_db():
db = SessionLocal() # своя сессия на запрос
try:
yield db
db.commit() # фиксируем при успехе
except Exception:
db.rollback() # откатываем при ошибке
raise
finally:
db.close() # закрываем всегда
DBSession = Annotated[Session, Depends(get_db)]
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: DBSession):
return db.query(User).get(user_id)
Обратите внимание на транзакционную дисциплину: при успешном завершении запроса транзакция фиксируется (commit), при исключении — откатывается (rollback), и в любом случае сессия закрывается. Эта связка try/except/finally вокруг yield — каноничный шаблон.
Как работает под капотом
Зависимость-сессия — это контекст транзакции: открыли, дали поработать, зафиксировали или откатили, закрыли. Смоделируем транзакционную дисциплину на stdlib:
class FakeSession:
def __init__(self): self.changes, self.committed, self.open = [], False, True
def add(self, x): self.changes.append(x)
def commit(self): self.committed = True; print("COMMIT:", self.changes)
def rollback(self): print("ROLLBACK, откат:", self.changes); self.changes.clear()
def close(self): self.open = False; print("сессия закрыта")
def run_request(work):
db = FakeSession()
try:
work(db) # тело обработчика
db.commit()
except Exception as e:
db.rollback()
print("ошибка:", e)
finally:
db.close()
print("--- успешный запрос ---")
run_request(lambda db: db.add("новый пользователь"))
print("--- запрос с ошибкой ---")
def broken(db):
db.add("временная запись")
raise ValueError("сбой бизнес-логики")
run_request(broken)
Попробуй сам ▶ Видно две ветки: успех → COMMIT, ошибка → ROLLBACK, и в обоих случаях сессия закрывается. Это ровно то, что делает зависимость get_db.
Частые ошибки
Первая — одна общая сессия на всё приложение: запросы начнут портить состояние друг друга. Сессия должна быть на запрос. Вторая — создавать engine внутри обработчика, теряя пул. Третья — забыть про rollback при ошибке, оставляя «грязную» транзакцию. Четвёртая — мешать async-обработчик с синхронной сессией SQLAlchemy без переноса в threadpool (для async нужен async-engine и async-сессия).
Best practices
- Engine/пул — один на приложение (создавайте в lifespan или модуле); сессия — одна на запрос (через зависимость).
- Соблюдайте дисциплину
commit/rollback/closeв зависимости. - Для async-эндпоинтов используйте async-engine и
AsyncSessionсawait. - Не делите одну сессию между параллельными запросами.
Миграции: почему недостаточно create_all
Для учебных примеров схему таблиц создают вызовом create_all, но в реальном проекте этого мало. Как только в продакшене есть данные, любое изменение схемы — добавить колонку, переименовать, сменить тип — должно выполняться аккуратно, без потери данных и согласованно между средами. Для этого существуют миграции: версионированные, упорядоченные изменения схемы, которые можно применять и откатывать. Стандартный инструмент в экосистеме SQLAlchemy — Alembic: он сравнивает модели с текущей схемой, генерирует скрипт миграции, и вы применяете его командой. Подход «просто пересоздать таблицы» допустим только на старте локальной разработки; в команде и в проде он ведёт к потере данных и рассинхрону. Поэтому правильная картина интеграции БД включает третий элемент помимо engine и сессии — систему миграций, которая управляет эволюцией схемы во времени.
Итог: правильная интеграция БД — это два уровня жизни: долгоживущий engine из lifespan и короткоживущая сессия-на-запрос из зависимости с yield, с честными commit/rollback/close.