You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

26 KiB

SQL (реляционные) базы данных

FastAPI не требует использования реляционной базы данных. Вы можете воспользоваться любой базой данных, которой хотите.

В этом разделе мы продемонстрируем, как работать с SQLModel.

Библиотека SQLModel построена на основе SQLAlchemy и Pydantic. Она была разработана автором FastAPI специально для приложений на основе FastAPI, которые используют реляционные базы данных.

/// tip | Подсказка

Вы можете воспользоваться любой библиотекой для работы с реляционными (SQL) или нереляционными (NoSQL) базами данных. (Их ещё называют ORM библиотеками). 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

Создайте виртуальное окружение virtual environment{.internal-link target=_blank}, активируйте его и установите sqlmodel:

$ pip install sqlmodel
---> 100%

Создание приложения с единственной моделью

Мы начнем с создания наиболее простой первой версии нашего приложения с одной единственной моделью SQLModel.

В дальнейшем с помощью дополнительных моделей мы его улучшим и сделаем более безопасным и универсальным. 🤓

Создание моделей

Импортируйте SQLModel и создайте модель базы данных:

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:12] hl[8:12] *}

Класс Hero очень напоминает модель Pydantic (фактически, под капотом, это и есть модель Pydantic).

Но есть и некоторые различия

  • table=True для SQLModel означает, что это модель-таблица, которая должна представлять таблицу в реляционной базе данных. Это не просто модель данных (в отличие от обычного класса в Pydantic).

  • Field(primary_key=True) для SQLModel означает, что поле id является первичным ключом в таблице базы данных (вы можете подробнее узнать о первичных ключах баз данных в документации по SQLModel).

    Тип int | None сигнализирует для SQLModel, что столбец таблицы базы данных должен иметь тип INTEGER, или иметь пустое значение NULL.

  • Field(index=True) для SQLModel означает, что нужно создать SQL индекс для данного столбца. Это обеспечит более быстрый поиск при чтении данных, отфильтрованных по данному столбцу.

    SQLModel будет знать, что данные типа str, будут представлены в базе данных как TEXT (или VARCHAR, в зависимости от типа базы данных).

Создание соединения с базой данных (Engine)

В SQLModel объект соединения engine (по сути это Engine из SQLAlchemy) содержит пул соединений к базе данных.

Для обеспечения всех подключений приложения к одной базе данных нужен только один объект соединения engine.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[15:19] hl[15:16,18:19] *} Использование настройки check_same_thread=False позволяет FastAPI использовать одну и ту же SQLite базу данных в различных потоках (threads). Это необходимо, когда **один запрос** использует **более одного потока** (например, в зависимостях).

Не беспокойтесь, учитывая структуру кода, мы позже позаботимся о том, чтобы использовать отдельную SQLModel-сессию на каждый отдельный запрос, это как раз то, что пытается обеспечить check_same_thread.

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

Далее мы добавляем функцию, использующую SQLModel.metadata.create_all(engine), для того, чтобы создать таблицы для каждой из моделей таблицы.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:23] hl[22:23] *}

Создание зависимости Session

Сессия базы данных (Session) хранит объекты в памяти и отслеживает любые необходимые изменения в данных, а затем использует engine для коммуникации с базой данных.

Мы создадим FastAPI-зависимость с помощью yield, которая будет создавать новую сессию (Session) для каждого запроса. Это как раз и обеспечит использование отдельной сессии на каждый отдельный запрос. 🤓

Затем мы создадим объявленную (Annotated) зависимость SessionDep. Мы сделаем это для того, чтобы упростить остальной код, который будет использовать эту зависимость.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[26:31] hl[26:28,31] *}

Создание таблиц базы данных при запуске приложения

Мы будем создавать таблицы базы данных при запуске приложения.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[34:42] hl[34:37, 42] *}

В данном примере мы создаем таблицы при наступлении события запуска приложения.

В продуктовом приложении вы, скорее всего, будете использовать скрипт для миграции базы данных, который выполняется перед запуском приложения. 🤓

/// tip | Подсказка

В SQLModel будут включены утилиты миграции, входящие в состав Alembic, но на данный момент вы просто можете использовать Alembic напрямую.

///

Создание героя (Hero)

Каждая модель в SQLModel является также моделью Pydantic, поэтому вы можете использовать её при объявлении типов, точно также, как и модели Pydantic.

