pydantic resolve解决嵌套数据结构生成痛点分析
作者:allmonday 发布时间:2022-06-05 02:16:36
案例
以论坛为例,有个接口返回帖子(posts)信息,然后呢,来了新需求,说需要显示帖子的 author 信息。
此时会有两种选择:
在 posts 的 query 中 join 查询 author 信息,在返回 post 中添加诸如 author_id, author_name 之类的字段。
{'post': 'v2ex', 'author_name': 'tangkikodo'}
根据 posts 的 ids , 单独查询 author 列表,然后把 author 对象循环添加到 post 对象中。
{'post':'v2ex', 'author': {'name': 'tangkikod'}}
方法 1 中,需要去修改 query, 还需要修改post的schema. 如果未来要加新字段,例如用户头像的话,会需要修改两处。
方法 2 需要手动做一次拼接。之后增减字段都是在 author 对象的范围内修改。
所以相对来说, 方法 2 在未来的可维护性会比较好。用嵌套对象的方式可以更好的扩展和维护。
方法2 的返回结构
[
{
"id": 1,
"post": "v2ex",
"author": {
"name": "tangkikodo",
"id": 1
}
},
{
"id": 2,
"post": "v3ex",
"author": {
"name": "tangkikodo2",
"id": 1
}
}
]
然而需求总是会变化,突然来了一个新的且奇怪的需求,要在 author 信息中添加数据,显示他最近浏览过的帖子。返回体变成了:
[
{
"id": 1,
"post": "v2ex",
"author": {
"name": "tangkikodo",
"recent_views": [
{
"id": 2,
"post": "v3ex"
},
{
"id": 3,
"post": "v4ex"
}
]
}
}
]
那这个时候该怎么弄呢?血压是不是有点上来了。
根据之前的方法 2, 通常的操作是在获取到authors信息后, 关联查找author的recent_posts, 拼接回authors, 再将 authors 拼接回posts。 流程类似层层查找再层层回拼。 伪代码类似:
# posts query
posts = query_all_posts()
# authors query
authors_ids = fetch_unique_author_id(posts)
authors = query_author(author_ids)
recent_view_posts = fetch_recent_review_posts(author_ids) # 新需求
recent_view_maps = calc_view_mapping(recent_view_posts) # 新需求
# authors attach
authors = [attach_posts(a, recent_view_maps) for a in authors]
author_map = calc_author_mapping(authors)
# posts attach
posts = [attach_author(p, author_map) for p in posts]
莫名的会联想到callback hell, 添加新的层级都会在代码中间部分切入。
反正想想就挺麻烦的对吧。要是哪天再嵌套一层呢? 代码改起来有点费劲, 如果你此时血压有点高,那请继续往下看。
那,有别的办法么? 这里有个小轮子也许能帮忙。
解决方法
祭出一个小轮子: allmonday/pydantic-resolve
以刚才的例子,要做的事情抽象成两部分:
定义 dataloader ,负责查询和group数据。前半部分是从数据库查询,后半部分是将数据转成 pydantic 对象后返回。 伪代码,看个大概意思就好。
class AuthorLoader(DataLoader):
async def batch_load_fn(self, author_ids):
async with async_session() as session:
# query authors
res = await session.execute(select(Author).where(Author.id.in_(author_ids)))
rows = res.scalars().all()
# transform into pydantic object
dct = defaultdict(dict)
for row in rows:
dct[row.author_id] = AuthorSchema.from_orm(row)
# order by author_id
return [dct.get(k, None) for k in author_ids]
class RecentViewPostLoader(DataLoader):
async def batch_load_fn(self, view_ids):
async with async_session() as session:
res = await session.execute(select(Post, PostVisit.visitor_id) # join 浏览中间表
.join(PostVist, PostVisit.post_id == Post.id)
.where(PostVisit.user_id.in_(view_ids)
.where(PostVisit.created_at < some_timestamp)))
rows = res.scalars().all()
dct = defaultdict(list)
for row in rows:
dct[row.visitor_id].append(PostSchema.from_orm(row)) # group 到 visitor
return [dct.get(k, []) for k in view_ids]
定义 schema, 并且注入依赖的 DataLoaders, LoaderDepend 会管理好loader 的异步上下文缓存。
class RecentPostSchema(BaseModel):
id: int
name: str
class Config:
orm_mode = True
class AuthorSchema(BaseModel):
id: int
name: str
img_url: str
recent_views: Tuple[RecentPostSchema, ...] = tuple()
def resolve_recent_views(self, loader=LoaderDepend(RecentViewPostLoader)):
return loader.load(self.id)
class Config:
orm_mode = True
class PostSchema(BaseModel):
id: int
author_id: int
name: str
author: Optional[AuthorSchema] = None
def resolve_author(self, loader=LoaderDepend(AuthorLoader)):
return loader.load(self.author_id)
class Config:
orm_mode = True
然后呢?
然后就没有了,接下来只要做个 post 的查询, 再简单地...resolve 一下,任务就完成了。
posts = (await session.execute(select(Post))).scalars().all()
posts = [PostSchema.from_orm(p) for p in posts]
results = await Resolver().resolve(posts)
在拆分了 loader 和 schema 之后,对数据地任意操作都很简单,添加任意新的schema 都不会破坏原有的代码。
完整的案例可以查看 6_sqlalchemy_loaderdepend_global_filter.py
如果之前使用过aiodataloader 的话会知道,开发需要手动维护loader在每个request 中的初始化过程 , 但在 pydantic-resolve
中你完全不用操心异步上下文的创建,不用维护DataLoader的实例化, 一切都在pydantic-resolve
的管理之中。
就完事了。如果必须说有啥缺点的话。。必须用 async await 可能算一个。
该项目已经在我司的生产环境中使用,并且保持了100%的测试覆盖率。 欢迎大家尝鲜体验,如果遇到问题欢迎发issue,我会尽快修复。
来源:https://juejin.cn/post/7218861498592149560


