跳转至

先定义模型,再描述获取方式

使用 pydantic-resolve 的推荐方式是先定义模型。 将所有的操作都附加在定义好的数据对象上。

当我们构建数据时, 一个很朴素的思考方式是:先定义好目标数据结构 Target, 比较当前已有的数据源结构 Source ,寻找最短的变化路径。

更加详细的内容会在 "ERD 驱动开发" 中介绍。

比如我有一个 Person 的数组, 现在要为每个人增加请假信息, 数据库中已有的请假数据包含年假,病假两种, 要把两者合并成一个类型。

期望的结构

期望的数据结构就是 Person 和对应的 absenses, Absense 是面向业务的数据结构。

class Person(BaseModel):
    id: int
    name: str
    absenses: List[Absense]

class Absense(BaseModel):
    type: Literal['annual', 'sick']
    start_date: date
    end_date: date

添加数据源

AnnualLeave 和 SickLeave 是已有的数据类型。

分别由 AnnualLeaveLoader 和 SickLeaveLoader 提供具体数据。

class AnnualLeave(BaseModel):
    person_id: int
    start_date: date
    end_date: date

class SickLeave(BaseModel):
    person_id: int
    start_date: date
    end_date: date

此时我们可以把源数据和期望数据组合在一起, 列出好需要获取的数据和将要计算出来的结果。

这代表了起始数据和目标数据。

配合 exclude=True 可以在最终输出中隐藏临时变量。

@model_config()
class Person(BaseModel):
    id: int
    name: str

    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)

    absenses: List[Absense] = []

加载数据

然后我们为 annual_leavessick_leaves 添加 resolve 和 dataloader, 他们将为起始数据提供数据。

@model_config()
class Person(BaseModel):
    id: int
    name: str

    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
        return loader.load(self.id)

    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
        return loader.load(self.id)

    absense: List[Absense] = []

转换数据

annual_leavessick_leaves 的 resolve 阶段结束时, 他们已经有实际数据了, 接下来利用 post 阶段来计算 absenses

@model_config()
class Person(BaseModel):
    id: int
    name: str

    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
        return loader.load(self.id)

    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
        return loader.load(self.id)

    absenses: List[Absense] = []
    def post_absense(self):
        a = [Absense(
                type='annual',
                start_date=a.start_date,
                end_date=a.end_date) for a in self.annual_leaves]
        b = [Absense(
                type='sick',
                start_date=s.start_date,
                end_date=s.end_date) for a in self.sick_leaves]
        return a + b

启动整个过程

当定义完整个 Person, Absense 的结构后, 我们就能通过初始化 people 来加载和计算所有数据了,只需对 people 数据执行一次 Resolver().resolve() 就能完成所有的操作。

people = [Person(id=1, name='alice'), Person(id=2, name='bob')]
people = await Resolver().resolve(people)

在这整个拼装过程中, 我们没有书写一行循环展开 people 的代码,所有的相关代码都在 Person 对象的内部。

如果使用面向过程的方式来处理的话, 会需要至少考虑:

  • annual, sick leaves 根据 person_id 聚合,生成 person_annual_map 和 person_sick_map 两个新变量
  • for person in people 遍历 person 对象

这些过程在 pydantic-resolve 都是内部封装掉了的。