Python 官方文档:入门教程 => 点击学习
目录pydantic-resolve 解决嵌套数据结构的生成和其他方案的比较和GraphQL相比和 ORM 的 relationship相比LoaderDepend的用途 背景解决方
pydantic-resolve
openapi.JSON
和工具自动生成client让前后端无缝对接的做法,在前后端一体的架构中维护这些查询语句,属于重复劳动。resolve
一下就实现。loader filters
的支持,在一些业务逻辑下可以简化很多代码。如果把Dataloader 的 keys 等价视为 relationship的 join on 条件的话, 那么 loader_filters
就类似在别处的其他过滤条件。结论:
GraphQL更适合 public API。
对前后端作为一个整体的项目,RESTful + Pydantic-resolve 才是快速灵活提供数据结构的最佳方法。
.option(subquery(Model.field))
之类的代码loader_filters
参数,可以提供额外的全局过滤条件。结论
relationship 方案的灵活度低,不方便修改,默认的用法会产生外键约束。对迭代频繁的项目不友好。
Pydantic-resolve 和 ORM 层完全解耦,可以通过灵活创建Dataloader 来满足各种需要。
如果你使用过dataloader, 不论是js还是python的,都会遇到一个问题,如何为单独的一个请求创建独立的dataloader?
以 Python 的 strawberry
来举例子:
@strawberry.type
class User:
id: strawberry.ID
async def load_users(keys) -> List[User]:
return [User(id=key) for key in keys]
loader = DataLoader(load_fn=load_users)
@strawberry.type
class Query:
@strawberry.field
async def get_user(self, id: strawberry.ID) -> User:
return await loader.load(id)
schema = strawberry.Schema(query=Query)
如果单独实例化的话,会导致所有的请求都使用同一个dataloader, 由于loader本身是有缓存优化机制的,所以即使内容更新之后,依然会返回缓存的历史数据。
因此 strawberry
的处理方式是:
@strawberry.type
class User:
id: strawberry.ID
async def load_users(keys) -> List[User]:
return [User(id=key) for key in keys]
class MyGraphQL(GraphQL):
async def get_context(
self, request: UNIOn[Request, websocket], response: Optional[Response]
) -> Any:
return {"user_loader": DataLoader(load_fn=load_users)}
@strawberry.type
class Query:
@strawberry.field
async def get_user(self, info: Info, id: strawberry.ID) -> User:
return await info.context["user_loader"].load(id)
schema = strawberry.Schema(query=Query)
app = MyGraphQL(schema)
开发者需要在get_context
中去初始化loader, 然后框架会负责在每次request的时候会执行初始化。 这样每个请求就会有独立的loader, 解决了多次请求被缓存的问题。
其中的原理是:contextvars 在 await 的时候会做一次浅拷贝,所以外层的context可以被内部读到,因此手动在最外层(request的时候) 初始化一个引用类型(dict)之后,那么在 request 内部自然就能获取到引用类型内的loader。
这个方法虽然好,但存在两个问题:
get_context
, 每当新增了一个 DataLoader, 就需要去里面添加, 而且实际执行 .load
的地方也要从context 里面取loader。而 graphene
就更加任性了,把loader 的活交给了 aiodataloader, 如果翻阅文档的话,会发现处理的思路也是类似的,只是需要手动去维护创建过程。
我所期望的功能是:
其实这两件事情说的是同一个问题,就是如何把初始化的事情依赖反转到 resolve_field 方法中。
具体转化为代码:
class CommentSchema(BaseModel):
id: int
task_id: int
content: str
feedbacks: List[FeedbackSchema] = []
def resolve_feedbacks(self, loader=LoaderDepend(FeedbackLoader)):
return loader.load(self.id)
class TaskSchema(BaseModel):
id: int
name: str
comments: List[CommentSchema] = []
def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
return loader.load(self.id)
就是说,我只要这样申明好loader,其他的事情就一律不用操心。那么,这做得到么?
得益于pydantic-resolve
存在一个手动执行resolve
的过程,于是有一个思路:
tasks: list[TaskSchema]
有n个,我希望在第一次遇到的时候把loader 初始化并缓存,后续其他都使用缓存的loader。总体就是一个lazy的路子,到实际执行的时候去处理初始化流程。
下图中 1 会执行LoaderA 初始化,2,3则是读取缓存, 1.1 会执行LoaderB初始化,2.1,3.1 读取缓存
代码如下:
class Resolver:
def __init__(self):
self.ctx = contextvars.ContextVar('pydantic_resolve_internal_context', default={})
def exec_method(self, method):
signature = inspect.signature(method)
params = {}
for k, v in signature.parameters.items():
if isinstance(v.default, Depends):
cache_key = str(v.default.dependency.__name__)
cache = self.ctx.get()
hit = cache.get(cache_key, None)
if hit:
instance = hit
else:
instance = v.default.dependency()
cache[cache_key] = instance
self.ctx.set(cache)
params[k] = instance
return method(**params)
有些DataLoader的实现可能需要一个外部的查询条件, 比如查询用户的absense信息的时候,除了user_key 之外,还需要额外提供其他全局filter 比如sprint_id)。 这种全局变量从load参数走会显得非常啰嗦。
这种时候就依然需要借助contextvars 在外部设置变量。 以一段项目代码为例:
async def get_team_users_load(team_id: int, sprint_id: Optional[int], session: AsyncSession):
ctx.team_id_context.set(team_id) # set global filter
ctx.sprint_id_context.set(sprint_id) # set global filter
res = await session.execute(select(User)
.join(UserTeam, UserTeam.user_id == User.id)
.filter(UserTeam.team_id == team_id))
db_users = res.Scalars()
users = [schema.UserLoadUser(id=u.id, employee_id=u.employee_id, name=u.name)
for u in db_users]
results = await Resolver().resolve(users) # resolve
return results
class AbsenseLoader(DataLoader):
async def batch_load_fn(self, user_keys):
async with async_session() as session, session.begin():
sprint_id = ctx.sprint_id_context.get() # read global filter
sprint_stmt = Sprint.status == SprintStatusEnum.onGoing if not sprint_id else Sprint.id == sprint_id
res = await session.execute(select(SprintAbsence)
.join(Sprint, Sprint.id == SprintAbsence.sprint_id)
.join(User, User.id == SprintAbsence.user_id)
.filter(sprint_stmt)
.filter(SprintAbsence.user_id.in_(user_keys)))
rows = res.scalars().all()
dct = {}
for row in rows:
dct[row.user_id] = row.hours
return [dct.get(k, 0) for k in user_keys]
期望的设置方式为:
loader_filters = {
AbsenseLoader: {'sprint_id': 10},
OtherLoader: {field: 'value_x'}
}
results = await Resolver(loader_filters=loader_filters).resolve(users)
如果需要filter但是却没有设置, 该情况下要抛异常
以上就是pydantic-resolve嵌套数据结构生成LoaderDepend管理contextvars的详细内容,更多关于LoaderDepend管理contextvars的资料请关注编程网其它相关文章!
--结束END--
本文标题: pydantic-resolve嵌套数据结构生成LoaderDepend管理contextvars
本文链接: https://lsjlt.com/news/204642.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0