committed by
GitHub
1 changed files with 358 additions and 0 deletions
@ -0,0 +1,358 @@ |
|||
# SQL (реляционные) базы данных |
|||
|
|||
**FastAPI** не требует использования реляционной базы данных. Вы можете воспользоваться любой базой данных, которой хотите. |
|||
|
|||
В этом разделе мы продемонстрируем, как работать с <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a>. |
|||
|
|||
Библиотека **SQLModel** построена на основе <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> и Pydantic. Она была разработана автором **FastAPI** специально для приложений на основе FastAPI, которые используют **реляционные базы данных**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вы можете воспользоваться любой библиотекой для работы с реляционными (SQL) или нереляционными (NoSQL) базами данных. (Их ещё называют <abbr title="ORM = Object Relational Mapper, этот термин для библиотеки, в которой классы представляют SQL-таблицы, а экземпляры классов представляют строки в этих таблицах.">**ORM**</abbr> библиотеками). FastAPI не принуждает вас к использованию чего-либо конкретного. 😎 |
|||
|
|||
/// |
|||
|
|||
В основе SQLModel лежит SQLAlchemy, поэтому вы спокойно можете использовать любую базу данных, поддерживаемую SQLAlchemy (и, соответственно, поддерживаемую SQLModel), например: |
|||
|
|||
* PostgreSQL |
|||
* MySQL |
|||
* SQLite |
|||
* Oracle |
|||
* Microsoft SQL Server, и т.д. |
|||
|
|||
В данном примере мы будем использовать базу данных **SQLite**, т.к. она состоит из единственного файла и поддерживается встроенными библиотеками Python. Таким образом, вы сможете скопировать данный пример и запустить его как он есть. |
|||
|
|||
В дальнейшем, для продакшн-версии вашего приложения, возможно, вам стоит использовать серверную базу данных, например, **PostgreSQL**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Существует официальный генератор проектов на **FastAPI** и **PostgreSQL**, который также включает frontend и дополнительные инструменты <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> |
|||
|
|||
/// |
|||
|
|||
Это очень простое и короткое руководство, поэтому, если вы хотите узнать о базах данных в целом, об SQL, разобраться с более продвинутым функционалом, то воспользуйтесь <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">документацией SQLModel</a>. |
|||
|
|||
## Установка `SQLModel` |
|||
|
|||
Создайте виртуальное окружение [virtual environment](../virtual-environments.md){.internal-link target=_blank}, активируйте его и установите `sqlmodel`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install sqlmodel |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Создание приложения с единственной моделью |
|||
|
|||
Мы начнем с создания наиболее простой первой версии нашего приложения с одной единственной моделью **SQLModel**. |
|||
|
|||
В дальнейшем с помощью **дополнительных моделей** мы его улучшим и сделаем более безопасным и универсальным. 🤓 |
|||
|
|||
### Создание моделей |
|||
|
|||
Импортируйте `SQLModel` и создайте модель базы данных: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} |
|||
|
|||
Класс `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[14:18] hl[14:15,17:18] *} |
|||
|
|||
Использование настройки `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: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, но на данный момент вы просто можете использовать |
|||
<a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a> напрямую. |
|||
|
|||
/// |
|||
|
|||
### Создание героя (Hero) |
|||
|
|||
Каждая модель в SQLModel является также моделью Pydantic, поэтому вы можете использовать её при **объявлении типов**, точно также, как и модели Pydantic. |
|||
|
|||
Например, при объявлении параметра типа `Hero`, она будет считана из **тела JSON**. |
|||
|
|||
Точно также, вы можете использовать её при объявлении типа значения, возвращаемого функцией, и тогда структурированные данные будут отображены через пользовательский интерфейс автоматически сгенерированной документации FastAPI. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} |
|||
|
|||
Мы используем зависимость `SessionDep` (сессию базы данных) для того, чтобы добавить нового героя `Hero` в объект сессии (`Session`), сохранить изменения в базе данных, обновить данные героя и затем вернуть их. |
|||
|
|||
### Чтение данных о героях |
|||
|
|||
Мы можем **читать** данные героев из базы данных с помощью `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] *} |
|||
|
|||
### Запуск приложения |
|||
|
|||
Вы можете запустить приложение следующим образом: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Далее перейдите в пользовательский интерфейс API `/docs`. Вы увидите, что **FastAPI** использует модели для создания документации API. Эти же модели используются для сериализации и проверки данных. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image01.png"> |
|||
</div> |
|||
|
|||
## Добавление в приложение дополнительных (вспомогательных) моделей |
|||
|
|||
Теперь давайте проведём **рефакторинг** нашего приложения, чтобы сделать его более безопасным и более универсальным. |
|||
|
|||
Обратите внимание, что на данном этапе наше приложение позволяет на уровне клиента определять `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[7:9] hl[7:9] *} |
|||
|
|||
#### Модель таблицы `Hero` |
|||
|
|||
Далее давайте создадим **модель таблицы** `Hero` с дополнительными полями, которых может не быть в других моделях: |
|||
|
|||
* `id` |
|||
* `secret_name` |
|||
|
|||
Модель `Hero` наследует от `HeroBase`, и поэтому включает также поля из `HeroBase`. Таким образом, все поля, содержащиеся в `Hero`, будут следующими: |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} |
|||
|
|||
#### Публичная модель данных `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[7:18] hl[17:18] *} |
|||
|
|||
#### Модель для создания героя `HeroCreate` |
|||
|
|||
Сейчас мы создадим модель `HeroCreate`. Эта модель будет использоваться для проверки данных, переданных клиентом. |
|||
|
|||
Она содержит те же поля, что и `HeroBase`, а также поле `secret_name`. |
|||
|
|||
Теперь, при создании нового героя, клиенты будут передавать секретное имя `secret_name`, которое будет сохранено в базе данных, но не будет возвращено в ответе API клиентам. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вот как нужно работать с **паролями**: получайте их, но не возвращайте их через API. |
|||
|
|||
Также хэшируйте значения паролей перед тем, как их сохранить. Ни в коем случае не храните пароли в открытом виде, как обычный текст. |
|||
|
|||
/// |
|||
|
|||
Поля модели `HeroCreate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} |
|||
|
|||
#### Модель для обновления данных героя `HeroUpdate` |
|||
|
|||
В предыдущих версиях нашей программы мы не могли обновить данные героя, теперь, воспользовавшись дополнительными моделями, мы сможем это сделать. 🎉 |
|||
|
|||
Модель данных `HeroUpdate` в некотором смысле особенная. Она содержит все те же поля, что и модель создания героя, но все поля модели являются **необязательными**. (Все они имеют значение по умолчанию.) Таким образом, при обновлении данных героя, вам достаточно передать только те поля, которые требуют изменения. |
|||
|
|||
Поскольку **все поля по сути меняются** (теперь тип каждого поля допускает значение `None` и значение по умолчанию `None`), мы должны их **объявить заново**. |
|||
|
|||
Фактически, нам не нужно наследоваться от `HeroBase`, потому что мы будем заново объявлять все поля. Я оставлю наследование просто для поддержания общего стиля, но оно (наследование) здесь необязательно. 🤷 |
|||
|
|||
Поля `HeroUpdate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} |
|||
|
|||
### Создание героя с помощью `HeroCreate` и возвращение результатов с помощью `HeroPublic` |
|||
|
|||
Теперь, когда у нас есть дополнительные модели, мы можем обновить те части приложения, которые их используют. |
|||
|
|||
Вместе c запросом на создание героя мы получаем объект данных `HeroCreate`, и создаем на его основе объект модели таблицы `Hero`. |
|||
|
|||
Созданный объект *модели таблицы* `Hero` будет иметь все поля, переданные клиентом, а также поле `id`, сгенерированное базой данных. |
|||
|
|||
Далее функция вернёт объект *модели таблицы* `Hero`. Но поскольку, мы объявили `HeroPublic` как модель ответа, то **FastAPI** будет использовать именно её для проверки и сериализации данных. |
|||
|
|||
{* ../../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` |
|||
|
|||
Мы можем проделать то же самое **для чтения данных** героев. Мы применим модель ответа `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` с данными, переданными клиентом (т.е. **только c данными, переданными клиентом**, исключая любые значения, которые могли бы быть там только потому, что они являются значениями по умолчанию). Для того чтобы сделать это, мы воспользуемся опцией `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] *} |
|||
|
|||
### Снова запустим приложение |
|||
|
|||
Вы можете снова запустить приложение: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если вы перейдете в пользовательский интерфейс API `/docs`, то вы увидите, что он был обновлен, и больше не принимает параметра `id` от клиента при создании нового героя, и т.д. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image02.png"> |
|||
</div> |
|||
|
|||
## Резюме |
|||
|
|||
Вы можете использовать <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a> для взаимодействия с реляционными базами данных, а также для упрощения работы с **моделями данных** и **моделями таблиц**. |
|||
|
|||
Вы можете узнать гораздо больше информации в документации по **SQLModel**. Там вы найдете более подробное <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">мини-руководство по использованию SQLModel с **FastAPI**</a>. 🚀 |
Loading…
Reference in new issue