Графен Graphql - как цепочки мутаций - PullRequest
0 голосов
/ 21 апреля 2020

Я случайно отправил 2 отдельных запроса в API Graphql (Python3 + Графен), чтобы:

  1. Создать объект
  2. Обновить другой объект, чтобы он имел отношение на созданный.

Я чувствовал, что это может быть не в духе Graphql, поэтому я искал и читал о вложенных миграциях . К сожалению, я также обнаружил, что это плохая практика , потому что вложенные миграции не являются последовательными, и это может привести к трудностям при отладке клиентов из-за условий гонки.

Я пытаюсь использовать последовательные root мутации для реализации случаев использования, когда рассматривались вложенные миграции. Позвольте мне представить вам пример использования и простое решение (но, вероятно, не очень хорошая практика), которое я себе представил. Извините за длинное сообщение.

Давайте представим, что у меня есть сущности Пользователь и Группа, и я хочу из формы клиента обновить группу, чтобы иметь возможность не только добавлять пользователя, но и создавать пользователя быть добавленным в группу, если пользователь не существует. У пользователей есть идентификаторы с именем uid (идентификатор пользователя) и группы gid (идентификатор группы), чтобы подчеркнуть разницу. Поэтому, используя мутации root, я представляю себе запрос, подобный следующему:

mutation {
    createUser(uid: "b53a20f1b81b439", username: "new user", password: "secret"){
        uid
        username
    }

    updateGroup(gid: "group id", userIds: ["b53a20f1b81b439", ...]){
        gid
        name
    }
}

Вы заметили, что я предоставляю идентификатор пользователя при вводе мутации createUser. Моя проблема в том, что для создания мутации updateGroup мне нужен идентификатор вновь созданного пользователя. Я не знаю, как получить это в графене внутри методов преобразования, разрешающих updateGroup, поэтому я представлял себе запрос UUID из API при загрузке данных клиентской формы. Поэтому перед отправкой мутации выше, при начальной загрузке моего клиента, я бы сделал что-то вроде:

query {
    uuid

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

Затем я бы использовал uuid из ответа этого запроса в запросе на мутацию (значение будет быть b53a20f1b81b439, как в первом скриплете выше).

Что вы думаете об этом процессе? Есть ли лучший способ сделать это? Python uuid.uuid4 безопасно ли это реализовать?

Заранее спасибо.

----- РЕДАКТИРОВАТЬ

Основываясь на обсуждении в комментариях, я должен Отметим, что приведенный выше вариант использования приведен только для иллюстрации. Действительно, сущность User может иметь уникальный ключ intrinsi c (электронная почта, имя пользователя), а также другие сущности (ISBN для Book ...). Я ищу общее решение для случая, в том числе для сущностей, которые могут не показывать такие естественные уникальные ключи.

Ответы [ 2 ]

0 голосов
/ 26 апреля 2020

Итак, я создал пакет графеновая цепочка Python для работы с Графен- python и позволил ссылаться на результаты узловых мутаций в ребре подобные мутации в том же запросе. Я просто вставлю раздел об использовании ниже:

5 шагов (см. Исполняемый пример модуля test / fake.py ).

  1. Установите пакет (требуется графен )
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"
    )
)
0 голосов
/ 22 апреля 2020

В комментариях к первому вопросу было несколько предложений. Я вернусь к некоторым из них в конце этого предложения.

Я думал об этой проблеме, а также о том, что она кажется постоянным вопросом среди разработчиков. Я пришел к выводу, что мы можем что-то упустить в том смысле, в каком мы хотим редактировать наш график, а именно операции с краями. Я думаю, что мы пытаемся делать ребра с помощью узловых операций. Чтобы проиллюстрировать это, создание графика на таком языке, как точка (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 предыдущих результатов в контексте графена. В случае успеха я постараюсь дополнить ответ техническими подробностями.

Может быть, уже есть способ достичь чего-то подобного, я также поищу это и дополню ответ, если найден.

Если выяснится, что описанная выше схема невозможна или найдена плохая практика, я думаю, что я буду придерживаться двух отдельных мутаций.


РЕДАКТИРОВАТЬ 1: обмен результатами

Я проверил способ разрешения запроса, как описано выше, используя Graphene- python middleware и базовый класс мутаций для совместного использования результатов. Я создал однофайловую python программу, доступную на Github , чтобы проверить это. Или поиграйте с ним в 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)

И вот в принципе и все (остальное - это обычные графеновые шаблоны и тестовые макеты). Теперь мы можем выполнить запрос, подобный следующему:

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 }
}

Проблема заключается в том, что аргументы в виде реберной мутации не удовлетворяют осведомленности о типе 1060 *, поддерживаемой GraphQL: в духе GraphQL, node1 и node2 должны быть набраны graphene.Field(ChildType) вместо graphene.String(), как в этой реализации. РЕДАКТИРОВАТЬ Добавлена ​​базовая проверка c типов для узлов ввода, подобных краевым мутациям .


РЕДАКТИРОВАТЬ 2: вложенные создания

Для сравнения я также реализовал шаблон вложенности, в котором разрешаются только создания (это единственный случай, когда у нас не может быть данных в предыдущем запросе), однофайловая программа, доступная на Github .

Это класс c Графен, кроме мутации 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

Итак, количество дополнительные вещи малы по сравнению с узлом узел + ребро. Теперь мы можем выполнить запрос вроде:

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 }
    }
}

Однако мы можем видеть, что в отличие от того, что было возможно с шаблоном node + edge, (shared_result_mutation.py) мы не можем установить родителя нового брат в той же мутации. Очевидная причина в том, что у нас нет его данных (в частности, его pk). Другая причина в том, что порядок для вложенных мутаций не гарантирован. Поэтому не может создать, например, мутацию без данных assignParentToSiblings, которая установила бы родителя всех братьев и сестер текущего root дочернего элемента, поскольку вложенный брат может быть создан перед вложенным родителем.

В некоторых практических случаях нам просто нужно создать новый объект и затем связать его с существующим объектом. Вложенность может удовлетворить эти варианты использования.


В комментариях к вопросу предлагалось использовать вложенные данные для мутаций. На самом деле это была моя первая реализация этой функции, и я отказался от нее из-за соображений безопасности. Проверки разрешений используют декораторы и выглядят так (у меня нет мутаций в Книге):

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 Идея из вопроса (с юнит-тестом Tescase). Оказывается, что быстрые последовательные вызовы python uuid.uuid4 могут конфликтовать, поэтому эта опция мне не нужна.

...