GraphQL med Golang: Et dybt dyk fra det grundlæggende til det avancerede

GraphQL er blevet et buzzword i løbet af de sidste par år, efter at Facebook gjorde det til open source. Jeg har prøvet GraphQL med Node.js, og jeg er enig med al brummen om fordelene og enkelheden ved GraphQL.

Så hvad er GraphQL? Dette siger den officielle GraphQL-definition:

GraphQL er et forespørgselssprog til API'er og runtime til at udfylde disse forespørgsler med dine eksisterende data. GraphQL giver en komplet og forståelig beskrivelse af dataene i din API, giver klienterne beføjelse til at bede om præcis, hvad de har brug for og intet mere, gør det lettere at udvikle API'er over tid og muliggør kraftfulde udviklerværktøjer.

Jeg skiftede for nylig til Golang for et nyt projekt, jeg arbejder på (fra Node.js), og jeg besluttede at prøve GraphQL med det. Der er ikke mange biblioteksmuligheder med Golang, men jeg har prøvet det med Thunder, graphql, graphql-go og gqlgen. Og jeg må sige, at gqlgen vinder blandt alle de biblioteker, jeg har prøvet.

gqlgen er stadig i beta med den nyeste version 0.7.2 på tidspunktet for denne artikels skrivning, og den udvikler sig hurtigt. Du kan finde deres vejkort her. Og nu sponsorerer 99designs officielt dem, så vi vil se endnu bedre udviklingshastighed til dette fantastiske open source-projekt. vektah og neelance er vigtige bidragydere, og neelance skrev også graphql-go.

Så lad os dykke ned i semantikken i biblioteket, forudsat at du har grundlæggende GraphQL-viden.

Højdepunkter

Som deres overskrift siger,

Dette er et bibliotek til hurtig oprettelse af strengt skrevet GraphQL-servere i Golang.

Jeg synes, det er den mest lovende ting ved biblioteket: Du vil aldrig se map[string]interface{}her, da det bruger en strengt skrevet tilgang.

Bortset fra det bruger den en Schema first Approach : så du definerer din API ved hjælp af graphql Schema Definition Language. Dette har sine egne kraftfulde kodegenereringsværktøjer, der automatisk genererer al din GraphQL-kode, og du bliver bare nødt til at implementere kernelogikken i denne interface-metode.

Jeg har delt denne artikel i to faser:

  • Grundlæggende: Konfiguration, mutationer, forespørgsler og abonnement
  • Det avancerede: Authentication, Dataloaders og Query Complexity

Fase 1: Grundlæggende - konfiguration, mutationer, forespørgsler og abonnementer

Vi bruger et videopubliceringssted som et eksempel, hvor en bruger kan udgive en video, tilføje skærmbilleder, tilføje en anmeldelse og få videoer og relaterede videoer.

mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/

Opret følgende skema i projektroden:

Her har vi defineret vores grundlæggende modeller og en mutation til at udgive nye videoer og en forespørgsel for at få alle videoer. Du kan læse mere om graphql-skemaet her. Vi har også defineret en brugerdefineret type (skalar), da graphql som standard kun har 5 skalartyper, der inkluderer Int, Float, String, Boolean og ID.

Så hvis du vil bruge tilpasset type, kan du definere en brugerdefineret skalar i schema.graphql(som vi har defineret Timestamp) og give dens definition i kode. I gqlgen skal du angive marshal og unmarshal metoder til alle brugerdefinerede skalarer og kortlægge dem til gqlgen.yml.

En anden større ændring i gqlgen i den sidste version er, at de har fjernet afhængigheden af ​​kompilerede binære filer. Så tilføj følgende fil til dit projekt under scripts / gqlgen.go.

og initialiser dep med:

dep init

Nu er det tid til at drage fordel af bibliotekets codegen-funktion, der genererer al den kedelige (men interessante for nogle få) skeletkoder.

go run scripts/gqlgen.go init

som opretter følgende filer:

gqlgen.yml - Konfigurationsfil til kontrol af generering af kode.

generated.go - Den genererede kode, som du måske ikke vil se.

models_gen.go - Alle modeller til input og type af dit leverede skema.

resolver.go - Du skal skrive dine implementeringer.

server / server.go - indgangspunkt med en http.Handler for at starte GraphQL-serveren.

