Préambule
Tout d'abord, cet article est conçu pour les lecteurs qui connaissent déjà GraphQL et plus sur les subtilités et les nuances de son utilisation. J'espÚre néanmoins qu'il sera utile aux débutants.
GraphQL est un excellent outil. Je pense que beaucoup de gens connaissent et comprennent déjà ses avantages. Cependant, il y a quelques nuances à prendre en compte lors de la création de vos API basées sur GraphQL.
Par exemple, GraphQL vous permet de retourner au consommateur (utilisateur ou programme) en demandant les donnĂ©es uniquement la partie qui l'intĂ©resse. NĂ©anmoins, lors de la construction d'un serveur, il est assez facile de se tromper, ce qui conduit au fait qu'Ă l'intĂ©rieur du serveur (qui peut ĂȘtre, entre autres, distribuĂ©), les donnĂ©es s'exĂ©cuteront en bundles complets. Cela est principalement dĂ» au fait que GraphQL lui-mĂȘme ne fournit pas d'outils pratiques pour analyser une requĂȘte entrante et que les interfaces qui y sont posĂ©es ne sont pas bien documentĂ©es.
Source du problĂšme
Regardons un exemple typique d'une implĂ©mentation non optimale (ouvrez l'image dans une fenĂȘtre sĂ©parĂ©e si elle est mal lue):

