[Quel est le problème avec GraphQL] ... Et comment y faire face

Dans l' article précédent , nous avons examiné les points incommodes du système de type GraphQL.
Et maintenant, nous allons essayer de vaincre certains d'entre eux. Tous intéressés, s'il vous plaît, sous cat.


La numérotation des partitions correspond aux problèmes auxquels j'ai réussi à faire face.


1.2 ENTRÉE NON_NULL


À ce stade, nous avons examiné l'ambiguïté qui génère une fonctionnalité d'implémentation nullable dans GraphQL.


Et le problème est qu'il ne permet pas d'implémenter le concept de mise à jour partielle à partir de zéro - un analogue de la méthode HTTP PATCH dans l'architecture REST. Dans les commentaires sur le matériel passé, j'ai été fortement critiqué pour la pensée "REST". Je peux seulement dire que l'architecture CRUD m'y oblige. Et je n'étais pas prêt à renoncer aux avantages de REST, simplement parce que «ne fais pas ça». Oui, et une solution à ce problème a été trouvée.


Et donc, revenons au problème. Comme nous le savons tous, le script CRUD lors de la mise à jour de l'enregistrement ressemble à ceci:


  1. J'ai un disque à l'arrière.
  2. Champs d'enregistrement modifiés.
  3. Envoyé un dossier à l'arrière.

Le concept de mise à jour partielle, dans ce cas, devrait nous permettre de renvoyer uniquement les champs qui ont été modifiés.
Donc, si nous définissons un modèle d'entrée de cette façon


 input ExampleInput { foo: String! bar: String } 

puis lors du mappage d'une variable de type ExampleInput avec cette valeur


 { "foo": "bla-bla-bla" } 