Например, при объявлении параметра типа Hero, она будет считана из тела JSON.

Точно также, вы можете использовать её при объявлении типа значения, возвращаемого функцией, и тогда структурированные данные будут отображены через пользовательский интерфейс автоматически сгенерированной документации FastAPI.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[45:50] hl[45:50] *}

Мы используем зависимость SessionDep (сессию базы данных) для того, чтобы добавить нового героя Hero в объект сессии (Session), сохранить изменения в базе данных, обновить данные героя и затем вернуть их.

Чтение данных о героях

Мы можем читать данные героев из базы данных с помощью select(). Мы можем включить limit и offset для постраничного считывания результатов.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[53:60] hl[56:57,59] *}

Чтение данных отдельного героя

Мы можем прочитать данные отдельного героя (Hero).

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[63:68] hl[65] *}

Удаление данных героя

Мы также можем удалить героя Hero из базы данных.

{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[71:78] hl[76] *}

Запуск приложения

Вы можете запустить приложение следующим образом:

$ fastapi dev main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Далее перейдите в пользовательский интерфейс API /docs. Вы увидите, что FastAPI использует модели для создания документации API. Эти же модели используются для сериализации и проверки данных.

Добавление в приложение дополнительных (вспомогательных) моделей

Теперь давайте проведём рефакторинг нашего приложения, чтобы сделать его более безопасным и более универсальным.

Обратите внимание, что на данном этапе наше приложение позволяет на уровне клиента определять id создаваемого героя (Hero). 😱

Мы не можем этого допустить, т.к. существует риск переписать уже присвоенные id в базе данных. Присвоение id должно происходить на уровне бэкэнда (backend) или на уровне базы данных, но никак не на уровне клиента.

Кроме того, мы создаем секретное имя secret_name для героя, но пока что, мы возвращаем его повсеместно, и это слабо напоминает секретность... 😅

Мы поправим это с помощью нескольких дополнительных (вспомогательных) моделей. Вот где SQLModel по-настоящему покажет себя.

Создание дополнительных моделей

В SQLModel, любая модель с параметром table=True является моделью таблицы.

Любая модель, не содержащая table=True является моделью данных, это по сути обычные модели Pydantic (с несколько расширенным функционалом). 🤓

С помощью SQLModel мы можем использовать наследование, что поможет нам избежать дублирования всех полей.

Базовый класс HeroBase

Давайте начнём с модели HeroBase, которая содержит поля, общие для всех моделей:

  • name
  • age

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[8:10] hl[8:10] *}

Модель таблицы Hero

Далее давайте создадим модель таблицы Hero с дополнительными полями, которых может не быть в других моделях:

  • id
  • secret_name

Модель Hero наследует от HeroBase, и поэтому включает также поля из HeroBase. Таким образом, все поля, содержащиеся в Hero, будут следующими:

  • id
  • name
  • age
  • secret_name

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[8:15] hl[13:15] *}

Публичная модель данных HeroPublic

Далее мы создадим модель HeroPublic. Мы будем возвращать её клиентам API.

Она включает в себя те же поля, что и HeroBase, и, соответственно, поле secret_name в ней отсутствует.

Наконец-то личность наших героев защищена! 🥷

В модели HeroPublic также объявляется поле id: int. Мы как бы заключаем договоренность с API клиентом, на то, что передаваемые данные всегда должны содержать поле id, и это поле должно содержать целое число (и никогда не содержать None).

/// tip | Подсказка

Модель ответа, гарантирующая наличие поля со значением типа int (не None), очень полезна при разработке API клиентов. Определенность в передаваемых данных может обеспечить написание более простого кода.

Также автоматически генерируемые клиенты будут иметь более простой интерфейс. И в результате жизнь разработчиков, использующих ваш API, станет значительно легче. 😎

///

HeroPublic содержит все поля HeroBase, а также поле id, объявленное как int (не None):

  • id
  • name
  • age

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[8:19] hl[18:19] *}

Модель для создания героя HeroCreate

Сейчас мы создадим модель HeroCreate. Эта модель будет использоваться для проверки данных, переданных клиентом.

Она содержит те же поля, что и HeroBase, а также поле secret_name.

Теперь, при создании нового героя, клиенты будут передавать секретное имя secret_name, которое будет сохранено в базе данных, но не будет возвращено в ответе API клиентам.

/// tip | Подсказка