猜你喜欢
- 当你连接一个MySQL服务器时,你通常应该使用一个口令。口令不以明文在连接上传输。所有其它信息作为能被任何人读懂的文本被传输。如果你担心这个
- 1.使用npm进行初始化在本地创建项目的文件夹名称,如 node_test,并在该文件夹下进行黑窗口执行初始化命令 2. 安装 e
- 什么是树表查询?借助具有特殊性质的树数据结构进行关键字查找。本文所涉及到的特殊结构性质的树包括:二叉排序树。 平衡二叉树。使用上述树结构存储
- 在计算机普及的现代设计领域,文字的设计的工作很大一部分由计算机代替人脑完成了(很多平面设计软件中都有制作艺术汉字的引导,以及提供了数十上百种
- 公司一个项目需要上传图片,一开始同事将图片上传后结合当前主机拼成了一个绝对的URL(http://192.168.1.1:888/m/get
- 前言读取站点资料数据对站点数据进行插值,插值到规则网格上绘制EOF第一模态和第二模态的空间分布图绘制PC序列关于插值,这里主要提供了两个插值
- Oracle生成单据编号存储过程,在做订单类似的系统都可能会存在订单编号不重复,或是流水号按日,按年,按月进行重新编号。可以参考以下存储过程
- 目录项目地址所用到的技术开始编写爬虫项目地址https://github.com/aliyoge/fund_crawler_py所用到的技术
- 特地查看了下手册,关于php magic quotes,常见的几个设置如下,magic_quotes_gpc,magic_quo
- 一、演示效果b站:虎年烟花演示二、python代码import pygamefrom math import *from pygame.lo
- 读取十万多条文本写入SQLite类型数据库,由于文本中存在中文字符,插入到数据库没错,取出时一直是UnicodeDecodeError,导致
- 前言今天我要教大家的是 如何实现nonebot插件之ChatGpt注意,本文涉及异步爬虫,json文件读写等知识点准备1.获取开发者key获
- 1. linux下消息记录关于系统的各种消息一般都会记录在/var/log/messages文件中,有些主机在中默认情况下有可能没有启用,具
- Python注释python中单行注释采用 # 开头。python 中多行注释使用三个单引号(''')或三个双引号(
- 目录arrow模块的使用获取arrow对象时间形式转换获取数据修改时间总结Python中有很多时间和日期处理的库,有time、datetim
- scrapy框架之增量式爬虫一 、增量式爬虫什么时候使用增量式爬虫:增量式爬虫:需求 当我们浏览一些网站会发现,某些网站定时的会在原有的基础
- 引言列表、字典:可变序列,可以执行增删改排序等字典:无序的一、字典的创建#使用{}创建scores = {'张三':100
- 网上有很多提供在线按钮制作、文字标题制作、Logo制作服务的网站,它们可以非常方便了让大家轻松的获得效果出色的各类图标型的图片,下面就快来看
- 1.下载pyinstaller并解压(可以去官网下载最新版):https://github.com/pyinstaller/pyinstal
- 1. 引言在Python中我们经常使用pip来安装第三方Python软件包,其实我们每个人都可以免费地将自己写的Python包发布到PyPI