GraphQL Resolvers: Best Practices

Fra graphql.org

Dette innlegget er den første delen av en serie med god praksis og observasjoner vi har gjort mens vi bygde GraphQL APIer på PayPal. I kommende innlegg vil vi dele tankene våre om: skjemdesign, feilhåndtering, produksjonssynlighet, optimalisering av klientsiden-integrasjoner og verktøy for team.

Du har kanskje sett forrige innlegg "GraphQL: En suksesshistorie for PayPal Checkout" om PayPal reise fra REST til GraphQL. Dette innlegget utdyper detaljer om beste fremgangsmåter for å bygge oppløsere som er raske, testbare og spenstige over tid.

Hva er en resolver?

La oss starte med samme grunnlinje. Hva er en resolver?

Resolver definisjon
Hvert felt på hver type er støttet av en funksjon som kalles en resolver.

En resolver er en funksjon som løser en verdi for en type eller felt i et skjema. Løsere kan returnere objekter eller skalarer som Strenger, Numbers, Booleans, etc. Hvis et objekt returneres, fortsetter kjøringen til neste barnefelt. Hvis en skalar returneres (vanligvis ved en bladknute), fullføres utførelsen. Hvis null returneres, stopper kjøringen og fortsetter ikke.

Løsere kan også være asynkrone! De kan løse verdier fra en annen REST API, database, cache, konstant osv.

Senere vil vi gå gjennom en serie eksempler som illustrerer hvordan man bygger oppløsere som er raske, testbare og spenstige.

Utfører spørsmål

For å forstå oppløsere bedre, må du vite hvordan spørsmål blir utført.

Hver GraphQL-spørring går gjennom tre faser. Spørringer blir analysert, validert og utført.

  1. Analyse - En spørring blir analysert i et abstrakt syntaks-tre (eller AST). AST er utrolig kraftige og bak verktøy som ESLint, babel, etc. Hvis du vil se hvordan en GraphQL AST ser ut, sjekk ut astexplorer.net og endre JavaScript til GraphQL. Du vil se et spørsmål til venstre og en AST til høyre.
  2. Validerer - AST er validert mot skjemaet. Sjekker for riktig syntaks og om feltene eksisterer.
  3. Utfør - Kjøretiden går gjennom AST, starter fra roten av treet, påkaller oppløsere, samler opp resultater og sender ut JSON.

For dette eksemplet refererer vi til denne spørringen:

Spørsmål for senere referanse

Når denne spørringen er analysert, konverteres den til en AST eller et tre.

Spørsmål representert som et tre

Root Query-typen er inngangspunktet til treet og inneholder de to rotfeltene våre, bruker og album. Bruker- og albumoppløsere kjøres parallelt (noe som er typisk for alle tidspunkter). Treet utføres bredde-først, noe som betyr at brukeren må løses før barnas navn og e-post blir kjørt. Hvis brukeroppløseren er asynkron, forsinker brukergrenen til den er løst. Når alle bladnoder, navn, e-post, tittel er løst, er utførelsen fullført.

Root Query-felt, som bruker og album, utføres parallelt, men i ingen spesiell rekkefølge. Vanligvis utføres felt i den rekkefølgen de vises i spørringen, men det er ikke trygt å anta det. Fordi felt utføres parallelt, antas de å være atom-, idempotente og bivirkningsfrie.

Ser nærmere på oppløsere

I de neste seksjonene bruker vi JavaScript, men GraphQL-servere kan skrives på nesten alle språk.

Resolvere med fire argumenter - rot, args, kontekst, info

I en eller annen form får hver resolver på hvert språk disse fire argumentene:

  • root - Resultat fra forrige / foreldretype
  • args - Argumenter levert til feltet
  • kontekst - et mutabelt objekt som blir gitt til alle oppløsere
  • info - Feltspesifikk informasjon relevant for spørringen (brukes sjelden)

Disse fire argumentene er kjernen i forståelsen av hvordan data flyter mellom oppløsere.

Standardoppløsere

Før vi fortsetter, er det verdt å merke seg at en GraphQL-server har innebygde standardoppløsere, slik at du ikke trenger å spesifisere en resolverfunksjon for hvert felt. En standard resolver vil se i roten for å finne en eiendom med samme navn som feltet. En implementering ser sannsynligvis slik ut:

