我碰巧向Graphql API(Python3 + Graphene)发送了2个单独的请求,以便:
我感觉到这可能不是Graphql的“精神”,因此我搜索并阅读了nested migrations。不幸的是,我还发现它是bad practice,因为嵌套迁移不是顺序的,并且可能由于竞争条件而导致客户端难以调试问题。
我正在尝试使用顺序的根突变,以实现考虑了嵌套迁移的用例。请允许我向您展示一个用例和一个我想象中的简单解决方案(但可能不是很好的实践)。很抱歉发布了很长的帖子。
让我们的图像具有用户和组实体,并且我希望从客户端表单更新组,以便不仅能够添加用户,而且能够创建要添加到组中的用户(如果有)不存在。用户具有名为uid的ID(用户ID)和组gid(分组ID),只是为了突出区别。因此,使用根突变,我想进行如下查询:
mutation {
createUser(uid: "b53a20f1b81b439", username: "new user", password: "secret"){
uid
username
}
updateGroup(gid: "group id", userIds: ["b53a20f1b81b439", ...]){
gid
name
}
}
您注意到,我在createUser
突变的输入中提供了用户ID。我的问题是要进行updateGroup
突变,我需要新创建的用户的ID。我不知道有一种方法可以在解析updateGroup
的mutate方法中的石墨烯中获取该信息,因此我想象了在加载客户端表单数据时从API查询UUID的方法。因此,在发送上述变体之前,在客户端的初始加载时,我会执行以下操作:
query {
uuid
group (gid: "group id") {
gid
name
}
}
然后,我将在变异请求中使用此查询的响应中的uuid(值将为b53a20f1b81b439
,如上面的第一个脚本中一样。)
您如何看待这个过程?有更好的方法吗? Python uuid.uuid4
可以安全地实现吗?
谢谢。
-----编辑
基于评论中的讨论,我应该提到上面的用例仅用于说明。实际上,用户实体可能具有固有的唯一键(电子邮件,用户名),而其他实体也可能具有(书的ISBN ...)。我正在寻找一种通用的解决方案,包括那些可能不会表现出这种自然唯一键的实体。
答案 0 :(得分:2)
在第一个问题下的评论中有很多建议。我将在提案末尾再谈一些。
我一直在考虑这个问题,也一直在思考开发人员中经常出现的问题。我得出的结论是,可能我们想编辑图形时错过了一些东西,即边操作。我认为我们尝试使用节点操作进行边缘操作。为了说明这一点,以点(Graphviz)之类的语言创建的图形可能如下所示:
digraph D {
/* Nodes */
A
B
C
/* Edges */
A -> B
A -> C
A -> D
}
按照这种模式,问题中的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
}
}
“边缘操作” addUserToGroup
的输入将是变异查询中先前节点的别名。
这也将允许使用权限检查来修饰边缘操作(创建关系的权限可能与每个对象的权限不同)。
我们绝对可以解决这样的查询。不太确定的是,后端框架(尤其是Graphene-python)是否提供允许实现addUserToGroup
的机制(在解决方案上下文中具有先前的变异结果)。我正在考虑在Graphene上下文中注入先前结果的dict
。如果成功,我将尝试用技术细节来完成答案。
也许已经存在实现类似目标的方法,我也会寻找并完成答案。
如果结果证明上述模式是不可能的或发现不良做法,我想我会坚持2个单独的突变。
我测试了一种解决上述查询的方法,使用了Graphene-python middleware和基本突变类来处理共享结果。我创建了一个one-file python program available on Github进行测试。 Or play with it on Repl。
中间件非常简单,并向解析器添加了一个dict作为kwarg
参数:
class ShareResultMiddleware:
shared_results = {}
def resolve(self, next, root, info, **args):
return next(root, info, shared_results=self.shared_results, **args)
基类也非常简单,可以管理结果在字典中的插入:
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
需要遵循共享结果模式的类似节点的突变将从SharedResultMutation
继承而代替Mutation
,并覆盖mutate_and_share_result
而不是mutate
:>
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
类似边缘的突变需要访问shared_results
字典,因此它们直接覆盖mutate
:
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)
基本上就是这样(其余就是常见的Graphene样板和测试模拟)。我们现在可以执行如下查询:
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 }
}
与此相关的问题是,类似边缘的变异参数不满足GraphQL促进的类型意识:按照GraphQL精神,node1
和node2
应该是键入graphene.Field(ChildType)
,而不是此实现中的graphene.String()
。 编辑 Added basic type checking for edge-like mutation input nodes。
为进行比较,我还实现了一个嵌套模式,其中仅解决了创建问题(这是唯一一种无法在先前查询中获得数据的情况)one-file program available on Github。
这是经典的石墨烯,除了突变UpsertChild
,我们添加了字段来解决嵌套创作和的解析器:
class UpsertChild(graphene.Mutation, ChildType):
class Arguments:
data = ChildInput()
create_parent = graphene.Field(ParentType, data=graphene.Argument(ParentInput))
create_sibling = graphene.Field(ParentType, data=graphene.Argument(lambda: ChildInput))
@staticmethod
def mutate(_: None, __: graphene.ResolveInfo, data: ChildInput):
return Child(
pk=data.pk
,name=data.name
,parent=FakeParentDB.get(data.parent)
,siblings=[FakeChildDB[pk] for pk in data.siblings or []]
) # <-- example
@staticmethod
def resolve_create_parent(child: Child, __: graphene.ResolveInfo, data: ParentInput):
parent = UpsertParent.mutate(None, __, data)
child.parent = parent.pk
return parent
@staticmethod
def resolve_create_sibling(node1: Child, __: graphene.ResolveInfo, data: 'ChildInput'):
node2 = UpsertChild.mutate(None, __, data)
node1.siblings.append(node2.pk)
node2.siblings.append(node1.pk)
return node2
因此,与节点+边缘模式相比,额外的 stuff 数量较少。我们现在可以执行如下查询:
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 }
}
}
但是,我们可以看到,与节点+边缘模式(shared_result_mutation.py)相比,我们无法将新同级的父级设置为同一突变。显而易见的原因是我们没有其数据(尤其是它的pk)。另一个原因是因为不能保证嵌套突变的顺序。因此,例如,不能创建无数据突变assignParentToSiblings
来设置当前 root 子级的所有同级父级,因为嵌套的同级级可能在嵌套父级之前创建。
在某些实际情况下,我们只需要创建一个新对象并 然后将其链接到现有对象。嵌套可以满足这些用例。
该问题的注释中建议使用嵌套数据进行突变。这实际上是我第一次使用该功能,出于安全考虑,我放弃了它。权限检查使用修饰符,看起来像(我实际上没有Book变体):
class UpsertBook(common.mutations.MutationMixin, graphene.Mutation, types.Book):
class Arguments:
data = types.BookInput()
@staticmethod
@authorize.grant(authorize.admin, authorize.owner, model=models.Book)
def mutate(_, info: ResolveInfo, data: types.BookInput) -> 'UpsertBook':
return UpsertBook(**data) # <-- example
我不认为我也应该在另一个地方进行此检查,例如在另一个嵌套数据突变的地方。另外,在另一个变异中调用此方法将需要在变异模块之间导入,我认为这不是一个好主意。我真的以为该解决方案应该依赖GraphQL解析功能,这就是为什么我研究嵌套变异的原因,这使我首先提出了这篇文章的问题。
此外,我通过问题(使用单元测试Tescase)对uuid想法进行了更多测试。事实证明,快速连续调用python uuid.uuid4可能会发生冲突,因此该选项被我放弃了。
答案 1 :(得分:1)
因此,我创建了graphene-chain-mutation Python package以与Graphene-python一起使用,并允许在同一查询中引用类似于边缘的突变中的类似于节点的突变的结果。我将粘贴以下用法部分:
5个步骤(有关可执行示例,请参见test/fake.py module)。
pip install graphene-chain-mutation
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__)
ParentChildEdgeMutation
(对于FK关系)或SiblingEdgeMutation
(对于m2m关系)set_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)
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)
ShareResultMiddleware
中间件: result = schema.execute(
GRAPHQL_MUTATION
,variables = VARIABLES
,middleware=[ShareResultMiddleware()]
)
现在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"
)
)