committed by
GitHub
2 changed files with 253 additions and 0 deletions
@ -0,0 +1,252 @@ |
|||
# Дополнительные модели |
|||
|
|||
В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей. |
|||
|
|||
Это особенно применимо в случае моделей пользователя, потому что: |
|||
|
|||
* **Модель для ввода** должна иметь возможность содержать пароль. |
|||
* **Модель для вывода** не должна содержать пароль. |
|||
* **Модель для базы данных**, возможно, должна содержать хэшированный пароль. |
|||
|
|||
!!! danger "Внимание" |
|||
Никогда не храните пароли пользователей в чистом виде. Всегда храните "безопасный хэш", который вы затем сможете проверить. |
|||
|
|||
Если вам это не знакомо, вы можете узнать про "хэш пароля" в [главах о безопасности](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. |
|||
|
|||
## Множественные модели |
|||
|
|||
Ниже изложена основная идея того, как могут выглядеть эти модели с полями для паролей, а также описаны места, где они используются: |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" |
|||
{!> ../../../docs_src/extra_models/tutorial001_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" |
|||
{!> ../../../docs_src/extra_models/tutorial001.py!} |
|||
``` |
|||
|
|||
### Про `**user_in.dict()` |
|||
|
|||
#### `.dict()` из Pydantic |
|||
|
|||
`user_in` - это Pydantic-модель класса `UserIn`. |
|||
|
|||
У Pydantic-моделей есть метод `.dict()`, который возвращает `dict` с данными модели. |
|||
|
|||
Поэтому, если мы создадим Pydantic-объект `user_in` таким способом: |
|||
|
|||
```Python |
|||
user_in = UserIn(username="john", password="secret", email="[email protected]") |
|||
``` |
|||
|
|||
и затем вызовем: |
|||
|
|||
```Python |
|||
user_dict = user_in.dict() |
|||
``` |
|||
|
|||
то теперь у нас есть `dict` с данными модели в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели). |
|||
|
|||
И если мы вызовем: |
|||
|
|||
```Python |
|||
print(user_dict) |
|||
``` |
|||
|
|||
мы можем получить `dict` с такими данными: |
|||
|
|||
```Python |
|||
{ |
|||
'username': 'john', |
|||
'password': 'secret', |
|||
'email': '[email protected]', |
|||
'full_name': None, |
|||
} |
|||
``` |
|||
|
|||
#### Распаковка `dict` |
|||
|
|||
Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python распакует его. Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение. |
|||
|
|||
Поэтому, продолжая описанный выше пример с `user_dict`, написание такого кода: |
|||
|
|||
```Python |
|||
UserInDB(**user_dict) |
|||
``` |
|||
|
|||
Будет работать так же, как примерно такой код: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username="john", |
|||
password="secret", |
|||
email="[email protected]", |
|||
full_name=None, |
|||
) |
|||
``` |
|||
|
|||
Или, если для большей точности мы напрямую используем `user_dict` с любым потенциальным содержимым, то этот пример будет выглядеть так: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username = user_dict["username"], |
|||
password = user_dict["password"], |
|||
email = user_dict["email"], |
|||
full_name = user_dict["full_name"], |
|||
) |
|||
``` |
|||
|
|||
#### Pydantic-модель из содержимого другой модели |
|||
|
|||
Как в примере выше мы получили `user_dict` из `user_in.dict()`, этот код: |
|||
|
|||
```Python |
|||
user_dict = user_in.dict() |
|||
UserInDB(**user_dict) |
|||
``` |
|||
|
|||
будет равнозначен такому: |
|||
|
|||
```Python |
|||
UserInDB(**user_in.dict()) |
|||
``` |
|||
|
|||
...потому что `user_in.dict()` - это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` и ставим перед ним `**`. |
|||
|
|||
Таким образом мы получаем Pydantic-модель на основе данных из другой Pydantic-модели. |
|||
|
|||
#### Распаковка `dict` и дополнительные именованные аргументы |
|||
|
|||
И затем, если мы добавим дополнительный именованный аргумент `hashed_password=hashed_password` как здесь: |
|||
|
|||
```Python |
|||
UserInDB(**user_in.dict(), hashed_password=hashed_password) |
|||
``` |
|||
|
|||
... то мы получим что-то подобное: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username = user_dict["username"], |
|||
password = user_dict["password"], |
|||
email = user_dict["email"], |
|||
full_name = user_dict["full_name"], |
|||
hashed_password = hashed_password, |
|||
) |
|||
``` |
|||
|
|||
!!! warning "Предупреждение" |
|||
Цель использованных в примере вспомогательных функций - не более чем демонстрация возможных операций с данными, но, конечно, они не обеспечивают настоящую безопасность. |
|||
|
|||
## Сократите дублирование |
|||
|
|||
Сокращение дублирования кода - это одна из главных идей **FastAPI**. |
|||
|
|||
Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д. |
|||
|
|||
А все описанные выше модели используют много общих данных и дублируют названия атрибутов и типов. |
|||
|
|||
Мы можем это улучшить. |
|||
|
|||
Мы можем определить модель `UserBase`, которая будет базовой для остальных моделей. И затем мы можем создать подклассы этой модели, которые будут наследовать её атрибуты (объявления типов, валидацию, и т.п.). |
|||
|
|||
Все операции конвертации, валидации, документации, и т.п. будут по-прежнему работать нормально. |
|||
|
|||
В этом случае мы можем определить только различия между моделями (с `password` в чистом виде, с `hashed_password` и без пароля): |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="7 13-14 17-18 21-22" |
|||
{!> ../../../docs_src/extra_models/tutorial002_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="9 15-16 19-20 23-24" |
|||
{!> ../../../docs_src/extra_models/tutorial002.py!} |
|||
``` |
|||
|
|||
## `Union` или `anyOf` |
|||
|
|||
Вы можете определить ответ как `Union` из двух типов. Это означает, что ответ должен соответствовать одному из них. |
|||
|
|||
Он будет определён в OpenAPI как `anyOf`. |
|||
|
|||
Для этого используйте стандартные аннотации типов в Python <a href="https://docs.python.org/3/library/typing.html#typing.Union" class="external-link" target="_blank">`typing.Union`</a>: |
|||
|
|||
!!! note "Примечание" |
|||
При объявлении <a href="https://pydantic-docs.helpmanual.io/usage/types/#unions" class="external-link" target="_blank">`Union`</a>, сначала указывайте наиболее детальные типы, затем менее детальные. В примере ниже более детальный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`. |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="1 14-15 18-20 33" |
|||
{!> ../../../docs_src/extra_models/tutorial003_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="1 14-15 18-20 33" |
|||
{!> ../../../docs_src/extra_models/tutorial003.py!} |
|||
``` |
|||
|
|||
### `Union` в Python 3.10 |
|||
|
|||
В этом примере мы передаём `Union[PlaneItem, CarItem]` в качестве значения аргумента `response_model`. |
|||
|
|||
Поскольку мы передаём его как **значение аргумента** вместо того, чтобы поместить его в **аннотацию типа**, нам придётся использовать `Union` даже в Python 3.10. |
|||
|
|||
Если оно было бы указано в аннотации типа, то мы могли бы использовать вертикальную черту как в примере: |
|||
|
|||
```Python |
|||
some_variable: PlaneItem | CarItem |
|||
``` |
|||
|
|||
Но если мы помещаем его в `response_model=PlaneItem | CarItem` мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа. |
|||
|
|||
## Список моделей |
|||
|
|||
Таким же образом вы можете определять ответы как списки объектов. |
|||
|
|||
Для этого используйте `typing.List` из стандартной библиотеки Python (или просто `list` в Python 3.9 и выше): |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="18" |
|||
{!> ../../../docs_src/extra_models/tutorial004_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="1 20" |
|||
{!> ../../../docs_src/extra_models/tutorial004.py!} |
|||
``` |
|||
|
|||
## Ответ с произвольным `dict` |
|||
|
|||
Вы также можете определить ответ, используя произвольный одноуровневый `dict` и определяя только типы ключей и значений без использования Pydantic-моделей. |
|||
|
|||
Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели). |
|||
|
|||
В этом случае вы можете использовать `typing.Dict` (или просто `dict` в Python 3.9 и выше): |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="6" |
|||
{!> ../../../docs_src/extra_models/tutorial005_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="1 8" |
|||
{!> ../../../docs_src/extra_models/tutorial005.py!} |
|||
``` |
|||
|
|||
## Резюме |
|||
|
|||
Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждой из них. |
|||
|
|||
Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями `password`, `password_hash` и без пароля. |
Loading…
Reference in new issue