Урок курса

Engine, Session и база SQLite

FastAPI для начинающих: API с базой данных и тестами

В прошлом уроке мы описали модель Room через DeclarativeBase и получили класс, который SQLAlchemy умеет превращать в таблицу. Но саму таблицу мы ещё не создали — для этого нужен Engine, указывающий на конкретный файл базы данных.

Engine и metadata.create_all: подключение к SQLite и создание таблиц

Engine — это точка входа SQLAlchemy к базе данных. Его создают один раз для всего приложения:

from sqlalchemy import create_engine
from models import Base

DATABASE_URL = "sqlite:///./roomly.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

URL sqlite:///./roomly.db означает файловую базу SQLite. Три слеша — это протокол плюс путь; точка-слеш делает путь относительным. Важный момент: ./ отсчитывается от рабочего каталога, в котором вы запускаете uvicorn, а не от расположения файла database.py. Если запустить uvicorn из другого каталога, файл roomly.db окажется там же.

connect_args={"check_same_thread": False} — в SQLAlchemy 2.0.36 это значение уже выставлено по умолчанию для файловой SQLite. Явная передача аргумента не исправляет ошибку, а документирует сделанный выбор: при чтении кода сразу видно, что параметр проверен и сознательно установлен. Этот аргумент не означает, что одну сессию безопасно использовать в нескольких потоках одновременно — об этом ниже.

create_engine не открывает соединение в момент вызова. Движок создаётся мгновенно; реальное обращение к файлу произойдёт при первой операции с базой, например при create_all.

Создание таблиц при импорте

Когда database.py импортируется, Python исполняет строку:

Base.metadata.create_all(bind=engine)

К этому моменту from models import Base уже выполнил весь models.py — в том числе объявление класса Room. Поэтому metadata знает о таблице rooms и создаст её в файле roomly.db, если она там ещё не существует.

Повторный вызов create_all — при следующем перезапуске приложения — безопасен: существующую таблицу он пропустит, данные в ней не тронет. Однако если вы добавили новый столбец в модель Room, create_all его не добавит в уже созданную таблицу. Для изменения схемы существующей базы нужны инструменты миграций — это отдельная тема.

SessionLocal: фабрика сессий и три разных объекта

Engine, фабрика и сессия — три разных уровня, и их легко перепутать.

Engine управляет пулом соединений с базой. Один на всё приложение.

Фабрика создаётся через sessionmaker и знает, как порождать сессии с нужными настройками:

from sqlalchemy.orm import sessionmaker

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

bind=engine связывает фабрику с конкретным движком. Это не означает, что фабрика захватывает одно соединение навсегда — соединения берутся из пула по мере надобности.

Сессия появляется при вызове SessionLocal(). Каждый вызов возвращает отдельный объект Session. Два вызова — два независимых объекта, каждый со своим состоянием транзакции.

Что делают флаги

autocommit=False — стандартный режим работы: сессия не фиксирует изменения автоматически. Чтобы строки попали в базу, нужен явный commit. Закрытие сессии или завершение запроса без commit означает откат незафиксированных изменений.

autoflush=False отключает автоматический flush перед ORM-запросами. В обычном режиме SQLAlchemy перед SELECT сбрасывает накопленные изменения во временную транзакцию, чтобы запрос видел актуальное состояние. С autoflush=False этого не происходит. Но commit всё равно выполняет flush перед фиксацией — этот шаг не зависит от флага. Явный db.flush() тоже всегда доступен.

Flush отправляет изменения в рамках открытой транзакции (SQL-операторы уходят к базе, но транзакция не закрыта). Commit фиксирует транзакцию целиком. Эти операции будут применяться в следующем уроке при реальном CRUD; здесь важно понять, что close не заменяет ни одну из них.

get_db: сессия на запрос через yield и finally

Зависимости с return возвращают значение и на этом заканчиваются. Для ресурсов, которые нужно явно освободить, FastAPI поддерживает зависимости с yield: код до yield выполняется до обработчика, код после — после.

get_db выглядит так:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Когда FastAPI видит функцию-генератор в Depends, он запускает её до yield, получает выданный объект и передаёт его в обработчик. После того как обработчик завершился и ответ сформирован, FastAPI продолжает выполнение генератора — то есть исполняет блок finally. Это происходит до отправки HTTP-ответа клиенту (поведение FastAPI 0.115.6).

finally нужен, чтобы db.close() выполнился в любом случае — и при нормальном завершении обработчика, и при исключении. Если убрать try/finally и оставить db.close() просто после yield, то при исключении в обработчике генератор прервётся на yield и строка закрытия не выполнится. Соединение, если сессия его уже заняла, окажется не возвращено в пул вовремя.

Каждый входящий запрос получает свою сессию: Depends(get_db) вызывает get_db() заново. Именно поэтому check_same_thread=False не делает общую сессию потокобезопасной — общей сессии просто нет.

Полный пример

Вот два файла нового минимального проекта. models.py остаётся из предыдущего урока.

database.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base

DATABASE_URL = "sqlite:///./roomly.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

main.py:

from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
from database import get_db

app = FastAPI()

@app.get("/db-check")
def db_check(db: Session = Depends(get_db)):
    return {"status": "ok"}

Запустите из каталога с этими файлами:

uvicorn main:app --reload

При старте Python импортирует database, исполнит create_all и создаст файл roomly.db с таблицей rooms. GET-запрос на /db-check вернёт 200 {"status": "ok"}. Обработчик получает объект Session, но не делает никаких SQL-запросов — маршрут подтверждает только то, что сессия создана и передана без ошибок. Реальные операции с таблицей появятся в следующем уроке.

Обратите внимание: SessionLocal определён до def get_db(), но это не требование к порядку. Python читает тело функции при вызове, а не при объявлении. Имя SessionLocal должно быть разрешено к моменту, когда генератор доходит до строки db = SessionLocal() — то есть когда FastAPI его запускает, а не когда файл импортируется.

Для FastAPI0.115.6 get_db создаёт SessionLocal(), yield передаёт сессию обработчику. Обычная подготовка ответа и исключение в обработчике сходятся в finally: db.close(). Выход из зависимости выполняется до передачи HTTP-ответа клиенту. Создание Session не обещает немедленного получения соединения; close не заменяет commit.
В закреплённой FastAPI 0.115.6 выход из yield-зависимости выполняется до передачи ответа. finally закрывает Session и при обычной ошибке обработчика.

Попробуйте решить

get_db объявлена так:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Обработчик маршрута принял db через Depends(get_db) и выбросил HTTPException(status_code=404). Что произойдёт с сессией db?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку