如何使用数据类在 Pydantic 中构建自引用模型?

Lio*_*yon 11 python pydantic

我正在使用 FastAPI 和 pydantic 构建 API。

\n

当我遵循 DDD / clean 架构(将模型的定义与持久层的定义分开)时,我在模型中使用标准 lib 数据类,然后使用命令式映射(即经典映射)将它们映射到 SQLAlchemy 表。

\n

这很完美:

\n
@dataclass\nclass User:\n    name: str\n    age: int\n\n@pydantic.dataclasses.dataclass\nclass PydanticUser(User):\n     ...\n
Run Code Online (Sandbox Code Playgroud)\n

但是,我在定义具有自引用的类时遇到了问题。

\n

\xe2\x9c\x85 类继承自 Pydantic\xe2\x80\x99s BaseModel 可以自引用

\n

从 pydantic\xe2\x80\x99s BaseModel 继承是可行的,但这与 SQLAlchemy 命令式映射不兼容,我想用它来坚持干净的架构/DDD 原则。

\n
class BaseModelPerson(BaseModel):\n     name: str\n     age: int\n     parent_person: BaseModelPerson = None\n\nBaseModelPerson.update_forward_refs()\njohn = BaseModelPerson(name="John", age=49, parent_person=None)\ntim = BaseModelPerson(name="Tim", age=14, parent_person=john)\n\nprint(john)\n# BaseModelPerson(name=\'John\', age=49, parent_person=None)\nprint(tim)\n# BaseModelPerson(name=\'Tim\', age=14, parent_person=BaseModelPerson(name=\'John\', age=49, parent_person=None))\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x9c\x85 标准库数据类也可以自引用

\n
from __future__ import annotations\nfrom dataclasses import dataclass\n\n@dataclass\nclass StdlibPerson:\n    name: str\n    age: int\n    parent: StdlibPerson\n\n\njohn = StdlibPerson(name="John", age=49, parent=None)\ntim = StdlibPerson(name="Tim", age=14, parent=john)\n\nprint(john)\n# StdlibPerson(name=\'John\', age=49, parent=None)\n\nprint(tim)\n# StdlibPerson(name=\'Tim\', age=14, parent=StdlibPerson(name=\'John\', age=49, parent=None))\n\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x9d\x8c Pydantic 数据类转换导致递归错误

\n

当我尝试将标准库数据类转换为 pydantic 数据类时,出现问题。

\n

像这样定义 Pydantic 数据类:

\n
PydanticPerson = pydantic.dataclasses.dataclass(StdlibPerson)\n
Run Code Online (Sandbox Code Playgroud)\n

返回错误:

\n
# output (hundreds of lines - that is recursive indeed)\n\n    # The name of an attribute on the class where we store the Field\n  File "pydantic/main.py", line 990, in pydantic.main.create_model\n  File "pydantic/main.py", line 299, in pydantic.main.ModelMetaclass.__new__\n  File "pydantic/fields.py", line 411, in pydantic.fields.ModelField.infer\n  File "pydantic/fields.py", line 342, in pydantic.fields.ModelField.__init__\n  File "pydantic/fields.py", line 456, in pydantic.fields.ModelField.prepare\n  File "pydantic/fields.py", line 673, in pydantic.fields.ModelField.populate_validators\n  File "pydantic/class_validators.py", line 255, in pydantic.class_validators.prep_validators\n  File "pydantic/class_validators.py", line 238, in pydantic.class_validators.make_generic_validator\n  File "/usr/lib/python3.9/inspect.py", line 3111, in signature\n    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)\n  File "/usr/lib/python3.9/inspect.py", line 2860, in from_callable\n    return _signature_from_callable(obj, sigcls=cls,\n  File "/usr/lib/python3.9/inspect.py", line 2323, in _signature_from_callable\n    return _signature_from_function(sigcls, obj,\n  File "/usr/lib/python3.9/inspect.py", line 2155, in _signature_from_function\n    if _signature_is_functionlike(func):\n  File "/usr/lib/python3.9/inspect.py", line 1883, in _signature_is_functionlike\n    if not callable(obj) or isclass(obj):\n  File "/usr/lib/python3.9/inspect.py", line 79, in isclass\n    return isinstance(object, type)\nRecursionError: maximum recursion depth exceeded while calling a Python object\n
Run Code Online (Sandbox Code Playgroud)\n

像这样定义 StdlibPerson 并不能解决问题:

\n
@dataclass\nclass StdlibPerson\n    name: str\n    age: int\n    parent: "Person" = None\n
Run Code Online (Sandbox Code Playgroud)\n

也不使用 pydantic 文档提供的第二种方式:

\n
@pydantic.dataclasses.dataclass\nclass PydanticPerson(StdlibPerson)\n    ...\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x9d\x8c 直接使用 Pydantic 数据类

\n
from __future__ import annotations\nfrom pydantic.dataclasses import dataclass\nfrom typing import Optional\n\n@pydantic.dataclasses.dataclass\nclass PydanticDataclassPerson:\n     name: str\n     age: int\n     parent: Optional[PydanticDataclassPerson] = None\n\njohn = PydanticDataclassPerson(name="John", age=49, parent=None)\n\nTraceback (most recent call last):\n  File "<stdin>", line 1, in <module>\n  File "<string>", line 6, in __init__\n  File "pydantic/dataclasses.py", line 97, in pydantic.dataclasses._generate_pydantic_post_init._pydantic_post_init\n    # | False |       |       |\n  File "pydantic/main.py", line 1040, in pydantic.main.validate_model\n  File "pydantic/fields.py", line 699, in pydantic.fields.ModelField.validate\npydantic.errors.ConfigError: field "parent" not yet prepared so type is still a ForwardRef, you might need to call PydanticDataclassPerson.update_forward_refs().\n\n>>> PydanticDataclassPerson.update_forward_refs()\nTraceback (most recent call last):\n  File "<stdin>", line 1, in <module>\nAttributeError: type object \'PydanticDataclassPerson\' has no attribute \'update_forward_refs\'\n\n
Run Code Online (Sandbox Code Playgroud)\n

问题

\n

如何使用自引用对象定义 pydantic 模型,以便它与 SQLAlchemy 命令式映射兼容?

\n

Lio*_*yon 4

似乎没有简单的解决方案来使用 FastAPI、自引用对象和 SQLAlchemy 命令式映射构建 REST API。

我决定切换到 FastAPI / GraphQL 堆栈,并使用FastAPI 文档中明确推荐的Strawberry 库

到目前为止没问题,Strawberry 可以轻松构建 GraphQL 服务器,并且可以轻松处理自引用对象。

#!/usr/bin/env python3.10
# src/my_app/entrypoints/api/schema.py

import typing
import strawberry

@strawberry.type
class Person:
    name: str
    age: int
    parent: Person | None
Run Code Online (Sandbox Code Playgroud)