跳转至

从 ER 模型构造期望数据结构

使用 pydantic-resolve 的推荐模式是,在 ER 模型的基础上,构建出期望的数据结构。

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

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

比如我有一个 Person 的数组,现在要为每个人附加请假信息 Absense,并计算总共请假了几天。

期望的结构

当前已有的 Entity 为 Person 和 Absense,Absense 可以使用 AbsenseLoader 根据 person_id 查询到.

erDiagram
    Person ||--o{ Absense : owns
    Person {
        int id
        string name
        string other_fields
    }
    Absense {
        int id
        int person_id
        date start_date
        date end_date
        string other_fields
    }
class Person(BaseModel):
    id: int
    name: str
    other_field: str

class Absense(BaseModel):
    id: int
    user_id: int
    start_date: date
    end_date: date
    other_fields: str

期望的结构需要在 Person 读取 id, name, 并且添加 absenses 和 total_duration 字段

在 Absense 中读取 start_date, end_date, 添加 duration 字段.

可以通过继承父类来获取字段, 并且添加所需的新字段

class AbsenseWithDuration(Absense):
    duration: float = 0

class PersonWithAbsneses(Person):
    absenses: List[AbsenseWithDuration] = []
    total_duration: float = 0

或者根据需要挑选字段, 拷贝所需字段, 添加 ensure_subset 来检查内容一致.

@ensure_subset(Absense)
class AbsenseWithDuration(BaseModel):
    start_date: date
    end_date: date
    duration: float = 0

@ensure_subset(Person)
class PersonWithDuration(BaseModel):
    id: int
    name: str
    absenses: List[AbsenseWithDuration] = []
    total_duration: float = 0

于是我们得到了期望的数据结构.

erDiagram
    Person ||--o{ Absense : owns
    Person {
        int id
        string name
        float total_duration
    }
    Absense {
        date start_date
        date end_date
        float duration
    }

获取数据, 计算数据

为 abnsese 添加 DataLoader, 来获取数据

定义 post_duration 来计算 duration, 定义 post_total_duration 来计算总和.

@ensure_subset(Absense)
class AbsenseWithDuration(BaseModel):
    start_date: date
    end_date: date

    duration: float = 0
    def post_duration(self):
        return calc_duration(self.end_date, self.start_date)

@ensure_subset(Person)
class PersonWithDuration(BaseModel):
    id: int
    name: str

    absenses: List[AbsenseWithDuration] = []
    def resolve_absenses(self, loader=LoaderDepend(AbsenseLoader)):
        return loader.load(self.id)

    total_duration: float = 0
    def post_total_duration(self):
        return sum([a.duration for a in self.absenses])

启动

当定义完整个结构后, 我们就能通过初始化 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 都是内部封装掉了的。