GraphQL-opløsere: bedste praksis

Fra graphql.org

Dette indlæg er den første del af en række bedste praksis og observationer, vi har foretaget, mens vi byggede GraphQL API'er på PayPal. I kommende stillinger vil vi dele vores tanker om: skema design, fejlhåndtering, produktionssynlighed, optimering af klientsiden integration og værktøj til teams.

Du har måske set vores tidligere indlæg “GraphQL: En succeshistorie for PayPal Checkout” om PayPal rejse fra REST til GraphQL. Dette indlæg uddyber detaljerne om nogle af de bedste fremgangsmåder til at bygge opløsere, der er hurtige, testbare og elastiske over tid.

Hvad er en resolver?

Lad os starte ved den samme basislinje. Hvad er en resolver?

Resolver definition
Hvert felt på hver type er bakket op af en funktion kaldet en resolver.

En resolver er en funktion, der løser en værdi for en type eller felt i et skema. Opløsere kan returnere objekter eller skalarer som strenge, tal, boolere osv. Hvis et objekt returneres, fortsætter udførelsen til det næste underordnede felt. Hvis en skalar returneres (typisk ved en bladknude), afsluttes udførelsen. Hvis null returneres, stopper eksekveringen og fortsætter ikke.

Opløsere kan også være asynkrone! De kan løse værdier fra et andet REST API, database, cache, konstant osv.

Senere vil vi gennemgå en række eksempler, der illustrerer, hvordan man bygger opløsere, der er hurtige, testbare og elastiske.

Udfører forespørgsler

For bedre at forstå beslutningstagere skal du vide, hvordan forespørgsler udføres.

Hver GraphQL-forespørgsel gennemgår tre faser. Forespørgsler analyseres, valideres og udføres.

  1. Analyse - En forespørgsel parses i et abstrakt syntakstræ (eller AST). AST'er er utroligt magtfulde og bag værktøjer som ESLint, babel osv. Hvis du vil se, hvordan en GraphQL AST ser ud, så tjek astexplorer.net og skift JavaScript til GraphQL. Du vil se en forespørgsel til venstre og en AST til højre.
  2. Valider - AST valideres mod skemaet. Kontrollerer for korrekt syntaks for forespørgsel, og hvis felterne findes.
  3. Udfør - Kørslingstiden går gennem AST, startende fra træets rod, påkalder resolvere, indsamler resultater og udsender JSON.

I dette eksempel henviser vi til denne forespørgsel:

Forespørgsel til senere henvisning

Når denne forespørgsel er parset, konverteres den til en AST eller et træ.

Forespørgsel repræsenteret som et træ

Roden Forespørgselstype er indgangspunktet til træet og indeholder vores to rodfelter, bruger og album. Bruger- og albumopløsere udføres parallelt (hvilket er typisk for alle driftstider). Træet udføres bredde-først, hvilket betyder, at brugeren skal løses, før dets børn navn og e-mail udføres. Hvis brugeropløseren er asynkron, forsinker brugergrenen indtil dens er løst. Når alle bladnoder, navn, e-mail, titel er løst, er udførelsen fuldført.

Root Query-felter, som bruger og album, udføres parallelt, men i ingen særlig rækkefølge. Felter udføres typisk i den rækkefølge, de vises i forespørgslen, men det er ikke sikkert at antage det. Da felter udføres parallelt, antages de at være atomare, idempotente og bivirkningsfrie.

Ser nærmere på beslutningstagere

I de næste par afsnit bruger vi JavaScript, men GraphQL-servere kan skrives på næsten ethvert sprog.

Resolver med fire argumenter - rod, args, kontekst, info

I en eller anden form modtager enhver resolver på hvert sprog disse fire argumenter:

  • root - Resultat fra forrige / overordnet type
  • args - Argumenter leveret til feltet
  • kontekst - et mutabelt objekt, der leveres til alle beslutningstagere
  • info - Feltspecifik information relevant for forespørgslen (bruges sjældent)

Disse fire argumenter er centrale for at forstå, hvordan data flyder mellem opløsere.

Standardopløsere

Før vi fortsætter, er det værd at bemærke, at en GraphQL-server har indbyggede standardopløsere, så du ikke behøver at specificere en resolverfunktion for hvert felt. En standardopløser vil se i rod for at finde en egenskab med samme navn som feltet. En implementering ser sandsynligvis sådan ud:

Standard implementering af resolver

Henter data i opløsere

Hvor skal vi hente data? Hvad er de kompromiser med vores muligheder?

I de næste par eksempler henviser vi til dette skema:

Et hændelsesfelt har et påkrævet id-argument, returnerer en begivenhed

Videregivelse af data mellem opløsere

kontekst er et mutérbart objekt, der leveres til alle opløsere. Det er oprettet og ødelagt mellem enhver anmodning. Det er et godt sted at gemme almindelige Auth-data, almindelige modeller / hentere til API'er og databaser osv. Hos PayPal er vi en stor Node.js-butik med infrastruktur bygget på Express, så vi opbevarer Express 'krav derinde.

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

Videregivelse af data mellem opløsere ved hjælp af kontekst. Dette anbefales ikke!

Når titel kaldes op, gemmer vi begivenhedsresultatet i en sammenhæng. Når photoUrl kaldes, trækker vi begivenheden ud af sammenhæng og bruger den. Denne kode er ikke pålidelig. Der er ingen garanti for, at titlen udføres før photoUrl.

Vi kunne løse begge opløsere for at kontrollere, om begivenheden findes i en sammenhæng. I så fald skal du bruge det. Ellers henter vi det og opbevarer det til senere, men der er stadig et stort overfladeareal for fejl.

I stedet skal vi undgå at mutere kontekst inden i resolverne. Vi bør forhindre viden og bekymringer i at blandes mellem hinanden, så vores opløsere er lette at forstå, debug og test.

Videresendelse af data fra forælder til barn

Grundargumentet er at videregive data fra overordnede beslutningstagere til børns beslutningstagere.

For eksempel, hvis du bygger en begivenhedstype, hvor alle felter i begivenheden
afhænger af de samme data, skal du muligvis hente dem en gang i begivenhedsfeltet,
snarere end på ethvert felt af begivenheden.

Virker som en god idé, ikke? Dette er en hurtig måde at komme i gang med at bygge opløsere på, men du får muligvis problemer. Lad os forstå hvorfor.

For eksemplerne nedenfor arbejder vi med en begivenhedstype, der har to felter.

Begivenhedstype med to felter: titel og fotoUrl

De fleste af felterne til begivenhed kan hentes fra et hændelses-API, så vi kan hente det på øverste niveau af hændelsesopløseren og give resultaterne til vores titel og fotoUrl-opløsere.

Begivenhedsopløseren på topniveau henter data, giver resultater til titel- og fotoUrl-feltopløsere

Endnu bedre behøver vi ikke at specificere de to nederste opløsere.
Vi kan bruge standardopløsere, fordi objektet returneres af getEvent ()
har en titel og en fotoUrl-egenskab.

id og titel løses ved hjælp af standardopløsere

Hvad er der galt med dette?

Der er to scenarier, hvor du muligvis løber ud med at overhente ...

Scenario nr. 1: Hentning af flere lag af data

Lad os sige, at nogle krav kommer ind, og du skal vise en begivenheds deltagere. Vi starter med at tilføje et deltagerfelt til Event.

Begivenhedstype med et ekstra deltagerfelt

Når du henter de deltagendes detaljer, har du to muligheder: hente disse data i begivenhedsopløseren eller deltagerne.

Vi tester den første mulighed: at tilføje den til begivenhedsopløseren.

begivenhedsopløseren kalder to API'er, henter detaljer om begivenheder og deltagernes detaljer

Hvis en klient forespørger kun om titel og fotoUrl, men ikke deltagere.Nu er du ineffektiv og fremsætter en unødvendig anmodning til dit deltagers API.

Det er ikke din skyld, det er sådan vi arbejder. Vi genkender mønstre og kopierer dem.
Hvis bidragydere ser, at dataindhentning udføres i begivenhedsopløseren, er de sandsynligvis
tilføje yderligere data, der henter der uden at tænke for hårdt over det.

Vi har endnu en mulighed til at teste med at hente de deltagere inde i de deltagende resolver.

deltagernes resolver henter deltagernes detaljer fra deltagerens API