Supposons que notre consommateur soit une certaine application ou composante du "rĂ©pertoire tĂ©lĂ©phonique" qui ne demande Ă notre API que l'identifiant, le nom et le numĂ©ro de tĂ©lĂ©phone des utilisateurs stockĂ©s par nous. Dans le mĂȘme temps, notre API est beaucoup plus Ă©tendue, elle permettra d'accĂ©der Ă d'autres donnĂ©es, telles que l'adresse physique de la rĂ©sidence et l'adresse e-mail des utilisateurs.
Au moment de l'Ă©change de donnĂ©es entre le consommateur et l'API, GraphQL fait parfaitement tout le travail dont nous avons besoin - seules les donnĂ©es demandĂ©es seront envoyĂ©es en rĂ©ponse Ă la demande. Le problĂšme dans ce cas est au point d'Ă©chantillonnage des donnĂ©es de la base de donnĂ©es - c'est-Ă -dire dans l'implĂ©mentation interne de notre serveur, et cela consiste dans le fait que pour chaque requĂȘte entrante nous sĂ©lectionnons toutes les donnĂ©es utilisateur de la base de donnĂ©es, malgrĂ© le fait que nous n'en ayons pas besoin. Cela gĂ©nĂšre une charge excessive sur la base de donnĂ©es et conduit Ă la circulation d'un trafic excessif au sein du systĂšme. Avec un nombre important de requĂȘtes, vous pouvez obtenir une optimisation significative en modifiant l'approche d'Ă©chantillonnage des donnĂ©es et en sĂ©lectionnant uniquement les champs qui ont Ă©tĂ© demandĂ©s. En mĂȘme temps, peu importe quelle est notre source de donnĂ©es - une base de donnĂ©es relationnelle, la technologie NoSQL ou un autre service (interne ou externe). Tout comportement non optimal peut ĂȘtre affectĂ© par n'importe quelle implĂ©mentation. MySQL dans ce cas est sĂ©lectionnĂ© simplement Ă titre d'exemple.
Solution
Il est possible d'optimiser ce comportement de serveur si nous analysons les arguments qui viennent Ă la fonction resolve()
:
async resolve(source, args, context, info) {
C'est le dernier argument, info
, qui nous intéresse particuliÚrement dans ce cas. Nous nous tournons vers la documentation et analysons en détail en quoi consistent la fonction resolve()
et l'argument qui nous intéresse:
type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any type GraphQLResolveInfo = { fieldName: string, fieldNodes: Array<Field>, returnType: GraphQLOutputType, parentType: GraphQLCompositeType, schema: GraphQLSchema, fragments: { [fragmentName: string]: FragmentDefinition }, rootValue: any, operation: OperationDefinition, variableValues: { [variableName: string]: any }, }
Ainsi, les trois premiers arguments transmis au résolveur sont la source
- les donnĂ©es transmises par le nĆud parent dans l'arborescence GraphQL du schĂ©ma, args
- les arguments de requĂȘte (qui proviennent de la requĂȘte) et le context
- l'objet de contexte d'exécution défini par le développeur, souvent appelé pour transmettre des données globales dans les «résolveurs». Et enfin, le quatriÚme argument est la méta-information sur la demande.
Que pouvons-nous extraire de GraphQLResolveInfo
pour résoudre notre problÚme?
Ses parties les plus intéressantes sont:
fieldName
est le nom de champ actuel de leur schéma GraphQL. C'est-à -dire il correspond au nom de champ spécifié dans le schéma pour ce résolveur. Si nous interceptons l'objet info
sur le champ users
, comme dans notre exemple ci-dessus, alors ce sont les "utilisateurs" qui seront contenus comme la valeur de fieldName
fieldNodes
- collection (tableau) de nĆuds qui ont Ă©tĂ© DEMANDĂS dans la requĂȘte. Exactement ce qui est nĂ©cessaire!fragments
- une collection de fragments de la demande (dans le cas oĂč la demande a Ă©tĂ© fragmentĂ©e). Informations importantes Ă©galement pour rĂ©cupĂ©rer les champs de donnĂ©es finaux.
Donc, comme solution, nous devons analyser l'outil d' info
et sĂ©lectionner la liste des champs qui nous sont venus de la requĂȘte, puis les transmettre Ă la requĂȘte SQL. Malheureusement, le package GraphQL de Facebook «prĂȘt Ă l'emploi» ne nous donne rien pour simplifier cette tĂąche. Dans l'ensemble, comme la pratique l'a montrĂ©, cette tĂąche n'est pas si simple, Ă©tant donnĂ© que les demandes peuvent ĂȘtre fragmentĂ©es. Et d'ailleurs, une telle analyse a une solution universelle, qui est ensuite simplement copiĂ©e de projet en projet.
J'ai donc dĂ©cidĂ© de l'Ă©crire en tant que bibliothĂšque open source sous licence ISC . Avec son aide, la solution pour analyser les champs de requĂȘte entrants est rĂ©solue tout simplement, par exemple, dans notre cas comme ceci:
const { fieldsList } = require('graphql-fields-list');
fieldsList(info)
dans ce cas fait tout le travail pour nous et retourne un tableau "plat" de champs enfants pour ce rĂ©solveur, c'est-Ă -dire notre requĂȘte SQL finale ressemblera Ă ceci:
SELECT id, name, phone FROM users;
Si nous modifions la demande entrante en:
query UserListQuery { users { id name phone email } }
alors la requĂȘte SQL se transformera en:
SELECT id, name, phone, email FROM users;
Cependant, il n'est pas toujours possible de faire avec un dĂ©fi aussi simple. Souvent, les applications rĂ©elles ont une structure beaucoup plus complexe. Dans certaines implĂ©mentations, nous pouvons avoir besoin de dĂ©crire le rĂ©solveur au niveau supĂ©rieur par rapport aux donnĂ©es dans le schĂ©ma GraphQL final. Par exemple, si nous dĂ©cidons d'utiliser la bibliothĂšque Relay , nous aimerions utiliser un mĂ©canisme prĂȘt Ă l'emploi pour diviser les collections d'objets de donnĂ©es en pages, ce qui conduit au fait que notre schĂ©ma GraphQL sera construit selon certaines rĂšgles. Par exemple, nous retravaillons notre schĂ©ma de cette façon (TypeScript):
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { connectionDefinitions, connectionArgs, nodeDefinitions, fromGlobalId, globalIdField, connectionFromArray, GraphQLResolveInfo, } from 'graphql-relay'; import { fieldsList } from 'graphql-fields-list'; export const { nodeInterface, nodeField } = nodeDefinitions(async (globalId: string) => { const { type, id } = fromGlobalId(globalId); let node: any = null; if (type === 'User') { node = await database.select(`SELECT id FROM user WHERE id="${id}"`); } return node; }); const User = new GraphQLObjectType({ name: 'User', interfaces: [nodeInterface], fields: { id: globalIdField('User', (user: any) => user.id), name: { type: GraphQLString }, email: { type: GraphQLString }, phone: { type: GraphQLString }, address: { type: GraphQLString }, } }); export const { connectionType: userConnection } = connectionDefinitions({ nodeType: User }); const Query = new GraphQLObjectType({ name: 'Query', fields: { node: nodeField, users: { type: userConnection, args: { ...connectionArgs }, async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) {
Dans ce cas, connectionDefinition
from Relay ajoutera des edges
, des node
, des informations de page et des nĆuds de cursor
au schĂ©ma, c'est-Ă -dire nous devrons maintenant reconstruire nos requĂȘtes diffĂ©remment (nous ne nous attarderons pas sur la pagination maintenant):
query UserListQuery { users { edges { node { id name phone email } } } }
Ainsi, resolve()
fonction implĂ©mentĂ©e sur le nĆud des users
devra maintenant dĂ©terminer quels champs sont demandĂ©s non pas pour lui-mĂȘme, mais pour son nĆud de node
enfant imbriqué, qui, comme nous le voyons, est relatif aux users
long du chemin edges.node
.
fieldsList
de la graphql-fields-list
aidera à résoudre ce problÚme, pour cela vous devez lui passer l'option de path
correspondante. Par exemple, voici l'implémentation dans notre cas:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node' }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
Dans le monde réel, il se peut également que dans le schéma GraphQL nous ayons défini un seul nom de champ, et dans le schéma de base de données, d'autres noms de champ leur correspondent. Par exemple, supposons que la table utilisateur de la base de données ait été définie différemment:
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) );
Dans ce cas, les champs de la requĂȘte GraphQL doivent ĂȘtre renommĂ©s avant d'ĂȘtre incorporĂ©s dans la requĂȘte SQL. fieldsList
y aidera si vous lui passez une mappe de traduction de nom dans l'option de transform
correspondante:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node', transform: { phone: 'phoneNumber', name: 'fullName' }, }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
Et pourtant, parfois, la conversion en un tableau plat de champs n'est pas suffisante (par exemple, si la source de données renvoie une structure complexe avec imbrication). Dans ce cas, la fonction fieldsMap
de la graphql-fields-list
viendra à la rescousse, qui retourne l'arborescence entiÚre des champs demandés en tant qu'objet:
const { fieldsMap } = require(`graphql-fields-list`);
Si nous supposons que l'utilisateur est décrit par une structure complexe, nous aurons tout. Cette méthode peut également prendre l'argument de path
facultatif, qui vous permet d'obtenir une carte de la branche nécessaire uniquement de l'arborescence entiÚre, par exemple:
const { fieldsMap } = require(`graphql-fields-list`);
La transformation des noms sur les cartes n'est actuellement pas prise en charge et reste à la merci du développeur.
Fragmentation de la demande
GraphQL prend en charge la fragmentation des requĂȘtes, par exemple, nous pouvons nous attendre Ă ce que le consommateur nous envoie une telle demande (ici, nous nous rĂ©fĂ©rons au schĂ©ma d'origine, un peu farfelu, mais syntaxiquement correct):
query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment }
Ne vous inquiétez pas dans ce cas, et fieldsList(info)
, et fieldsMap(info)
dans ce cas fieldsMap(info)
le résultat attendu, car ils prennent en compte la possibilité de fragmenter les demandes. Ainsi, fieldsList(info)
renverra ['id', 'name', 'phone', 'email', 'address']
, et fieldsMap(info)
, respectivement, renverra:
{ id: false, name: false, phone: false, email: false, address: false }
PS
J'espÚre que cet article a aidé à faire la lumiÚre sur certaines des nuances du travail avec GraphQL sur le serveur, et la bibliothÚque graphql-fields-list peut vous aider à créer des solutions optimales à l'avenir.
UPD 1
La version 1.1.0 de la bibliothÚque a été publiée - la prise en @include
@skip
et @include
dans les requĂȘtes a Ă©tĂ© ajoutĂ©e. Par dĂ©faut, l'option est activĂ©e, si nĂ©cessaire, dĂ©sactivez-la comme ceci:
fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false);