Arbeiten mit Daten beim Erstellen einer API auf Basis von GraphQL

Präambel


Dieser Artikel richtet sich zunächst an Leser, die bereits mit GraphQL vertraut sind und mehr über die Feinheiten und Nuancen der Arbeit mit GraphQL erfahren. Trotzdem hoffe ich, dass es für Anfänger nützlich sein wird.


GraphQL ist ein großartiges Werkzeug. Ich denke, viele Menschen kennen und verstehen die Vorteile bereits. Beim Erstellen Ihrer GraphQL-basierten APIs sind jedoch einige Nuancen zu beachten.


Mit GraphQL können Sie beispielsweise zum Verbraucher (Benutzer oder Programm) zurückkehren und nur die Daten anfordern, an denen dieser Verbraucher interessiert ist. Beim Erstellen eines Servers kann es jedoch leicht zu Fehlern kommen, was dazu führt, dass die Daten innerhalb des Servers (der unter anderem verteilt werden kann) in vollständigen "Bundles" ausgeführt werden. Dies ist in erster Linie auf die Tatsache zurückzuführen, dass GraphQL selbst keine praktischen Tools zum Parsen einer eingehenden Abfrage bietet und die darin verlegten Schnittstellen nicht gut dokumentiert sind.


Problemquelle


Schauen wir uns ein typisches Beispiel für eine nicht optimale Implementierung an (öffnen Sie das Bild in einem separaten Fenster, wenn es schlecht gelesen wird):


Bild


Angenommen, unser Verbraucher ist eine bestimmte Anwendung oder Komponente des "Telefonbuchs", die von unserer API nur die Kennung, den Namen und die Telefonnummer der von uns gespeicherten Benutzer abfragt. Gleichzeitig ist unsere API viel umfangreicher und ermöglicht den Zugriff auf andere Daten, wie z. B. die physische Adresse des Wohnsitzes und die E-Mail-Adresse der Benutzer.


Beim Datenaustausch zwischen dem Verbraucher und der API erledigt GraphQL alle erforderlichen Arbeiten perfekt - nur die angeforderten Daten werden als Antwort auf die Anforderung gesendet. Das Problem liegt in diesem Fall an der Stelle, an der Daten aus der Datenbank abgetastet werden - d. H. in der internen Implementierung unseres Servers, und es besteht in der Tatsache, dass wir für jede eingehende Anfrage alle Benutzerdaten aus der Datenbank auswählen, obwohl wir einige davon nicht benötigen. Dies führt zu einer übermäßigen Belastung der Datenbank und zu einer Zirkulation von übermäßigem Datenverkehr innerhalb des Systems. Bei einer erheblichen Anzahl von Abfragen können Sie eine erhebliche Optimierung erzielen, indem Sie den Ansatz für die Datenerfassung ändern und nur die Felder auswählen, die angefordert wurden. Gleichzeitig spielt es keine Rolle, was unsere Datenquelle ist - eine relationale Datenbank, NoSQL-Technologie oder ein anderer Dienst (intern oder extern). Jedes nicht optimale Verhalten kann durch jede Implementierung beeinflusst werden. In diesem Fall wird MySQL lediglich als Beispiel ausgewählt.


Lösung


