GraphQL Resolvers: Best Practices

Van graphql.org

Dit bericht is het eerste deel van een reeks best practices en opmerkingen die we hebben gemaakt bij het bouwen van GraphQL API's bij PayPal. In aankomende berichten zullen we onze mening delen over: schemaontwerp, foutafhandeling, productiezichtbaarheid, optimalisatie van client-side integraties en tooling voor teams.

Je hebt misschien ons vorige bericht 'GraphQL: een succesverhaal voor PayPal-afrekening' gezien over de reis van PayPal van REST naar GraphQL. Dit bericht gaat dieper in op enkele best practices voor het bouwen van resolvers die snel, testbaar en veerkrachtig zijn in de tijd.

Wat is een resolver?

Laten we beginnen met dezelfde basislijn. Wat is een resolver?

Resolverdefinitie
Elk veld op elk type wordt ondersteund door een functie die een resolver wordt genoemd.

Een resolver is een functie die een waarde voor een type of veld in een schema oplost. Resolvers kunnen objecten of scalars zoals tekenreeksen, getallen, Booleans, etc. retourneren. Als een object wordt geretourneerd, gaat de uitvoering door naar het volgende onderliggende veld. Als een scalair wordt geretourneerd (meestal op een bladknooppunt), is de uitvoering voltooid. Als null wordt geretourneerd, stopt de uitvoering en gaat deze niet verder.

Resolvers kunnen ook asynchroon zijn! Ze kunnen waarden uit een andere REST API, database, cache, constante, etc. oplossen.

Later zullen we een aantal voorbeelden doorlopen die illustreren hoe resolvers te bouwen die snel, testbaar en veerkrachtig zijn.

Query's uitvoeren

Om resolvers beter te begrijpen, moet u weten hoe query's worden uitgevoerd.

Elke GraphQL-query doorloopt drie fasen. Query's worden geparseerd, gevalideerd en uitgevoerd.

  1. Ontleden - Een zoekopdracht wordt ontleed in een abstracte syntaxisboom (of AST). AST's zijn ongelooflijk krachtig en achter tools zoals ESLint, babel, enz. Als je wilt zien hoe een GraphQL AST eruit ziet, kijk dan op astexplorer.net en verander JavaScript in GraphQL. U ziet links een zoekopdracht en rechts een AST.
  2. Valideren - De AST wordt gevalideerd volgens het schema. Controleert op de juiste syntaxis van de zoekopdracht en of de velden bestaan.
  3. Uitvoeren - De runtime loopt door de AST, beginnend bij de root van de boom, roept resolvers op, verzamelt resultaten en zendt JSON uit.

Voor dit voorbeeld verwijzen we naar deze zoekopdracht:

Vraag voor latere referentie

Wanneer deze zoekopdracht wordt geparseerd, wordt deze geconverteerd naar een AST of een boomstructuur.

Zoekopdracht weergegeven als een boom

Het rootquerytype is het toegangspunt tot de boom en bevat onze twee rootvelden, gebruiker en album. De gebruikers- en albumresolvers worden parallel uitgevoerd (wat typisch is voor alle runtimes). De boom wordt eerst in de breedte uitgevoerd, wat betekent dat de gebruiker moet worden opgelost voordat de onderliggende naam en e-mail worden uitgevoerd. Als de gebruikersresolver asynchroon is, wordt de gebruikerstak vertraagd totdat deze is opgelost. Zodra alle bladknooppunten, naam, e-mail, titel zijn opgelost, is de uitvoering voltooid.

Rootqueryvelden, zoals gebruiker en album, worden parallel uitgevoerd, maar in willekeurige volgorde. Velden worden doorgaans uitgevoerd in de volgorde waarin ze in de query worden weergegeven, maar het is niet veilig om aan te nemen dat. Omdat velden parallel worden uitgevoerd, wordt ervan uitgegaan dat ze atomair, idempotent en vrij van bijwerkingen zijn.

Resolvers nader bekeken

In de volgende paragrafen zullen we JavaScript gebruiken, maar GraphQL-servers kunnen in bijna elke taal worden geschreven.

Resolvers met vier argumenten - root, args, context, info

In een of andere vorm ontvangt elke resolver in elke taal deze vier argumenten:

  • root - Resultaat van het vorige / bovenliggende type
  • args - Argumenten verstrekt aan het veld
  • context - een veranderlijk object dat aan alle resolvers wordt verstrekt
  • info - Veldspecifieke informatie relevant voor de zoekopdracht (zelden gebruikt)

Deze vier argumenten zijn essentieel om te begrijpen hoe gegevens tussen resolvers stromen.

Standaard resolvers

Voordat we verdergaan, is het vermeldenswaard dat een GraphQL-server standaard resolvers heeft, zodat u niet voor elk veld een resolverfunctie hoeft op te geven. Een standaardresolver zoekt in de root naar een eigenschap met dezelfde naam als het veld. Een implementatie ziet er waarschijnlijk als volgt uit:

Standaard resolver-implementatie

Gegevens ophalen in resolvers

Waar moeten we gegevens ophalen? Wat zijn de voordelen van onze opties?

In de volgende paar voorbeelden zullen we teruggaan naar dit schema:

Een gebeurtenisveld heeft een verplicht id-argument, retourneert een gebeurtenis

Gegevens doorgeven tussen resolvers

context is een veranderlijk object dat aan alle resolvers wordt verstrekt. Het is gemaakt en vernietigd tussen elk verzoek. Het is een geweldige plek om algemene Auth-gegevens, gemeenschappelijke modellen / fetchers voor API's en databases, enz. Op te slaan. Bij PayPal zijn we een grote Node.js-winkel met infrastructuur gebouwd op Express, dus daar bewaren we de vraag van Express.

Wanneer u voor het eerst over context leert, is een eerste gedachte om context te gebruiken als cache voor algemene doeleinden. Dit wordt niet aanbevolen, maar hier is hoe een implementatie eruit zou kunnen zien.

Gegevens doorgeven tussen resolvers met behulp van context. Dit wordt niet aanbevolen!

Wanneer de titel wordt opgeroepen, slaan we het gebeurtenisresultaat in context op. Wanneer photoUrl wordt aangeroepen, halen we de gebeurtenis uit de context en gebruiken we deze. Deze code is niet betrouwbaar. Er is geen garantie dat de titel vóór photoUrl wordt uitgevoerd.

We kunnen beide resolvers repareren om te controleren of de gebeurtenis in context bestaat. Zo ja, gebruik het dan. Anders halen we het op en bewaren we het voor later, maar er is nog steeds een groot oppervlak voor fouten.

In plaats daarvan moeten we de context binnen resolvers vermijden. We moeten voorkomen dat kennis en zorgen met elkaar vermengen, zodat onze resolvers gemakkelijk te begrijpen, te debuggen en te testen zijn.

Gegevens doorgeven van ouder op kind

Het root-argument is voor het doorgeven van gegevens van bovenliggende resolvers aan onderliggende resolvers.

Als u bijvoorbeeld een gebeurtenistype bouwt waar alle velden van het evenement zijn
afhankelijk van dezelfde gegevens, wilt u deze misschien één keer ophalen in het gebeurtenisveld,
in plaats van op elk evenementgebied.

Lijkt me een goed idee, toch? Dit is een snelle manier om aan de slag te gaan met het bouwen van resolvers, maar u kunt problemen tegenkomen. Laten we begrijpen waarom.

Voor de onderstaande voorbeelden werken we met een gebeurtenistype dat twee velden heeft.

Type gebeurtenis met twee velden: titel en photoUrl

De meeste velden voor Evenement kunnen worden opgehaald uit een Evenement-API, dus we kunnen het ophalen bij de evenementresolver op het hoogste niveau en de resultaten leveren aan onze titel- en photoUrl-resolvers.

Top-level event resolver haalt gegevens op, geeft resultaten aan titel- en photoUrl-veldresolvers

Nog beter, we hoeven de onderste twee resolvers niet op te geven.
We kunnen de standaard resolvers gebruiken omdat het Object geretourneerd door getEvent ()
heeft een titel en een eigenschap photoUrl.

id en titel worden opgelost met standaardresolvers

Wat is hier mis mee?

Er zijn twee scenario's waarbij u te veel aan het ophalen bent ...

Scenario # 1: gegevens ophalen met meerdere lagen

Stel dat er een aantal vereisten zijn ingevoerd en dat u de deelnemers van een evenement moet weergeven. We beginnen met het toevoegen van een veld deelnemers aan Evenement.

Soort evenement met een extra veld deelnemers

Wanneer u de details van de deelnemers ophaalt, hebt u twee opties: haal die gegevens op bij de resolver van het evenement of de resolver van de deelnemers.

We zullen de eerste optie testen: deze toevoegen aan de gebeurtenisresolver.

evenementresolver roept twee API's op, ophalen evenementdetails en deelnemersgegevens

Als een client alleen naar titel en photoUrl vraagt, maar niet naar deelnemers. Nu bent u inefficiënt en doet u een onnodig verzoek aan uw API voor deelnemers.

Het is niet jouw schuld, zo werken we. We herkennen patronen en kopiëren ze.
Als bijdragers zien dat het ophalen van gegevens gebeurt in de resolver van de gebeurtenis, zullen ze waarschijnlijk
voeg daar extra gegevens op zonder er al te hard over na te denken.

We hebben nog een optie om te testen met het ophalen van de deelnemers in de resolver van de deelnemers.

