Подключение базы данных через зависимости

Стандартный паттерн работы с БД: пул/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.

Проверьте себя
1. Почему сессию БД создают на каждый запрос, а не одну общую на всё приложение?
AТак требует Python
BОбщая сессия между параллельными запросами приведёт к тому, что они будут портить транзакционное состояние друг друга
CЭто ускоряет создание engine
DОбщая сессия запрещена SQLAlchemy
2. Что должна сделать зависимость-сессия при возникновении исключения в обработчике?
AЗафиксировать транзакцию (commit)
BОткатить транзакцию (rollback) и затем закрыть сессию в finally
CОставить транзакцию открытой
DПерезапустить приложение