石墨烯Graphql-如何链接突变

时间:2020-04-21 09:25:40

标签: python-3.x graphql graphene-python graphql-mutation

我碰巧向Graphql API(Python3 + Graphene)发送了2个单独的请求,以便:

  1. 创建对象
  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 ...)。我正在寻找一种通用的解决方案,包括那些可能不会表现出这种自然唯一键的实体。

2 个答案:

答案 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个单独的突变。


编辑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字典,因此它们直接覆盖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精神,node1node2应该是键入graphene.Field(ChildType),而不是此实现中的graphene.String()编辑 Added basic type checking for edge-like mutation input nodes


编辑2:嵌套创建

为进行比较,我还实现了一个嵌套模式,其中仅解决了创建问题(这是唯一一种无法在先前查询中获得数据的情况)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)。

  1. 安装软件包(需要graphene
pip install graphene-chain-mutation
    通过继承ShareResult 之前 graphene.Muation
  1. 编写节点样突变:
 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关系)
  1. 创建类似边缘的突变。指定其输入节点的类型并实现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)
  1. 照常创建模式
 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)
  1. 在执行查询时指定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"
    )
)