石墨烯 Graphql - 如何链接突变

Jos*_*ron 5 python-3.x graphql graphene-python graphql-mutation

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

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

Jos*_*ron 6

在最初的问题下的评论中有很多建议。我将在本提案结束时回过头来讨论一些问题。

我一直在思考这个问题,并且事实上这似乎是开发人员中反复出现的问题。我得出的结论是,我们可能会错过编辑图形的方式中的一些东西,即边缘操作。我认为我们尝试用节点操作来进行边操作。为了说明这一点,使用 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在石墨烯上下文中注入之前的结果。如果成功的话,我将尝试用技术细节来完成答案。

也许已经存在实现类似目标的方法,我也会寻找并完成答案(如果找到)。

如果事实证明上述模式不可行或发现不好的做法,我想我会坚持两个单独的突变。


编辑1:共享结果

我测试了一种解决上述查询的方法,使用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()


编辑2:嵌套创作

为了进行比较,我还实现了一种嵌套模式,其中仅解析创建(这是我们无法在先前查询中获得数据的唯一情况),这是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 可能会发生冲突,所以我放弃了这个选项。


Jos*_*ron 3

因此,我创建了graphene-chain-mutation Python 包来与Graphene-python配合使用,并允许在同一查询中引用类边缘突变中的类节点突变的结果。我将粘贴下面的用法部分:

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

  1. 安装包(需要石墨烯
pip install graphene-chain-mutation
Run Code Online (Sandbox Code Playgroud)
  1. 通过继承before来编写类似节点的突变: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)
  1. 通过继承(对于 FK 关系)或(对于 m2m 关系)来创建类似边缘的突变。指定其输入节点的类型并实现方法: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)
  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)
Run Code Online (Sandbox Code Playgroud)
  1. 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)