Récemment, GraphQL gagne de plus en plus en popularité. Syntaxe gracieuse des demandes, de la typification et des abonnements.
Il semble: "le voici - nous avons trouvé le langage parfait pour l'échange de données!" ...
Je développe en utilisant ce langage depuis plus d'un an, et je vais vous dire: tout est loin d'être si fluide. GraphQL a à la fois des moments inconfortables et des problèmes vraiment fondamentaux dans la conception du langage lui-même.
D'un autre côté, la plupart de ces «changements de conception» ont été effectués pour une raison - cela était dû à diverses considérations. En fait, GraphQL n'est pas pour tout le monde et n'est peut-être pas du tout l'outil dont vous avez besoin. Mais tout d'abord.
Je pense qu'il vaut la peine de faire une petite remarque quant à l'endroit où j'utilise cette langue. Il s'agit d'un panneau d'administration SPA assez compliqué, la plupart des opérations dans lesquelles sont des CRUD assez non triviaux (entités complexes). Une partie importante de l'argumentation de ce document est liée à la nature de la demande et à la nature des données traitées. Dans les applications d'un type différent (ou avec une nature différente des données), de tels problèmes peuvent ne pas se poser en principe.
1. NON_NULL
Ce n'est pas un problème sérieux. Il s'agit plutôt de toute une série d'inconvénients liés à l'organisation du travail avec nullable dans GraphQL.
Il existe des langages de programmation fonctionnels (et pas seulement), un tel paradigme est celui des monades. Donc, il y a une chose telle que la monade Maybe
(Haskel) ou Option
(Scala). En bout de ligne, la valeur contenue dans une telle monade peut ou non exister (c'est-à-dire être nulle). Eh bien, ou il peut être implémenté via enum, comme dans Rust.
D'une manière ou d'une autre, et dans la plupart des langues, cette valeur, qui "enveloppe" l'original, fait de null une option supplémentaire à la principale. Et syntaxiquement - c'est toujours un ajout au type principal. Ce n'est pas toujours juste une classe de type séparée - dans certaines langues, est-ce juste un ajout sous la forme d'un suffixe ou d'un préfixe ?
.
Dans GraqhQL, l'inverse est vrai. Tous les types sont annulables par défaut - et ce n'est pas seulement marquer le type comme annulable, c'est la monade Maybe
- Maybe
, au contraire.
Et si l'on considère la section d'introspection du champ de name
pour un tel schéma:
on trouve:

Type de String
NON_NULL
dans NON_NULL
1.1. SORTIE
Pourquoi En bref, il est lié à la conception de langage «tolérante» par défaut (entre autres, il est convivial pour l'architecture de microservices).
Pour comprendre l'essence de cette "tolérance", considérons un exemple légèrement plus complexe, dans lequel toutes les valeurs renvoyées sont strictement enveloppées dans NON_NULL:
type User { name: String!
Supposons que nous ayons un service qui renvoie une liste d'utilisateurs et un micro-service séparé "amitié" qui nous renvoie une correspondance pour les amis de l'utilisateur. Ensuite, en cas d'échec du service "amitié", nous ne pourrons plus lister les utilisateurs. Besoin de corriger la situation:
type User { name: String!
Il s'agit de la tolérance aux erreurs internes. Un exemple, bien sûr, tiré par les cheveux. Mais j'espère que vous avez saisi l'essence.
De plus, vous pouvez vous faciliter la vie dans d'autres situations. Supposons qu'il existe des utilisateurs distants et que les identifiants des amis puissent être stockés dans une structure externe indépendante. Nous pourrions simplement éliminer et renvoyer uniquement ce que nous avons, mais nous ne pourrons alors pas comprendre exactement ce qui a été éliminé.
type Query {
Tout va bien. Et quel est le problème?
En général, ce n'est pas un très gros problème - donc le goût. Mais si vous avez une application monolithique avec une base de données relationnelle, alors les erreurs les plus probables sont vraiment des erreurs, et l'api doit être aussi stricte que possible. Bonjour, points d'exclamation! Partout où vous le pouvez.
Je voudrais pouvoir "inverser" ce comportement et placer des points d'interrogation au lieu de points d'exclamation) Ce serait plus familier d'une manière ou d'une autre.
Mais en entrant, nullable est une histoire complètement différente. Il s'agit d'un montant du niveau de la case à cocher en HTML (je pense que tout le monde se souvient de cette non-évidence lorsque le champ d'une case à cocher non cochée n'est tout simplement pas envoyé à l'arrière).
Prenons un exemple:
type Post { id: ID! title: String!
Jusqu'à présent, tout va bien. Ajouter une mise à jour:
type Mutation { createPost(post: PostInput!): Post! updatePost(id: ID!, post: PostInput!): Post! }
Et maintenant, la question est: que pouvons-nous attendre du champ de description lors de la mise à jour du message? Le champ peut être nul, ou peut être complètement absent.
Si le champ est manquant, que faut-il faire? Ne le mettez pas à jour? Ou le mettre à null? L'essentiel est qu'autoriser null et permettre l'absence d'un champ sont deux choses différentes. Cependant, GraphQL est la même chose.
2. Séparation des entrées et des sorties
C'est juste de la douleur. Dans le modèle de travail CRUD, vous récupérez l'objet par l'arrière en le «tordant» et le renvoyez. En gros, c'est un seul et même objet. Mais il suffit de le décrire deux fois - pour l'entrée et la sortie. Et rien ne peut être fait avec cela, sauf pour écrire un générateur de code pour cette entreprise. Je préférerais séparer en "entrée et sortie" non pas les objets eux-mêmes, mais les champs de l'objet. Par exemple, les modificateurs:
type Post { input output text: String! output updatedAt(format: DateFormat = W3C): Date! }
ou en utilisant des directives:
type Post { text: String! @input @output updatedAt(format: DateFormat = W3C): Date! @output }
3. Polymorphisme
Les problèmes de séparation des types en entrée et en sortie ne se limitent pas à une double description. Dans le même temps, pour les types génériques, vous pouvez définir des interfaces généralisées:
interface Commentable { comments: [Comment!]! } type Post implements Commentable { text: String! comments: [Comment!]! } type Photo implements Commentable { src: URL! comments: [Comment!]! }
ou les syndicats
type Person { firstName: String, lastName: String, } type Organiation { title: String } union Subject = Organiation | Person type Account { login: String subject: Subject }
Vous ne pouvez pas faire de même pour les types d'entrée. Il existe un certain nombre de conditions préalables à cela, mais cela est en partie dû au fait que json est utilisé comme format de données pour le transport. Cependant, dans la sortie, le champ __typename
est utilisé pour spécifier le type. Pourquoi il était impossible de faire de même en entrant - n'est pas très clair. Il me semble que ce problème pourrait être résolu un peu plus élégamment en abandonnant json pendant le transport et en entrant dans son propre format. Quelque chose dans l'esprit:
union Subject = OrganiationInput | PersonInput input AccountInput { login: String! password: String! subject: Subject! }
Mais cela obligerait à écrire des analyseurs supplémentaires pour cette entreprise.
4. Génériques
Quel est le problème avec GraphQL avec des génériques? Et tout est simple - ils ne le sont pas. Prenons une requête d'index CRUD typique avec une pagination ou un curseur - ce n'est pas important. Je vais donner un exemple avec pagination.
input Pagination { page: UInt, perPage: UInt, } type Query { users(pagination: Pagination): PageOfUsers! } type PageOfUsers { total: UInt items: [User!]! }
et maintenant pour les organisations
type Query { organizations(pagination: Pagination): PageOfOrganizations! } type PageOfOrganizations { total: UInt items: [Organization!]! }
et ainsi de suite ... comment j'aimerais avoir des génériques pour ça
type PageOf<T> { total: UInt items: [T!]! }
alors j'écrirais
type Query { users(page: UInt, perPage: UInt): PageOf<User>! }
Oui des tonnes d'applications! Dois-je vous parler des génériques?
5. Espaces de noms
Ils ne sont pas là non plus. Lorsque le nombre de types dans le système est supérieur à cent et demi, la probabilité de collisions de noms tend à cent pour cent.
Et toutes sortes de Service_GuideNDriving_Standard_Model_Input
apparaissent. Je ne parle pas d'espaces de noms à part entière à différents points de terminaison, comme dans SOAP (oui, oui, c'est terrible, mais les espaces de noms y sont parfaitement créés). Et au moins plusieurs schémas sur un même point de terminaison avec la possibilité de «tâtonner» les types entre les schémas.
Total
GraphQL est un bon outil. Il s'intègre parfaitement dans une architecture de microservices tolérante, qui est tout d'abord orientée vers la sortie d'informations et la simple entrée déterministe.
Si vous avez des entités polymorphes à saisir, vous pouvez rencontrer des problèmes.
La séparation des types d'entrée et de sortie, ainsi que le manque de génériques - génèrent un tas de gribouillis à partir de zéro.
Graphql n'est pas vraiment (et parfois pas du tout ) sur CRUD.
Mais cela ne signifie pas que vous ne pouvez pas le manger :)
Dans le prochain article, je veux parler de la façon dont je me bats (et parfois avec succès) avec certains des problèmes décrits ci-dessus.