Jos*_*ron 5 python-3.x graphql graphene-python graphql-mutation
我碰巧向 Graphql API(Python3 + Graphene)发送了 2 个单独的请求,以便:
我意识到这可能不符合 Graphql 的“精神”,所以我搜索并阅读了有关嵌套迁移的内容。不幸的是,我还发现这是不好的做法,因为嵌套迁移不是连续的,并且可能会导致客户端由于竞争条件而难以调试问题。
我正在尝试使用顺序根突变来实现考虑嵌套迁移的用例。请允许我向您展示我想象的一个用例和一个简单的解决方案(但可能不是一个好的实践)。很抱歉发了这么长的帖子。
让我们想象一下,我有用户和组实体,我希望从客户端表单更新组,不仅能够添加用户,而且还能够创建一个要添加到组中的用户(如果该用户不存在)。用户的 id 名为 uid(用户 id)和组 gid(组 id),只是为了突出区别。因此,使用根突变,我想象执行如下查询:
mutation {
createUser(uid: "b53a20f1b81b439", username: "new user", password: "secret"){
uid
username
}
updateGroup(gid: "group id", userIds: ["b53a20f1b81b439", ...]){
gid
name
}
}
Run Code Online (Sandbox Code Playgroud)
您注意到我在突变的输入中提供了用户 ID createUser。我的问题是,要进行updateGroup更改,我需要新创建用户的 ID。我不知道如何在 mutate 方法 resolving 内的石墨烯中获取该信息updateGroup,因此我想象在加载客户端表单数据时从 API 查询 UUID。因此,在发送上面的突变之前,在我的客户端初始加载时,我会执行以下操作:
query {
uuid
group (gid: "group id") {
gid
name
}
}
Run Code Online (Sandbox Code Playgroud)
然后,我将在突变请求中使用此查询响应中的 uuid(该值将为b53a20f1b81b439,如上面的第一个 scriptlet 中所示)。
您对这个过程有何看法?有更好的方法吗?Pythonuuid.uuid4实现这个安全吗?
提前致谢。
- - - 编辑
根据评论中的讨论,我应该提到上面的用例仅供说明之用。事实上,用户实体可能有一个内在的唯一键(电子邮件、用户名),其他实体也可能有(书籍的 ISBN...)。我正在寻找一种通用的案例解决方案,包括可能不会表现出这种自然唯一键的实体。
在最初的问题下的评论中有很多建议。我将在本提案结束时回过头来讨论一些问题。
我一直在思考这个问题,并且事实上这似乎是开发人员中反复出现的问题。我得出的结论是,我们可能会错过编辑图形的方式中的一些东西,即边缘操作。我认为我们尝试用节点操作来进行边操作。为了说明这一点,使用 dot (Graphviz) 等语言创建的图形可能如下所示:
digraph D {
/* Nodes */
A
B
C
/* Edges */
A -> B
A -> C
A -> D
}
Run Code Online (Sandbox Code Playgroud)
按照这种模式,也许问题中的 graphql 突变应该如下所示:
mutation {
# Nodes
n1: createUser(username: "new user", password: "secret"){
uid
username
}
n2: updateGroup(gid: "group id"){
gid
name
}
# Edges
addUserToGroup(user: "n1", group: "n2"){
status
}
}
Run Code Online (Sandbox Code Playgroud)
“边缘操作” 的输入addUserToGroup将是突变查询中先前节点的别名。
这还允许通过权限检查来装饰边缘操作(创建关系的权限可能与每个对象的权限不同)。
我们绝对可以解决这样的查询。不太确定的是后端框架,特别是 Graphene-python,是否提供了允许实现的机制addUserToGroup(在解析上下文中具有先前的突变结果)。我正在考虑dict在石墨烯上下文中注入之前的结果。如果成功的话,我将尝试用技术细节来完成答案。
也许已经存在实现类似目标的方法,我也会寻找并完成答案(如果找到)。
如果事实证明上述模式不可行或发现不好的做法,我想我会坚持两个单独的突变。
我测试了一种解决上述查询的方法,使用Graphene-python 中间件和基本突变类来处理共享结果。我在 Github 上创建了一个单文件 python 程序来测试这一点。或者在 Repl 上玩它。
中间件非常简单,并添加一个字典作为kwarg解析器的参数:
digraph D {
/* Nodes */
A
B
C
/* Edges */
A -> B
A -> C
A -> D
}
Run Code Online (Sandbox Code Playgroud)
基类也非常简单,管理字典中结果的插入:
mutation {
# Nodes
n1: createUser(username: "new user", password: "secret"){
uid
username
}
n2: updateGroup(gid: "group id"){
gid
name
}
# Edges
addUserToGroup(user: "n1", group: "n2"){
status
}
}
Run Code Online (Sandbox Code Playgroud)
需要遵守共享结果模式的类似节点的突变将继承 in 而SharedResultMutation不是Mutation并覆盖mutate_and_share_result而不是mutate:
class ShareResultMiddleware:
shared_results = {}
def resolve(self, next, root, info, **args):
return next(root, info, shared_results=self.shared_results, **args)
Run Code Online (Sandbox Code Playgroud)
类似边缘的突变需要访问字典shared_results,因此它们mutate直接覆盖:
class SharedResultMutation(graphene.Mutation):
@classmethod
def mutate(cls, root: None, info: graphene.ResolveInfo, shared_results: dict, *args, **kwargs):
result = cls.mutate_and_share_result(root, info, *args, **kwargs)
if root is None:
node = info.path[0]
shared_results[node] = result
return result
@staticmethod
def mutate_and_share_result(*_, **__):
return SharedResultMutation() # override
Run Code Online (Sandbox Code Playgroud)
基本上就是这样(其余的是常见的石墨烯样板和测试模型)。我们现在可以执行如下查询:
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
n1: upsertParent(data: $parent) {
pk
name
}
n2: upsertChild(data: $child1) {
pk
name
}
n3: upsertChild(data: $child2) {
pk
name
}
e1: setParent(parent: "n1", child: "n2") { ok }
e2: setParent(parent: "n1", child: "n3") { ok }
e3: addSibling(node1: "n2", node2: "n3") { ok }
}
Run Code Online (Sandbox Code Playgroud)
问题在于,类边突变参数不满足GraphQL 所提倡的类型意识:本着 GraphQL 精神,应该键入,node1而不是像本实现中那样。编辑添加了对类边突变输入节点的基本类型检查。node2graphene.Field(ChildType)graphene.String()
为了进行比较,我还实现了一种嵌套模式,其中仅解析创建(这是我们无法在先前查询中获得数据的唯一情况),这是Github 上提供的单文件程序。
它是经典的石墨烯,除了UpsertChild我们添加字段来解决嵌套创建及其解析器的突变:
class UpsertParent(SharedResultMutation, ParentType):
class Arguments:
data = ParentInput()
@staticmethod
def mutate_and_share_result(root: None, info: graphene.ResolveInfo, data: ParentInput, *___, **____):
return UpsertParent(id=1, name="test") # <-- example
Run Code Online (Sandbox Code Playgroud)
因此,与节点+边模式相比,额外内容的数量很小。我们现在可以执行如下查询:
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
n1: upsertChild(data: $child1) {
pk
name
siblings { pk name }
parent: createParent(data: $parent) { pk name }
newSibling: createSibling(data: $child2) { pk name }
}
}
Run Code Online (Sandbox Code Playgroud)
然而,我们可以看到,与节点+边缘模式可能的情况相反,(shared_result_mutation.py)我们无法在同一突变中设置新同级的父级。明显的原因是我们没有它的数据(特别是它的 pk)。另一个原因是嵌套突变的顺序不能得到保证。因此,例如,无法创建一个无数据突变assignParentToSiblings来设置当前根子节点的所有兄弟节点的父节点,因为嵌套兄弟节点可能是在嵌套父节点之前创建的。
但在某些实际情况下,我们只需要创建一个新对象,然后将其链接到现有对象。嵌套可以满足这些用例。
问题的评论中建议使用嵌套数据进行突变。这实际上是我第一次实现该功能,出于安全考虑我放弃了它。权限检查使用装饰器,看起来像(我真的没有 Book 突变):
class AddSibling(SharedResultMutation):
class Arguments:
node1 = graphene.String(required=True)
node2 = graphene.String(required=True)
ok = graphene.Boolean()
@staticmethod
def mutate(root: None, info: graphene.ResolveInfo, shared_results: dict, node1: str, node2: str): # ISSUE: this breaks type awareness
node1_ : ChildType = shared_results.get(node1)
node2_ : ChildType = shared_results.get(node2)
# do stuff
return AddSibling(ok=True)
Run Code Online (Sandbox Code Playgroud)
我认为我不应该在另一个地方进行此检查,例如在另一个具有嵌套数据的突变中。另外,在另一个突变中调用此方法需要在突变模块之间进行导入,我认为这不是一个好主意。我真的认为解决方案应该依赖于 GraphQL 解析功能,这就是我研究嵌套突变的原因,这让我首先提出了这篇文章的问题。
另外,我对问题中的 uuid 想法进行了更多测试(使用单元测试 Tescase)。事实证明,快速连续调用 python uuid.uuid4 可能会发生冲突,所以我放弃了这个选项。
因此,我创建了graphene-chain-mutation Python 包来与Graphene-python配合使用,并允许在同一查询中引用类边缘突变中的类节点突变的结果。我将粘贴下面的用法部分:
5 个步骤(有关可执行示例,请参阅test/fake.py 模块)。
pip install graphene-chain-mutation
Run Code Online (Sandbox Code Playgroud)
ShareResult graphene.Muation import graphene
from graphene_chain_mutation import ShareResult
from .types import ParentType, ParentInput, ChildType, ChildInput
class CreateParent(ShareResult, graphene.Mutation, ParentType):
class Arguments:
data = ParentInput()
@staticmethod
def mutate(_: None, __: graphene.ResolveInfo,
data: ParentInput = None) -> 'CreateParent':
return CreateParent(**data.__dict__)
class CreateChild(ShareResult, graphene.Mutation, ChildType):
class Arguments:
data = ChildInput()
@staticmethod
def mutate(_: None, __: graphene.ResolveInfo,
data: ChildInput = None) -> 'CreateChild':
return CreateChild(**data.__dict__)
Run Code Online (Sandbox Code Playgroud)
ParentChildEdgeMutationSiblingEdgeMutationset_link import graphene
from graphene_chain_mutation import ParentChildEdgeMutation, SiblingEdgeMutation
from .types import ParentType, ChildType
from .fake_models import FakeChildDB
class SetParent(ParentChildEdgeMutation):
parent_type = ParentType
child_type = ChildType
@classmethod
def set_link(cls, parent: ParentType, child: ChildType):
FakeChildDB[child.pk].parent = parent.pk
class AddSibling(SiblingEdgeMutation):
node1_type = ChildType
node2_type = ChildType
@classmethod
def set_link(cls, node1: ChildType, node2: ChildType):
FakeChildDB[node1.pk].siblings.append(node2.pk)
FakeChildDB[node2.pk].siblings.append(node1.pk)
Run Code Online (Sandbox Code Playgroud)
class Query(graphene.ObjectType):
parent = graphene.Field(ParentType, pk=graphene.Int())
parents = graphene.List(ParentType)
child = graphene.Field(ChildType, pk=graphene.Int())
children = graphene.List(ChildType)
class Mutation(graphene.ObjectType):
create_parent = CreateParent.Field()
create_child = CreateChild.Field()
set_parent = SetParent.Field()
add_sibling = AddSibling.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Run Code Online (Sandbox Code Playgroud)
ShareResultMiddleware执行查询时指定中间件: result = schema.execute(
GRAPHQL_MUTATION
,variables = VARIABLES
,middleware=[ShareResultMiddleware()]
)
Run Code Online (Sandbox Code Playgroud)
现在GRAPHQL_MUTATION可以是一个查询,其中类边突变引用类节点突变的结果:
GRAPHQL_MUTATION = """
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
n1: upsertParent(data: $parent) {
pk
name
}
n2: upsertChild(data: $child1) {
pk
name
}
n3: upsertChild(data: $child2) {
pk
name
}
e1: setParent(parent: "n1", child: "n2") { ok }
e2: setParent(parent: "n1", child: "n3") { ok }
e3: addSibling(node1: "n2", node2: "n3") { ok }
}
"""
VARIABLES = dict(
parent = dict(
name = "Emilie"
)
,child1 = dict(
name = "John"
)
,child2 = dict(
name = "Julie"
)
)
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
6406 次 |
| 最近记录: |