Lad os se på en af ​​de genererede modeller af Videotypen:

Her er, som du kan se, ID defineret som en streng, og CreatedAt er også en streng. Andre relaterede modeller kortlægges i overensstemmelse hermed, men i den virkelige verden ønsker du ikke dette - hvis du bruger en SQL-datatype, skal du have dit ID-felt som int eller int64, afhængigt af din database.

For eksempel bruger jeg PostgreSQL til demo, så selvfølgelig vil jeg have ID som en int og CreatedAt som en time.Time . Så vi er nødt til at definere vores egen model og instruere gqlgen i at bruge vores model i stedet for at generere en ny.

og opdater gqlgen til at bruge disse modeller på denne måde:

Så omdrejningspunktet er de brugerdefinerede definitioner for ID og tidsstempel med marshal- og unmarshal-metoder og deres kortlægning i en gqlgen.yml-fil. Når brugeren nu angiver en streng som ID, konverterer UnmarshalID en streng til en int. Mens svaret sendes, konverterer MarshalID int til streng. Det samme gælder for Timestamp eller enhver anden brugerdefineret skalar, du definerer.

Nu er det tid til at implementere ægte logik. Åbn resolver.goog angiv definitionen til mutation og forespørgsler. Stubberne genereres allerede automatisk med en ikke implementeret panikerklæring, så lad os tilsidesætte det.

og ramte mutationen:

Ohh det fungerede ... .. men vent, hvorfor er min bruger tom ?? Så her er der et lignende koncept som doven og ivrig lastning. Da graphQL kan udvides, skal du definere, hvilke felter du vil udfylde ivrigt, og hvilke dovne.

I have created this golden rule for my organization team working with gqlgen:

Don’t include the fields in a model which you want to load only when requested by the client.

For our use-case, I want to load Related Videos (and even users) only if a client asks for those fields. But as we have included those fields in the models, gqlgen will assume that you will provide those values while resolving video — so currently we are getting an empty struct.

Sometimes you need a certain type of data every time, so you don’t want to load it with another query. Rather you can use something like SQL joins to improve performance. For one use-case (not included in the article), I needed video metadata every time with the video which is stored in a different place. So if I loaded it when requested, I would need another query. But as I knew my requirements (that I need it everywhere on the client side), I preferred it to load eagerly to improve the performance.

So let’s rewrite the model and regenerate the gqlgen code. For the sake of simplicity, we will only define methods for the user.

So we have added UserID and removed User struct and regenerated the code:

go run scripts/gqlgen.go -v

This will generate the following interface methods to resolve the undefined structs and you need to define those in your resolver:

And here is our definition:

Now the result should look something like this:

So this covers the very basics of graphql and should get you started. Try a few things with graphql and the power of Golang! But before that, let’s have a look at subscription which should be included in the scope of this article.

Subscriptions

Graphql provides subscription as an operation type which allows you to subscribe to real tile data in GraphQL. gqlgen provides web socket-based real-time subscription events.

You need to define your subscription in the schema.graphql file. Here we are subscribing to the video publishing event.

Regenerate the code by running: go run scripts/gqlgen.go -v.

As explained earlier, it will make one interface in generated.go which you need to implement in your resolver. In our case, it looks like this:

Now, you need to emit events when a new video is created. As you can see on line 23 we have done that.

And it’s time to test the subscription:

GraphQL comes with certain advantages, but everything that glitters is not gold. You need to take care of a few things like authorizations, query complexity, caching, N+1 query problem, rate limiting, and a few more issues — otherwise it will put you in performance jeopardy.

Phase 2: The advanced - Authentication, Dataloaders, and Query Complexity

Every time I read a tutorial like this, I feel like I know everything I need to know and can get my all problems solved.

But when I start working on things on my own, I usually end up getting an internal server error or never-ending requests or dead ends and I have to dig deep into that to carve my way out. Hopefully we can help prevent that here.

Let’s take a look at a few advanced concepts starting with basic authentication.

Authentication

In a REST API, you have a sort of authentication system and some out of the box authorizations on particular endpoints. But in GraphQL, only one endpoint is exposed so you can achieve this with schema directives.

You need to edit your schema.graphql as follows:

We have created an isAuthenticated directive and now we have applied that directive to createVideo subscription. After you regenerate code you need to give a definition of the directive. Currently, directives are implemented as struct methods instead of the interface so we have to give a definition.