deelnemers resolver haalt details van deelnemers uit de API van deelnemers

Als onze klant alleen naar aanwezigen vraagt, niet naar titel en photoUrl. We zijn nog steeds inefficiënt door een onnodig verzoek in te dienen bij onze evenementen-API.

Scenario # 2: N + 1 probleem

Omdat gegevens op veldniveau worden opgehaald, lopen we het risico te veel te worden opgehaald. Overhalen en het N + 1-probleem is een populair onderwerp in de GraphQL-wereld. Shopify heeft een geweldig artikel waarin N + 1 goed wordt uitgelegd.

Hoe beïnvloedt dat ons hier?

Om het beter te illustreren, voegen we een nieuw evenementenveld toe dat alle evenementen retourneert.

Een gebeurtenissenveld retourneert alle gebeurtenissen.Zoekopdracht voor alle evenementen met hun titel en deelnemers

Als een klant naar alle evenementen en hun deelnemers vraagt, lopen we het risico dat we te veel halen omdat bezoekers meer dan één evenement kunnen bijwonen. We kunnen dubbele verzoeken doen voor dezelfde deelnemer.

Dit probleem wordt versterkt in een grote organisatie waar aanvragen kunnen uitwaaieren en onnodige druk op uw systeem kunnen veroorzaken.

Om dit op te lossen, moeten we batches aanvragen en de-dupliceren!

In JavaScript zijn enkele populaire opties dataloader- en Apollo-gegevensbronnen.

Als u een andere taal gebruikt, is er waarschijnlijk iets dat u kunt leren. Dus kijk eens rond voordat je dit zelf oplost.

De kern hiervan is dat deze bibliotheken zich bovenop uw datatoegangslaag bevinden en uitgaande aanvragen in de cache plaatsen en de-dupliceren met behulp van debouncing of memoisatie. Als je nieuwsgierig bent naar hoe asynchrone memo eruit ziet, bekijk dan de uitstekende post van Daniel Brain!

Gegevens ophalen op veldniveau

Eerder zagen we dat het gemakkelijk is om te verbranden door te "ophalen" met "topzware" resellers van ouder op kind.

Is er een beter alternatief?

Laten we de optie ouder-kind opnieuw plagen. Wat als we dat ongedaan maken, zodat onze onderliggende velden verantwoordelijk zijn voor het ophalen van hun eigen gegevens?

Velden zijn zelf verantwoordelijk voor het ophalen van gegevens.
Waarom is dit een beter alternatief?

Over deze code is gemakkelijk te redeneren. U weet precies waar een e-mail wordt opgehaald. Dit zorgt voor eenvoudig debuggen.

Deze code is beter te testen. U hoeft de evenementresolver niet te testen terwijl u de titelresolver echt alleen maar wilde testen.

Voor sommigen lijkt de duplicatie van getEvent op een codegeur. Maar het hebben van code die eenvoudig, gemakkelijk te redeneren en meer testbaar is, is een beetje duplicatie waard.

Maar er is nog steeds een potentieel probleem hier. Als een klant naar titel en photoUrl vraagt, veroorzaken we een extra verzoek op onze Event API met getEvent. Zoals we eerder in het N + 1-probleem hebben gezien, moeten we verzoeken op een raamwerkniveau ontdubbelen met behulp van bibliotheken zoals dataloader en Apollo-gegevensbronnen.

Als we gegevens ophalen op veldniveau en dedupe-aanvragen, hebben we code die gemakkelijker te debuggen en testen is en kunnen we gegevens optimaal ophalen zonder erover na te denken.

Best practices

  • Het ophalen en doorgeven van gegevens van ouder op kind moet spaarzaam worden gebruikt.
  • Gebruik bibliotheken zoals dataloader om downstream-aanvragen te de-dupliceren.
  • Houd rekening met eventuele druk op uw gegevensbronnen.
  • Wijzig de "context" niet. Zorgt voor consistente, minder buggy-code.
  • Schrijf resolvers die leesbaar, onderhoudbaar en testbaar zijn. Niet zo slim.
  • Maak uw resolvers zo dun mogelijk. Extractielogica voor het ophalen van gegevens voor herbruikbare async-functies.

Blijf kijken!

Gedachten? We horen graag de best practices en ervaringen van uw team met het bouwen van resolvers. Dit is een onderwerp dat niet vaak wordt besproken maar belangrijk is voor het bouwen van langlevende GraphQL API's.

In aankomende berichten zullen we onze mening delen over: schemaontwerp, foutafhandeling, productiezichtbaarheid, optimalisatie van client-side integraties en tooling voor teams.

We zijn aan het inhuren! Als je wilt werken aan front-end infrastructuur, GraphQL of React bij PayPal, DM me op Twitter op @mark_stuart!