【问题标题】:Graphene Graphql - how to chain mutationsGraphene Graphql - 如何链式突变
【发布时间】:2020-08-03 23:58:27
【问题描述】:

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

  1. 创建一个对象
  2. 更新另一个对象,使其与创建的对象相关。

我感觉这可能不在 Graphql 的“精神”中,所以我搜索并阅读了有关 nested migrations 的信息。不幸的是,我还发现它是 bad practice,因为嵌套迁移不是顺序的,并且可能会由于竞争条件导致客户端难以调试问题。

我正在尝试使用顺序根突变来实现考虑嵌套迁移的用例。请允许我向您展示我想象的一个用例和一个简单的解决方案(但可能不是好的做法)。很抱歉来了这么长的帖子。

让我们想象一下,我有用户和组实体,我希望从客户端表单更新组,不仅可以添加用户,还可以创建要添加到组中的用户,如果用户这样做不存在。用户有名为 uid (user id) 和 groups gid (groupd id) 的 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 的变异方法中的石墨烯中获得它,所以我想象在加载客户端表单数据时从 API 查询 UUID。因此,在发送上述突变之前,在我的客户端初始加载时,我会执行以下操作:

query {
    uuid

    group (gid: "group id") {
        gid
        name
    }
}

然后我将在突变请求中使用此查询的响应中的 uuid(该值将是 b53a20f1b81b439,如上面的第一个小脚本中所示)。

您如何看待这个过程?有更好的方法吗? Python uuid.uuid4 可以安全地实现这个吗?

提前致谢。

----- 编辑

根据 cmets 中的讨论,我应该提一下,上面的用例仅用于说明。实际上,用户实体可能具有固有的唯一键(电子邮件、用户名),其他实体也可能具有(ISBN for Book...)。我正在寻找一个通用案例解决方案,包括可能不显示此类自然唯一键的实体。

【问题讨论】:

  • 你有根突变,然后保证顺序...const updateBook = (book, authorId) => { 提示如何获取相同参数的 id(用于创建)
  • @xadm 你的意思是在前面的查询中获取 uuid 并在突变中使用它是可以的,因为保证了顺序我得到了预期的结果? (我主要关心的一个是知道使用以前从服务器查询的 uui 是否可以,另一个是知道 Graphene(后端)是否提供替代方案)
  • createUser(username="new user", password="secret"){.. ... updateGroup(gid="group id", username="new user", password="secret"){ ... 第二个解析器可以通过使用 (username, password) 查找插入的 id ... 您不需要公开/使用内部 (uid)根本......在这个地方
  • @xadm 确实有些数据具有内在标识符(用户有电子邮件,预订有 ISBN ......),但其他数据可能没有这样的功能唯一键。例如,在一个应用程序中,我有一个 Degree 实体,我认为它没有一个字段,期望一个 ID,它保证检索特定实例......也许我应该找到一个更好的用例 :)跨度>
  • 用于突变的嵌套数据?在插入/更新突变内插入...stackoverflow.com/a/61273760/6124657

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


【解决方案1】:

在最初的问题下,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 精神中,node1node2 应键入 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 的快速连续调用可能会发生冲突,所以这个选项被我丢弃了。

【讨论】:

  • @xadm 是第一个建议在上下文中注入突变结果的人。我从中获得灵感,想到了一个节点+边缘提案
【解决方案2】:

因此,我创建了graphene-chain-mutation Python package 以与Graphene-python 一起使用,并允许在同一个查询中引用类似节点突变的结果。我将在下面粘贴使用部分:

5 个步骤(有关可执行示例,请参阅test/fake.py module)。

  1. 安装包(需要graphene
pip install graphene-chain-mutation
  1. 通过继承ShareResult before graphene.Muation:
  2. 编写 node-like 突变
 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__)
  1. 通过继承 ParentChildEdgeMutation(对于 FK 关系)或 SiblingEdgeMutation(对于 m2m 关系)来创建 edge-like 突变。指定其输入节点的类型并实现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"
    )
)

【讨论】:

  • 仍然只用于根级突变(保证顺序),而不是更深/嵌套(不保留顺序)?
  • @xadm 是的,仅用于根突变。这个想法是只使用根突变,因为只有它们才能保证是连续的,但能够在另一个根突变中使用一个这样的根突变的结果来改变 “edges”,实体。 -编辑:最初的问题实际上是“如何链接突变”(实际上并不是关于嵌套)
  • @xadm 我不确定我是否正确理解了您的问题,所以我只是测试了在嵌套突变中引用根突变的结果(使用解析器)并且它有效。我更新了 GithubPypi 的自述文件。
  • 问题是关于非根,在两个兄弟嵌套突变之间 - 来自“不良做法”链接的用例
  • @xadm 我没有测试过,但我想你可以,条件是你使用解析器并让它们有shared_results 参数。但是,最初的问题仍然存在,嵌套操作不能保证顺序。需要在共享字典中添加嵌套操作的结果。自定义装饰器可以帮助实现自动化。
猜你喜欢
  • 2021-06-30
  • 2017-09-23
  • 2017-07-21
  • 2018-01-25
  • 2023-03-29
  • 2018-05-15
  • 2020-08-13
  • 2019-01-23
  • 2021-03-09
相关资源
最近更新 更多