I have updated the generated code of server.go and created a method to return graphql config for server.go as follows:

We have read the userId from the context. Looks strange right? How was userId inserted in the context and why in context? Ok, so gqlgen only provides you the request contexts at the implementation level, so you can not read any of the HTTP request data like headers or cookies in graphql resolvers or directives. Therefore, you need to add your middleware and fetch those data and put the data in your context.

So we need to define auth middleware to fetch auth data from the request and validate.

I haven’t defined any logic there, but instead I passed the userId as authorization for demo purposes. Then chain this middleware in server.go along with the new config loading method.

Now, the directive definition makes sense. Don’t handle unauthorized users in your middleware as it will be handled by your directive.

Demo time:

You can even pass arguments in the schema directives like this:

directive @hasRole(role: Role!) on FIELD_DEFINITIONenum Role { ADMIN USER }

Dataloaders

This all looks fancy, doesn’t it? You are loading data when needed. Clients have control of the data, there is no under-fetching and no over-fetching. But everything comes with a cost.

So what’s the cost here? Let’s take a look at the logs while fetching all the videos. We have 8 video entries and there are 5 users.

query{ Videos(limit: 10){ name user{ name } }}
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1

Why 9 queries (1 videos table and 8 users table)? It looks horrible. I was just about to have a heart attack when I thought about replacing our current REST API servers with this…but dataloaders came as a complete cure for it!

This is known as the N+1 problem, There will be one query to get all the data and for each data (N) there will be another database query.

This is a very serious issue in terms of performance and resources: although these queries are parallel, they will use your resources up.

We will use the dataloaden library from the author of gqlgen. It is a Go- generated library. We will generate the dataloader for the user first.

go get github.com/vektah/dataloadendataloaden github.com/ridhamtarpara/go-graphql-demo/api.User

This will generate a file userloader_gen.go which has methods like Fetch, LoadAll, and Prime.

Now, we need to define the Fetch method to get the result in bulk.

Here, we are waiting for 1ms for a user to load queries and we have kept a maximum batch of 100 queries. So now, instead of firing a query for each user, dataloader will wait for either 1 millisecond for 100 users before hitting the database. We need to change our user resolver logic to use dataloader instead of the previous query logic.

After this, my logs look like this for similar data:

Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5)

Now only two queries are fired, so everyone is happy. The interesting thing is that only five user keys are given to query even though 8 videos are there. So dataloader removed duplicate entries.

Query Complexity

In GraphQL you are giving a powerful way for the client to fetch whatever they need, but this exposes you to the risk of denial of service attacks.

Let’s understand this through an example which we’ve been referring to for this whole article.

Now we have a related field in video type which returns related videos. And each related video is of the graphql video type so they all have related videos too…and this goes on.

Consider the following query to understand the severity of the situation:

{ Videos(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 100, offset: 0){ name url } } } }}

If I add one more subobject or increase the limit to 100, then it will be millions of videos loading in one call. Perhaps (or rather definitely) this will make your database and service unresponsive.

gqlgen provides a way to define the maximum query complexity allowed in one call. You just need to add one line (Line 5 in the following snippet) in your graphql handler and define the maximum complexity (300 in our case).

gqlgen assigns fix complexity weight for each field so it will consider struct, array, and string all as equals. So for this query, complexity will be 12. But we know that nested fields weigh too much, so we need to tell gqlgen to calculate accordingly (in simple terms, use multiplication instead of just sum).

Just like directives, complexity is also defined as struct, so we have changed our config method accordingly.

Jeg har ikke defineret den relaterede metodelogik og har lige returneret det tomme array. Så relateret er tomt i output, men dette skal give dig en klar idé om, hvordan du bruger forespørgslens kompleksitet.

Afsluttende noter

Denne kode findes på Github. Du kan lege med det, og hvis du har spørgsmål eller bekymringer, så lad mig det vide i kommentarsektionen.

Tak for læsningen! Et par (forhåbentlig 50) klapper? er altid værdsat. Jeg skriver om JavaScript, Go Language, DevOps og Computer Science. Følg mig og del denne artikel, hvis du kan lide det.

Kontakt mig på @Twitter @Linkedin. Besøg www.ridham.me for mere.