Hvis vores klientforespørgsler kun til deltagere, ikke titel og fotoUrl. Vi er stadig ineffektive ved at fremsætte en unødvendig anmodning til vores Events API.

Scenario nr. 2: N + 1 Problem

Da data hentes på et feltniveau, risikerer vi at overhente. Overfetching og N + 1-problemet er et populært emne i GraphQL-verdenen. Shopify har en fantastisk artikel, der forklarer N + 1 godt.

Hvordan påvirker det os her?

For at illustrere det bedre tilføjer vi et nyt begivenhedsfelt, der returnerer alle begivenheder.

Et hændelsesfelt returnerer alle begivenheder.Forespørgsel til alle begivenheder m / deres titel og deltagere

Hvis en klient forespørger om alle begivenheder og deres deltagere, risikerer vi at overhente, fordi deltagere kan deltage i mere end en begivenhed. Vi kan muligvis indgive duplikatanmodninger til den samme deltager.

Dette problem forstærkes i en stor organisation, hvor anmodninger kan blæse ud og forårsage unødvendigt pres på dit system.

For at løse dette er vi nødt til at batches og udtype anmodninger!

I JavaScript er nogle af de populære indstillinger datalader og Apollo datakilder.

Hvis du bruger et andet sprog, er der sandsynligvis noget, du kan samle op. Så kig dig omkring, før du løser dette på egen hånd.

I kernen af ​​det sidder disse biblioteker oven på dit datatilgangslag og cache og udtømmer udgående anmodninger ved hjælp af afvisning eller memoization. Hvis du er nysgerrig efter, hvordan async memoization ser ud, så tjek Daniel Brains fremragende indlæg!

Henter data på feltniveau

Tidligere så vi, at det er let at blive forbrændt ved at overhente med "toptunge" forældre-til-barn-resolvere.

Er der et bedre alternativ?

Lad os drille muligheden for forældre til barn igen. Hvad hvis vi vender det, så vores børnefelt er ansvarlige for at hente deres egne data?

Felter er ansvarlige for deres egen dataindhentning.
Hvorfor er dette et bedre alternativ?

Denne kode er let at resonnere over. Du ved nøjagtigt, hvor en e-mail hentes. Dette gør det nemt at debugge.

Denne kode er mere testbar. Du behøver ikke at teste hændelsesopløseren, når du virkelig bare ville teste titelopløseren.

For nogle kan getEvent-duplikationen se ud som en kodeluft. Men at have kode, der er enkel, let at resonere om og er mere testbar, er værd at være en lille smule duplikation.

Men der er stadig et potentielt problem her. Hvis en klient forespørger om titel og fotoUrl, fremsætter vi en yderligere anmodning på vores Event API med getEvent. Som vi så tidligere i N + 1-problemet, skulle vi udtømme anmodninger på et rammeniveau ved hjælp af biblioteker som dataloader og Apollo-datakilder.

Hvis vi henter data på et feltniveau og udleder anmodninger, har vi kode, der er lettere at fejlsøge og teste, og vi kan hente data optimalt uden at tænke over det.

Bedste praksis

  • Hentning og videregivelse af data fra forældre til barn bør bruges sparsomt.
  • Brug biblioteker som datalader til at nedtype forespørgsler nedstrøms.
  • Vær opmærksom på ethvert pres, du udøver på dine datakilder.
  • Muter ikke "kontekst". Sikrer ensartet, mindre buggy-kode.
  • Skriv opløsere, der er læsbare, vedligeholdelige, testbare. Ikke for smart.
  • Gør dine opløsere så tynde som muligt. Udtræk data, der henter logik til genanvendelige async-funktioner.

Bliv hængende!

Tanker? Vi vil meget gerne høre dit teams bedste praksis og erfaringer med bygningsopløsere. Dette er et emne, der ikke ofte diskuteres, men som er vigtigt for at opbygge langvarige GraphQL API'er.

I kommende stillinger vil vi dele vores tanker om: skema design, fejlhåndtering, produktionssynlighed, optimering af klientsiden integration og værktøj til teams.

Vi ansætter! Hvis du gerne vil arbejde på front-end infrastruktur, GraphQL eller React på PayPal, DM mig på Twitter på @mark_stuart!