sur un DTO avec cette structure:


 ExampleDTO { foo: String #   bar: ?String #   } 

nous obtenons un objet DTO avec cette valeur:


 { foo: "bla-bla-bla", bar: null } 

et lors du mappage d'une variable avec cette valeur


 { "foo": "bla-bla-bla", "bar": null } 

on obtient un objet DTO avec la même valeur que la dernière fois:


 { foo: "bla-bla-bla", bar: null } 

C'est-à-dire que l'entropie se produit - nous perdons des informations sur la transmission ou non du champ par le client.
Dans ce cas, il n'est pas clair ce qui doit être fait avec le champ de l'objet final: ne le touchez pas parce que le client n'a pas passé le champ, ou ne le définissez pas parce que le client a passé null .


À strictement parler, GraphQL est un protocole RPC. Et j'ai commencé à réfléchir à la façon dont je fais ces choses dans le dos et aux procédures à suivre pour faire exactement ce que je veux. Et sur le backend, je fais une mise à jour partielle des champs comme celui-ci:


 $repository->find(42)->setFoo('bla-bla-lba'); 

C'est-à-dire que je ne touche littéralement pas à l'auteur d'une propriété d'entité, sauf si je dois changer la valeur de cette propriété. Si vous le déplacez vers le schéma GraphQL, vous obtenez le résultat suivant:


 type Mutation { entityRepository: EntityManager! } type EntityManager { update(id: ID!): PersitedEntity } type PersitedEntity { setFoo(foo: String!): String! setBar(foo: String): String } 

Maintenant, si nous voulons, nous pouvons appeler la méthode setBar et définir sa valeur sur null, ou ne pas toucher à cette méthode, puis la valeur ne sera pas modifiée. Ainsi, une belle implémentation de partial update sort. Pas pire que PATCH du fameux REST.


Dans les commentaires sur le matériel passé, Summerwind a demandé: pourquoi avons-nous besoin d'une partial update ? Je réponds: il y a de TRÈS grands champs.

3. Polymorphisme


Il arrive souvent que vous deviez vous soumettre aux entités d'entrée qui sont en quelque sorte "une seule et même chose" mais pas tout à fait. J'utiliserai l'exemple de création d'un compte à partir de documents antérieurs.


 #   AccountInput { login: "Acme", password: "***", subject: OrganiationInput { title: "Acme Inc" } } 

 #    AccountInput { login: "Acme", password: "***", subject: PersonInput { firstName: "Vasya", lastName: "Pupkin", } } 

De toute évidence, nous ne pouvons pas soumettre des données avec une telle structure pour un argument - GraphQL ne nous permettra tout simplement pas de le faire. Donc, vous devez en quelque sorte résoudre ce problème.


Méthode 0 - front


La première chose qui me vient à l'esprit est la séparation de la partie variable de l'entrée:


 input AccountInput { login: String! password: Password! subjectOrganization: OrganiationInput subjectPerson: PersonInput } 

Hmm ... quand je vois un tel code, je me souviens souvent de Joséphine Pavlovna. Ça ne me convient pas.


Méthode 1 - pas sur le front, mais sur le front
Ensuite, le fait m'est venu à l'esprit que pour identifier les entités, j'utilise J'utilise UUID (je le recommande généralement à tout le monde - cela aidera plus d'une fois). Et cela signifie que je peux créer des entités valides directement sur le client, les lier ensemble par identifiant et les envoyer au back-end, séparément.


Ensuite, nous pouvons faire quelque chose dans l'esprit de:


 input AccountInput { login: String! password: Password! subject: SubjectSelectInput! } input SubjectSelectInput { id: ID! } type Mutation { createAccount( organization: OrganizationInput, person: PersonInput, account: AccountInput! ): Account! } 

ou, ce qui s'est avéré encore plus pratique (pourquoi c'est plus pratique, je vous dirai quand nous arriverons à la génération des interfaces utilisateurs), divisez-le en différentes méthodes:


 type Mutation { createAccount(account: AccountInput!): Account! createOrganization(organization: OrganizationInput!): Organization! createPerson(person: PersonInput!) : Person! } 

Ensuite, nous devrons envoyer une demande pour createAccount et createOrganization / createPerson
un lot. Il convient de noter que le traitement du lot doit alors être encapsulé dans une transaction.


Méthode 2 - Le scalaire magique
L'astuce est que le scalaire dans GraphQL n'est pas seulement Int , Sting , Float , etc. C'est généralement n'importe quoi (enfin, alors que JSON peut gérer cela, bien sûr).
Ensuite, nous pouvons simplement déclarer un scalaire:


 scalar SubjectInput 

Ensuite, écrivez votre gestionnaire dessus, et ce n'est pas cuit à la vapeur. Ensuite, nous pouvons facilement glisser des champs variables à l'entrée.


Quelle façon choisir? J'utilise les deux et j'ai développé une règle pour moi:
Si l'entité parent est une racine agrégée pour l'enfant, je choisis la deuxième méthode, sinon la première.


4. Génériques.


Tout est trivial ici et je n'ai rien trouvé de mieux que la génération de code. Et sans Rails (paquet railt / sdl) je n'aurais pas pu (ou plutôt, j'aurais fait la même chose avec des béquilles). L'astuce est que Rail vous permet de définir des directives au niveau du document (il n'y a pas une telle position pour les directives dans la feuille de spécifications).


 directive @example on DOCUMENT 

Autrement dit, les directives ne sont attachées à rien d'autre que le document dans lequel elles sont appelées.


J'ai présenté les directives suivantes:


 directive @defineMacro(name: String!, template: String!) on DOCUMENT directive @macro(name: String!, arguments: [String]) on DOCUMENT 

Je pense que personne n'a besoin d'expliquer l'essence des macros ...


C'est tout pour l'instant. Je ne pense pas que ce matériau causera autant de bruit que par le passé. Tout de même, le titre y était assez "jaune")


Dans les commentaires de l'article précédent, les habitants de Khabrovsk se sont noyés pour avoir partagé l'accès ... donc le prochain article portera sur l'autorisation.

Source: https://habr.com/ru/post/fr435690/


All Articles