在最初的问题下,cmets 中有许多建议。我会在这个提议的最后再谈一些。
我一直在思考这个问题,以及它似乎是开发人员中反复出现的问题。我得出的结论是,我们可能会在想要编辑图形的方式上遗漏一些东西,即边缘操作。我认为我们尝试使用节点操作进行边缘操作。为了说明这一点,使用 dot (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 的机制(在解析上下文中具有先前的突变)。我正在考虑在石墨烯上下文中注入先前结果的dict。如果成功,我将尝试用技术细节完成答案。
也许已经有方法可以实现这样的目标,如果找到,我也会寻找并完成答案。
如果事实证明上述模式是不可能的或发现不好的做法,我想我会坚持使用 2 个单独的突变。
编辑 1:分享结果
我测试了一种解决上述查询的方法,使用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 dict,所以它们直接覆盖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)
基本上就是这样(其余的是常见的石墨烯样板和测试模拟)。我们现在可以执行如下查询:
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.
编辑 2:嵌套创作
为了比较,我还实现了一个嵌套模式,其中只解析创建(这是我们无法在之前的查询中获得数据的唯一情况)one-file program available on Github。
它是经典的 Graphene,除了突变 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 子节点的所有兄弟节点的父节点,因为嵌套的兄弟节点可能在嵌套的父节点之前创建。
在一些实际情况下,我们只需要创建一个新对象并
然后将其链接到现有对象。嵌套可以满足这些用例。
问题的 cmets 中建议使用 嵌套数据 进行突变。这实际上是我第一次实现该功能,出于安全考虑我放弃了它。权限检查使用装饰器,看起来像(我真的没有 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 解析能力,这就是我研究嵌套突变的原因,这让我首先提出了这篇文章的问题。
此外,我从问题中对 uuid 的想法进行了更多测试(使用 unittest Tescase)。事实证明,python uuid.uuid4 的快速连续调用可能会发生冲突,所以这个选项被我丢弃了。