Вот как нужно работать с паролями: получайте их, но не возвращайте их через API.

Также хэшируйте значения паролей перед тем, как их сохранить. Ни в коем случае не храните пароли в открытом виде, как обычный текст.

///

Поля модели HeroCreate:

  • name
  • age
  • secret_name

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[8:23] hl[22:23] *}

Модель для обновления данных героя HeroUpdate

В предыдущих версиях нашей программы мы не могли обновить данные героя, теперь, воспользовавшись дополнительными моделями, мы сможем это сделать. 🎉

Модель данных HeroUpdate в некотором смысле особенная. Она содержит все те же поля, что и модель создания героя, но все поля модели являются необязательными. (Все они имеют значение по умолчанию.) Таким образом, при обновлении данных героя, вам достаточно передать только те поля, которые требуют изменения.

Поскольку все поля по сути меняются (теперь тип каждого поля допускает значение None и значение по умолчанию None), мы должны их объявить заново.

Фактически, нам не нужно наследоваться от HeroBase, потому что мы будем заново объявлять все поля. Я оставлю наследование просто для поддержания общего стиля, но оно (наследование) здесь необязательно. 🤷

Поля HeroUpdate:

  • name
  • age
  • secret_name

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[8:29] hl[26:29] *}

Создание героя с помощью HeroCreate и возвращение результатов с помощью HeroPublic

Теперь, когда у нас есть дополнительные модели, мы можем обновить те части приложения, которые их используют.

Вместе c запросом на создание героя мы получаем объект данных HeroCreate, и создаем на его основе объект модели таблицы Hero.

Созданный объект модели таблицы Hero будет иметь все поля, переданные клиентом, а также поле id, сгенерированное базой данных.

Далее функция вернёт объект модели таблицы Hero. Но поскольку, мы объявили HeroPublic как модель ответа, то FastAPI будет использовать именно её для проверки и сериализации данных.

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[61:67] hl[61:63] *}

/// tip | Подсказка

Теперь мы используем модель ответа response_model=HeroPublic, вместо того, чтобы объявить тип возвращаемого значения как -> HeroPublic. Мы это делаем потому, что тип возвращаемого значения не относится к HeroPublic.

Если бы мы объявили тип возвращаемого значения как -> HeroPublic, то редактор и линтер начали бы ругаться (и вполне справедливо), что возвращаемое значение принадлежит типу Hero, а совсем не HeroPublic.

Объявляя модель ответа в response_model, мы как бы говорим FastAPI: делай свое дело, не вмешиваясь в аннотацию типов и не полагаясь на помощь редактора или других инструментов.

///

Чтение данных героев с помощью HeroPublic

Мы можем проделать то же самое для чтения данных героев. Мы применим модель ответа response_model=list[HeroPublic], и тем самым обеспечим правильную проверку и сериализацию данных.

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[70:77] hl[70] *}

Чтение данных отдельного героя с помощью HeroPublic

Мы можем прочитать данные отдельного героя:

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[80:85] hl[82] *}

Обновление данных героя с помощью HeroUpdate

Мы можем обновить данные героя. Для этого мы воспользуемся HTTP методом PATCH.

В коде мы получаем объект словаря dict с данными, переданными клиентом (т.е. только c данными, переданными клиентом, исключая любые значения, которые могли бы быть там только потому, что они являются значениями по умолчанию). Для того чтобы сделать это, мы воспользуемся опцией exclude_unset=True. В этом главная хитрость. 🪄

Затем мы применим hero_db.sqlmodel_update(hero_data), и обновим hero_db, использовав данные hero_data.

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[88:98] hl[88:89,93:94] *}

Удалим героя ещё раз

Операция удаления героя практически не меняется.

В данном случае желание отрефакторить всё остаётся неудовлетворенным. 😅

{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[101:108] hl[106] *}

Снова запустим приложение

Вы можете снова запустить приложение:

$ fastapi dev main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Если вы перейдете в пользовательский интерфейс API /docs, то вы увидите, что он был обновлен, и больше не принимает параметра id от клиента при создании нового героя, и т.д.

Резюме

Вы можете использовать SQLModel для взаимодействия с реляционными базами данных, а также для упрощения работы с моделями данных и моделями таблиц.

Вы можете узнать гораздо больше информации в документации по SQLModel. Там вы найдете более подробное мини-руководство по использованию SQLModel с FastAPI. 🚀