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

Задача 1
get_db объявлена так:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Обработчик маршрута принял db через Depends(get_db) и выбросил HTTPException(status_code=404). Что произойдёт с сессией db?
Задача 2
В рабочем каталоге ещё нет roomly.db. Выполнена только строка engine = create_engine("sqlite:///./roomly.db"), create_all и другие операции с базой не вызывались. Обязан ли файл уже появиться на диске?
Задача 3
В файле roomly.db есть таблица rooms с двумя строками. Base.metadata.create_all(bind=engine) выполнен повторно, других операций с данными нет. Сколько из этих двух строк останется в rooms? Введите одно целое число.
Задача 4
Сессия для каждого обращения
Roomly занимается управлением переговорными комнатами и до недавнего времени хранила весь каталог прямо в памяти приложения. Теперь команда готовится перенести эти данные в полноценное хранилище и для начала закладывает правильный фундамент: нужно убедиться, что соединение с хранилищем открывается, используется и закрывается так, как этого требует работа с реальными ресурсами.
Главный принцип, который команда хочет соблюсти, прост: каждое обращение к системе должно работать со своей собственной сессией. Никакой общей сессии, которую все запросы делят между собой и которая рано или поздно оказывается в непредсказуемом состоянии. Сессия открывается в начале обработки, сопровождает её до конца и затем аккуратно освобождает занятые ресурсы — независимо от того, завершилось ли обращение успехом или привело к ошибке.
Особенно важно последнее: сбой в одном обращении не должен оставлять за собой незакрытых ресурсов и тем более мешать тем обращениям, которые придут следом. Это требование звучит очевидно, но именно оно чаще всего нарушается, когда освобождение ресурсов доверяют случаю. Отдельного внимания заслуживает и повторная инициализация хранилища: если данные в нём уже есть, они должны остаться нетронутыми.
Результат, к которому стремится команда, — это работающая обвязка вокруг хранилища, которая ведёт себя предсказуемо при любом исходе: сессия появляется тогда, когда нужна, и исчезает, когда её работа закончена.
Что уже дано
Это отдельная однофайловая лаба настройки SQLAlchemy, а не перенос полного Roomly. В редакторе уже есть Base, модель Room(id, name, capacity), app и два законченных диагностических маршрута. Они выполняют предоставленные SQL-операции; писать запросы к строкам самостоятельно в этом уроке не нужно. TODO относятся только к Engine, фабрике и жизненному циклу Session.
Перед видимым файлом служебная подготовка создаёт пустой временный каталог внутри рабочей папки и задаёт DATABASE_URL: str — SQLite URL собственного файла roomly.db. Используйте именно это имя; создавать путь или открывать прежние базы не нужно. Публичный переходник выполняет HTTP через TestClient, передаёт диагностические наблюдения и читает этот же файл отдельным SQLite-соединением. Данные для проверок поступают через RPC, ожидаемые ответы в переходнике отсутствуют. После проверки он закрывает свои ресурсы и удаляет только собственный временный каталог. Писать переходник, SQL-диагностику, управление файлами или тесты не требуется. Наблюдатель удерживает ссылки на выданные Session, их соединения и загруженные ORM-объекты, чтобы проверить освобождение после успеха и после преднамеренного исключения. Он не выдаёт и не закрывает Session вместо вашей get_db во время запросов.
Что нужно сделать
Создайте публичный engine для DATABASE_URL и SessionLocal через sessionmaker: каждый вызов фабрики даёт отдельную Session, связанную с engine; autoflush=False, обычный режим без autocommit. Для файловой SQLite SQLAlchemy2 допускает как явный check_same_thread=False, так и его стандартное значение.
Реализуйте init_db(): она создаёт отсутствующие таблицы зарегистрированного Base и сохраняет существующие строки при повторном вызове. Готовый вызов внизу выполняет инициализацию при запуске. Реализуйте get_db() как зависимость с yield: новая Session остаётся рабочей в течение каждого обработчика и обязательно закрывается после его завершения, в том числе при исключении. Не используйте общую Session для нескольких обращений. Способ гарантированного закрытия выбираете вы.
Сохраните готовые app, модель и обработчики. GET /db-check возвращает200 с status="ok" и числом строк комнаты; GET /db-failure после обращения к той же базе преднамеренно возвращает500 с detail="Intentional diagnostic failure". Следующий запрос должен снова получить свою рабочую Session. Эти диагностические ответы уже реализованы и не являются задачей на их форматирование.
Ввод и вывод
stdin не используется. Печатать через print в stdout ничего не нужно. После выполнения файла проверяющая система обращается к верхнеуровневому app: публичный переходник вызывает TestClient внутри процесса приложения и передаёт результат скрытой проверке. Запускать Uvicorn, сеть, клиентский скрипт или pytest не требуется.
Если в собственной учебной таблице две комнаты, GET /db-check возвращает {"status":"ok","room_count":2}. После GET /db-failure новый успешный запрос видит те же две комнаты; повтор init_db не очищает таблицу.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- Engine, Session и база SQLite — задача по FastAPI
Практическая задача по теме «Engine, Session и база SQLite» с проверкой ответа онлайн.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