Standard implementering av resolver

Henter data i oppløsere

Hvor skal vi hente data? Hva er avveiningene med alternativene våre?

I de neste eksemplene vil vi vise tilbake til dette skjemaet:

Et hendelsesfelt har et nødvendig ID-argument, returnerer en hendelse

Overføring av data mellom oppløsere

kontekst er et mutabelt objekt som gis til alle oppløsere. Det er opprettet og ødelagt mellom hver forespørsel. Det er et flott sted å lagre vanlige Auth-data, vanlige modeller / hentere for APIer og databaser, osv. Hos PayPal er vi en stor Node.js-butikk med infrastruktur bygd på Express, så vi lagrer Express 'spørsmål der inne.

Når du først lærer om kontekst, kan en første tanke være å bruke kontekst som en generell cache. Dette anbefales ikke, men her er hvordan en implementering kan se ut.

Overføring av data mellom oppløsere ved bruk av kontekst. Dette anbefales ikke!

Når tittelen påberopes, lagrer vi hendelsesresultatet i sammenheng. Når photoUrl påberopes, trekker vi hendelsen ut av konteksten og bruker den. Denne koden er ikke pålitelig. Det er ingen garanti for at tittelen blir kjørt før photoUrl.

Vi kan fikse opp begge oppløsere for å sjekke om hendelsen eksisterer i sammenheng. Bruk i så fall det. Ellers henter vi den og lagrer den til senere, men det er fremdeles et stort overflate for feil.

I stedet bør vi unngå å mutere kontekst inne i oppløsere. Vi bør forhindre at kunnskap og bekymringer blandes mellom hverandre, slik at våre oppløsere er enkle å forstå, feilsøke og teste.

Videresending av data fra foreldre til barn

Rotargumentet er for å overføre data fra foreldreløsere til barneoppløsere.

For eksempel, hvis du bygger en hendelsestype der alle felt av hendelsen
avhengig av de samme dataene, kan det hende du vil hente dem en gang i arrangementsfeltet,
i stedet for på hvert felt av arrangementet.

Virker som en god idé, ikke sant? Dette er en rask måte å komme i gang med å bygge oppløsere på, men du kan få problemer. La oss forstå hvorfor.

For eksemplene nedenfor, jobber vi med en hendelsestype som har to felt.

Aktivitetstype med to felt: tittel og fotoUrl

De fleste av feltene for Event kan hentes fra en Event API, slik at vi kan hente den på toppnivå-hendelsesoppløseren og gi resultatene til tittelen og fotoUrl-oppløserne.

Begivenhetsresolver på toppnivå henter data, gir resultater til tittel- og fotoUrl-feltoppløsere

Enda bedre, vi trenger ikke å spesifisere de to nederste oppløsningene.
Vi kan bruke standardoppløserne fordi objektet returneres av getEvent ()
har en tittel og fotoUrl-egenskap.

ID og tittel løses ved bruk av standardoppløsere

Hva er galt med dette?

Det er to scenarier der du kanskje får overhenting ...

Scenario nr. 1: Henting av flere lag

La oss si at noen krav kommer inn, og du må vise deltakerne fra et arrangement. Vi starter med å legge et deltakerfelt til Event.

Arrangementstype med et ekstra deltakerfelt

Når du henter deltakernes detaljer, har du to alternativer: hente dataene på hendelsesoppløseren, eller deltakernes resolver.

Vi vil teste ut det første alternativet: legge det til hendelsesoppløseren.

hendelsesløseren kaller to API-er, henter detaljer om deltakere og deltakernes detaljer

Hvis en klient etterspør bare tittel og fotoUrl, men ikke deltakere. Nå er du ineffektiv og ber en unødvendig forespørsel til deltakernes API.

Det er ikke din feil, det er slik vi jobber. Vi kjenner igjen mønstre og kopierer dem.
Hvis bidragsytere ser at henting av data gjøres i hendelsesoppløseren, vil de sannsynligvis gjøre det
legg til ytterligere data som henter der uten å tenke for hardt på det.

Vi har ett alternativ til å teste med å hente de fremmøtte inne i deltakernes resolver.

