Урок курса
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 его запускает, а не когда файл импортируется.

Попробуйте решить
get_db объявлена так:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Обработчик маршрута принял db через Depends(get_db) и выбросил HTTPException(status_code=404). Что произойдёт с сессией db?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
