23 KiB
SQL (реляционные) базы данных
FastAPI не требует использовать SQL (реляционную) базу данных. Но вы можете использовать любую базу данных, которую хотите.
Здесь мы рассмотрим пример с использованием SQLModel.
SQLModel построен поверх SQLAlchemy и Pydantic. Его создал тот же автор, что и FastAPI, чтобы он идеально подходил для приложений FastAPI, которым нужны SQL базы данных.
/// tip | Подсказка
Вы можете использовать любую другую библиотеку для работы с SQL или NoSQL базами данных (иногда их называют "ORMs"), FastAPI ничего не навязывает. 😎
///
Так как SQLModel основан на SQLAlchemy, вы можете легко использовать любую поддерживаемую SQLAlchemy базу данных (а значит, и поддерживаемую SQLModel), например:
- PostgreSQL
- MySQL
- SQLite
- Oracle
- Microsoft SQL Server, и т.д.
В этом примере мы будем использовать SQLite, потому что она использует один файл и имеет встроенную поддержку в Python. Так что вы можете скопировать этот пример и запустить его как есть.
Позже, для продакшн-приложения, возможно, вы захотите использовать серверную базу данных, например PostgreSQL.
/// tip | Подсказка
Существует официальный генератор проектов на FastAPI и PostgreSQL, включающий frontend и другие инструменты: https://github.com/fastapi/full-stack-fastapi-template
///
Это очень простое и короткое руководство. Если вы хотите узнать больше о базах данных в целом, об SQL или о более продвинутых возможностях, обратитесь к документации SQLModel.
Установка SQLModel
Сначала убедитесь, что вы создали виртуальное окружение{.internal-link target=_blank}, активировали его и затем установили sqlmodel:
$ pip install sqlmodel
---> 100%
Создание приложения с единственной моделью
Сначала мы создадим самую простую первую версию приложения с одной моделью SQLModel.
Позже мы улучшим его, повысив безопасность и универсальность, добавив несколько моделей. 🤓
Создание моделей
Импортируйте SQLModel и создайте модель базы данных:
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *}
Класс Hero очень похож на модель Pydantic (фактически, под капотом, это и есть модель Pydantic).
Есть несколько отличий:
-
table=Trueсообщает SQLModel, что это модель-таблица, она должна представлять таблицу в SQL базе данных, это не просто модель данных (как обычный класс Pydantic). -
Field(primary_key=True)сообщает SQLModel, чтоid— это первичный ключ в SQL базе данных (подробнее о первичных ключах можно узнать в документации SQLModel).Благодаря типу
int | None, SQLModel будет знать, что этот столбец должен бытьINTEGERв SQL базе данных и должен допускать значениеNULL. -
Field(index=True)сообщает SQLModel, что нужно создать SQL индекс для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу.SQLModel будет знать, что объявленное как
strстанет SQL-столбцом типаTEXT(илиVARCHAR, в зависимости от базы данных).
Создание Engine
Объект engine в SQLModel (под капотом это engine из SQLAlchemy) удерживает соединения с базой данных.
У вас должен быть один объект engine для всей кодовой базы, чтобы подключаться к одной и той же базе данных.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *}
Параметр check_same_thread=False позволяет FastAPI использовать одну и ту же базу данных SQLite в разных потоках. Это необходимо, так как один запрос может использовать больше одного потока (например, в зависимостях).
Не волнуйтесь, с такой структурой кода мы позже обеспечим использование одной сессии SQLModel на запрос, по сути именно этого и добивается check_same_thread.
Создание таблиц
Далее мы добавим функцию, которая использует SQLModel.metadata.create_all(engine), чтобы создать таблицы для всех моделей-таблиц.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *}
Создание зависимости Session
Session хранит объекты в памяти и отслеживает необходимые изменения в данных, затем использует engine для общения с базой данных.
Мы создадим зависимость FastAPI с yield, которая будет предоставлять новую Session для каждого запроса. Это и обеспечивает использование одной сессии на запрос. 🤓
Затем мы создадим объявленную (Annotated) зависимость SessionDep, чтобы упростить остальной код, который будет использовать эту зависимость.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *}
Создание таблиц базы данных при старте
Мы создадим таблицы базы данных при запуске приложения.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *}
Здесь мы создаём таблицы в обработчике события запуска приложения.
Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
/// tip | Подсказка
В SQLModel появятся утилиты миграций - обёртки над Alembic, но пока вы можете использовать Alembic напрямую.
///
Создание героя (Hero)
Так как каждая модель SQLModel также является моделью Pydantic, вы можете использовать её в тех же аннотациях типов, в которых используете модели Pydantic.
Например, если вы объявите параметр типа Hero, он будет прочитан из JSON body (тела запроса).
Аналогично вы можете объявить её как тип возвращаемого значения функции, и тогда форма данных отобразится в автоматически сгенерированном UI документации API.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *}
Здесь мы используем зависимость SessionDep (это Session), чтобы добавить нового Hero в экземпляр Session, зафиксировать изменения в базе данных, обновить данные в hero и затем вернуть его.
Чтение героев
Мы можем читать записи Hero из базы данных с помощью select(). Можно добавить limit и offset для постраничного вывода результатов.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *}
Чтение одного героя
Мы можем прочитать одного Hero.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *}
Удаление героя
Мы также можем удалить Hero.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *}
Запуск приложения
Вы можете запустить приложение:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Затем перейдите в UI /docs. Вы увидите, что FastAPI использует эти модели для документирования API, а также для сериализации и валидации данных.
Обновление приложения с несколькими моделями
Теперь давайте немного отрефакторим приложение, чтобы повысить безопасность и универсальность.
Если вы посмотрите на предыдущую версию, в UI видно, что до сих пор клиент мог сам задавать id создаваемого Hero. 😱
Так делать нельзя, иначе они могли бы перезаписать id, который уже присвоен в БД. Решение по id должно приниматься бэкендом или базой данных, а не клиентом.
Кроме того, мы создаём для героя secret_name, но пока что возвращаем его повсюду — это не очень секретно... 😅
Мы исправим это, добавив несколько дополнительных моделей. Здесь SQLModel раскроется во всей красе. ✨
Создание нескольких моделей
В SQLModel любая модель с table=True — это модель-таблица.
Любая модель без table=True — это модель данных, по сути обычная модель Pydantic (с парой небольших дополнений). 🤓
С SQLModel мы можем использовать наследование, чтобы избежать дублирования полей.
HeroBase — базовый класс
Начнём с модели HeroBase, которая содержит общие поля для всех моделей:
nameage
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *}
Hero — модель-таблица
Далее создадим Hero, фактическую модель-таблицу, с дополнительными полями, которых может не быть в других моделях:
idsecret_name
Так как Hero наследуется от HeroBase, он также имеет поля, объявленные в HeroBase, поэтому все поля Hero:
idnameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *}
HeroPublic — публичная модель данных
Далее мы создадим модель HeroPublic, именно она будет возвращаться клиентам API.
У неё те же поля, что и у HeroBase, поэтому она не включает secret_name.
Наконец-то личность наших героев защищена! 🥷
Также здесь заново объявляется id: int. Тем самым мы заключаем контракт с клиентами API: они всегда могут рассчитывать, что поле id присутствует и это int (никогда не None).
/// tip | Подсказка
Гарантия того, что в модели ответа значение всегда присутствует и это int (не None), очень полезна для клиентов API — так можно писать гораздо более простой код.
Кроме того, автоматически сгенерированные клиенты будут иметь более простые интерфейсы, и разработчикам, взаимодействующим с вашим API, будет работать значительно комфортнее. 😎
///
Все поля HeroPublic такие же, как в HeroBase, а id объявлен как int (не None):
idnameage
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *}
HeroCreate — модель данных для создания героя
Теперь создадим модель HeroCreate, она будет валидировать данные от клиентов.
У неё те же поля, что и у HeroBase, а также есть secret_name.
Теперь, когда клиенты создают нового героя, они будут отправлять secret_name, он сохранится в базе данных, но не будет возвращаться клиентам в API.
/// tip | Подсказка
Так следует обрабатывать пароли: принимать их, но не возвращать в API.
Также перед сохранением значения паролей нужно хэшировать, никогда не храните их в открытом виде.
///
Поля HeroCreate:
nameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *}
HeroUpdate — модель данных для обновления героя
В предыдущей версии приложения у нас не было способа обновлять героя, но теперь, с несколькими моделями, мы можем это сделать. 🎉
Модель данных HeroUpdate особенная: у неё те же поля, что и для создания нового героя, но все поля необязательные (у всех есть значение по умолчанию). Таким образом, при обновлении героя можно отправлять только те поля, которые нужно изменить.
Поскольку фактически меняются все поля (их тип теперь включает None, и по умолчанию они равны None), нам нужно переобъявить их.
Наследоваться от HeroBase не обязательно, так как мы заново объявляем все поля. Я оставлю наследование для единообразия, но это не необходимо. Скорее дело вкуса. 🤷
Поля HeroUpdate:
nameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *}
Создание с HeroCreate и возврат HeroPublic
Теперь, когда у нас есть несколько моделей, мы можем обновить части приложения, которые их используют.
Мы получаем в запросе модель данных HeroCreate и на её основе создаём модель-таблицу Hero.
Новая модель-таблица Hero будет иметь поля, отправленные клиентом, а также id, сгенерированный базой данных.
Затем возвращаем из функции ту же модель-таблицу Hero как есть. Но так как мы объявили response_model с моделью данных HeroPublic, FastAPI использует HeroPublic для валидации и сериализации данных.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *}
/// tip | Подсказка
Теперь мы используем response_model=HeroPublic вместо аннотации типа возвращаемого значения -> HeroPublic, потому что фактически возвращаемое значение — это не HeroPublic.
Если бы мы объявили -> HeroPublic, ваш редактор кода и линтер справедливо пожаловались бы, что вы возвращаете Hero, а не HeroPublic.
Объявляя модель в response_model, мы говорим FastAPI сделать своё дело, не вмешиваясь в аннотации типов и работу редактора кода и других инструментов.
///
Чтение героев с HeroPublic
Аналогично мы можем читать Hero — снова используем response_model=list[HeroPublic], чтобы данные валидировались и сериализовались корректно.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *}
Чтение одного героя с HeroPublic
Мы можем прочитать одного героя:
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *}
Обновление героя с HeroUpdate
Мы можем обновить героя. Для этого используем HTTP операцию PATCH.
В коде мы получаем dict со всеми данными, отправленными клиентом — только с данными, отправленными клиентом, исключая любые значения, которые были бы там лишь как значения по умолчанию. Для этого мы используем exclude_unset=True. Это главный трюк. 🪄
Затем мы используем hero_db.sqlmodel_update(hero_data), чтобы обновить hero_db данными из hero_data.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *}
Снова удаление героя
Операция удаления героя остаётся практически прежней.
Желание «отрефакторить всё» на этот раз останется неудовлетворённым. 😅
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *}
Снова запустим приложение
Вы можете снова запустить приложение:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Если вы перейдёте в UI API /docs, вы увидите, что он обновился: теперь при создании героя он не ожидает получить id от клиента и т. д.
Резюме
Вы можете использовать SQLModel для взаимодействия с SQL базой данных и упростить код с помощью моделей данных и моделей-таблиц.
Гораздо больше вы можете узнать в документации SQLModel, там есть более подробный мини-туториал по использованию SQLModel с FastAPI. 🚀