deltakernes resolver henter deltakernes detaljer fra deltakernes API

Hvis kunden vår bare spør for deltakere, ikke tittel og fotoUrl. Vi er fremdeles ineffektive ved å sende en unødvendig forespørsel til Events API.

Scenario nr. 2: N + 1 Problem

Fordi data hentes på feltnivå, risikerer vi å overhente. Overhenting og N + 1-problemet er et populært tema i GraphQL-verdenen. Shopify har en flott artikkel som forklarer N + 1 godt.

Hvordan påvirker det oss her?

For å illustrere det bedre, vil vi legge til et nytt hendelsesfelt som returnerer alle hendelser.

Et hendelsesfelt returnerer alle hendelser.Forespørsel for alle hendelser m / tittel og deltakere

Hvis en klient spør om alle arrangementer og deres deltagere, risikerer vi å overhente fordi deltakere kan delta på mer enn ett arrangement. Vi kan komme med duplikatforespørsler for samme deltaker.

Dette problemet forsterkes i en stor organisasjon der forespørsler kan vifte ut og forårsake unødvendig press på systemet ditt.

For å løse dette, må vi batches og tømme ned forespørsler!

I JavaScript er noen populære alternativer datalader og Apollo datakilder.

Hvis du bruker et annet språk, er det sannsynligvis noe du kan plukke opp. Så ta en titt rundt før du løser dette på egen hånd.

I kjernen av det sitter disse bibliotekene på toppen av datatilgangslaget ditt og vil bufrer og tapper utgående forespørsler ved å bruke avvisning eller memoisering. Hvis du er nysgjerrig på hvordan async memoization ser ut, sjekk ut Daniel Brains utmerkede innlegg!

Henter data på feltnivå

Tidligere så vi at det er lett å bli brent av å overhente med “topptunge” foreldre-til-barn-oppløsere.

Er det et bedre alternativ?

La oss drille ut alternativet mellom foreldre og barn igjen. Hva om vi reverserer det slik at barnefeltene våre er ansvarlige for å hente egne data?

Felt er ansvarlig for egen datahenting.
Hvorfor er dette et bedre alternativ?

Denne koden er lett å resonnere over. Du vet nøyaktig hvor en e-postadresse blir hentet. Dette gjør det enkelt å feilsøke.

Denne koden er mer testbar. Du trenger ikke å teste hendelsesoppløseren når du egentlig bare ville teste titteloppløseren.

For noen kan getEvent-dupliseringen se ut som en kodelukt. Men å ha kode som er enkel, enkel å resonnere om, og som er mer testbar, er verdt litt duplisering.

Men det er fremdeles et potensielt problem her. Hvis en klient spør om tittel og fotoUrl, fremsetter vi en ekstra forespørsel på Event API med getEvent. Som vi så tidligere i N + 1-problemet, bør vi fjerne ned forespørsler på rammenivå ved å bruke biblioteker som dataloader og Apollo datakilder.

Hvis vi henter data på feltnivå og trekker forespørsler, har vi kode som er lettere å feilsøke og teste, og vi kan hente data optimalt uten å tenke på det.

Beste praksis

  • Henting og videreføring av data fra foreldre til barn bør brukes sparsomt.
  • Bruk biblioteker som datalader for å fjerne nedtrekksforespørsler.
  • Vær oppmerksom på presset du påfører datakildene dine.
  • Ikke muter “kontekst”. Sikrer konsistent, mindre buggy-kode.
  • Skriv oppløsere som er lesbare, vedlikeholdbare, testbare. Ikke for smart.
  • Gjør dine oppløsere så tynne som mulig. Trekk ut datahenterlogikk til gjenbrukbare asynkfunksjoner.

Følg med!

Tanker? Vi vil gjerne høre teamets beste praksis og erfaringer med bygningsoppløsere. Dette er et tema som ikke ofte diskuteres, men som er viktig for å bygge langvarige GraphQL API-er.

I kommende innlegg vil vi dele tankene våre om: skjemdesign, feilhåndtering, produksjonssynlighet, optimalisering av klientsiden-integrasjoner og verktøy for team.

Vi ansetter! Hvis du vil jobbe med front-end infrastruktur, GraphQL eller React på PayPal, DM meg på Twitter på @mark_stuart!