Es ist möglich, dieses Serververhalten zu optimieren, wenn wir die Argumente analysieren, die zur Funktion resolve() :


 async resolve(source, args, context, info) { // ... } 

Es ist das letzte Argument, info , das in diesem Fall für uns von besonderem Interesse ist. Wir wenden uns der Dokumentation zu und analysieren im Detail, woraus die Funktion resolve() und das Argument bestehen, an dem wir interessiert sind:


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

Die ersten drei Argumente, die an den Resolver übergeben werden, sind source - die vom übergeordneten Knoten im GraphQL-Baum des Schemas übergebenen Daten, Argumente - die Anforderungsargumente (die aus der Abfrage stammen) und der context - das vom Entwickler definierte Ausführungskontextobjekt, das häufig zum Übertragen einiger globaler Daten aufgerufen wird in den "Resolvern". Und schließlich ist das vierte Argument die Metainformation über die Anfrage.


Was können wir aus GraphQLResolveInfo extrahieren, um unser Problem zu lösen?


Die interessantesten Teile sind:


  • fieldName ist der aktuelle Feldname des GraphQL-Schemas. Das heißt, Es entspricht dem Feldnamen, der im Schema für diesen Resolver angegeben ist. Wenn wir das info Objekt im users abfangen, wie in unserem obigen Beispiel, werden "Benutzer" als Wert von fieldName
  • fieldNodes - Sammlung (Array) von Knoten, die in der Abfrage angefordert wurden. Genau das, was benötigt wird!
  • fragments - Eine Sammlung von Fragmenten der Anforderung (falls die Anforderung fragmentiert war). Auch wichtige Informationen zum Abrufen der endgültigen Datenfelder.

Als Lösung müssen wir das info Tool analysieren, die Liste der Felder auswählen, die aus der Abfrage zu uns gekommen sind, und sie dann an die SQL-Abfrage übergeben. Leider gibt uns das GraphQL-Paket von Facebook "out of the box" nichts, um diese Aufgabe zu vereinfachen. Insgesamt ist diese Aufgabe, wie die Praxis gezeigt hat, angesichts der Tatsache, dass Anforderungen fragmentiert werden können, nicht so trivial. Außerdem hat eine solche Analyse eine universelle Lösung, die anschließend einfach von Projekt zu Projekt kopiert wird.


Also habe ich beschlossen, es als Open-Source-Bibliothek unter der ISC- Lizenz zu schreiben. Mit seiner Hilfe wird die Lösung zum Parsen eingehender Abfragefelder ganz einfach gelöst, zum Beispiel in unserem Fall wie folgt:


 const { fieldsList } = require('graphql-fields-list'); // ... async resolve(source, args, context, info) { const requestedFields = fieldsList(info); return await database.query(`SELECT ${requestedFields.join(',')} FROM users`) } 

fieldsList(info) in diesem Fall die gesamte Arbeit für uns und gibt ein "flaches" Array von untergeordneten Feldern für diesen Resolver zurück, d. h. Unsere letzte SQL-Abfrage sieht folgendermaßen aus:


 SELECT id, name, phone FROM users; 

Wenn wir die eingehende Anfrage ändern in:


 query UserListQuery { users { id name phone email } } 

dann wird die SQL-Abfrage zu:


 SELECT id, name, phone, email FROM users; 

Eine so einfache Herausforderung ist jedoch nicht immer möglich. Reale Anwendungen sind häufig viel komplexer aufgebaut. In einigen Implementierungen müssen wir möglicherweise den Resolver auf der oberen Ebene in Bezug auf die Daten im endgültigen GraphQL-Schema beschreiben. Wenn wir uns beispielsweise für die Verwendung der Relay- Bibliothek entschieden haben, möchten wir einen vorgefertigten Mechanismus zum Aufteilen von Sammlungen von Datenobjekten in Seiten verwenden, was dazu führt, dass unser GraphQL-Schema nach bestimmten Regeln erstellt wird. Zum Beispiel überarbeiten wir unser Schema folgendermaßen (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, ) { // TODO: implement }, }, }); export const schema = new GraphQLSchema({ query: Query }); 

In diesem Fall fügt die connectionDefinition von Relais dem Schema edges , node , pageInfo und pageInfo , d. H. Wir müssen unsere Abfragen jetzt anders neu erstellen (wir werden uns jetzt nicht mit der Paginierung befassen):


 query UserListQuery { users { edges { node { id name phone email } } } } 

resolve() auf dem users implementierte Funktion nun bestimmen, welche Felder nicht für sich selbst angefordert werden, sondern für den verschachtelten edges.node , der, wie wir sehen, relativ zu den users entlang des Pfades edges.node .


fieldsList aus der Bibliothek graphql-fields-list hilft ebenfalls bei der Lösung dieses Problems. fieldsList die entsprechende graphql-fields-list . Hier ist zum Beispiel die Implementierung in unserem Fall:


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

Auch in der realen Welt kann es sein, dass wir im GraphQL-Schema nur einen Feldnamen definiert haben und im Datenbankschema andere Feldnamen diesen entsprechen. Angenommen, die Benutzertabelle in der Datenbank wurde anders definiert:


 CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) ); 

In diesem Fall sollten die Felder aus der GraphQL-Abfrage umbenannt werden, bevor sie in die SQL-Abfrage eingebettet werden. fieldsList hilft dabei, wenn Sie eine fieldsList in der entsprechenden transform :


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

Manchmal reicht die Konvertierung in ein flaches Array von Feldern jedoch nicht aus (z. B. wenn die Datenquelle eine komplexe Struktur mit Verschachtelung zurückgibt). In diesem Fall fieldsMap Funktion fieldsMap aus der Bibliothek graphql-fields-list , die den gesamten Baum der angeforderten Felder als Objekt zurückgibt:


 const { fieldsMap } = require(`graphql-fields-list`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info); /* RESULT: { edges: { node: { id: false, name: false, phone: false, } } } */ } 

Wenn wir davon ausgehen, dass der Benutzer durch eine komplexe Struktur beschrieben wird, erhalten wir alles. Diese Methode kann auch das optionale Pfadargument verwenden, mit dem Sie eine Karte nur des erforderlichen Zweigs aus dem gesamten Baum abrufen können, zum Beispiel:


 const { fieldsMap } = require(`graphql-fields-list`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info, 'edges.node'); /* RESULT: { id: false, name: false, phone: false, } */ } 

Die Umwandlung von Namen auf Karten wird derzeit nicht unterstützt und bleibt dem Entwickler ausgeliefert.


Fragmentierung anfordern


GraphQL unterstützt die Fragmentierung von Abfragen. Wir können beispielsweise erwarten, dass der Verbraucher uns eine solche Anfrage sendet (hier verweisen wir auf das ursprüngliche Schema, das ein wenig weit hergeholt, aber syntaktisch korrekt ist):


 query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment } 

In diesem Fall sollten Sie sich keine Sorgen machen, und fieldsList(info) und fieldsMap(info) in diesem Fall das erwartete Ergebnis zurück, da sie die Möglichkeit der Fragmentierung von Anforderungen berücksichtigen. fieldsList(info) gibt also ['id', 'name', 'phone', 'email', 'address'] und fieldsMap(info) zurück:


 { id: false, name: false, phone: false, email: false, address: false } 

PS


Ich hoffe, dieser Artikel hat dazu beigetragen, einige der Nuancen der Arbeit mit GraphQL auf dem Server zu beleuchten, und die Bibliothek graphql-fields-list kann Ihnen helfen, in Zukunft optimale Lösungen zu erstellen.


UPD 1


Version 1.1.0 der Bibliothek wurde veröffentlicht - Unterstützung für die @skip und @include in Anfragen wurde hinzugefügt. Standardmäßig ist die Option aktiviert, falls erforderlich, deaktivieren Sie sie wie folgt:


 fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false); 

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


All Articles