GraphQL с Golang: Дълбоко гмуркане от основите към напредналите

GraphQL се превърна в модна дума през последните няколко години, след като Facebook го направи с отворен код. Опитах GraphQL с Node.js и съм съгласен с целия шум относно предимствата и простотата на GraphQL.

И така, какво е GraphQL? Ето какво казва официалната дефиниция на GraphQL:

GraphQL е език за заявки за API и време за изпълнение за изпълнение на тези заявки със съществуващите ви данни. GraphQL предоставя пълно и разбираемо описание на данните във вашия API, дава на клиентите силата да искат точно това, от което се нуждаят, и нищо повече, улеснява развитието на API с течение на времето и дава възможност за мощни инструменти за разработчици.

Наскоро преминах към Golang за нов проект, по който работя (от Node.js) и реших да опитам GraphQL с него. Няма много опции за библиотека с Golang, но аз го опитах с Thunder, graphql, graphql-go и gqlgen. И трябва да кажа, че gqlgen печели сред всички библиотеки, които съм опитвал.

gqlgen все още е в бета версия с най-новата версия 0.7.2 към момента на писане на тази статия и бързо се развива. Можете да намерите пътната им карта тук. И сега 99designs ги спонсорира официално, така че ще видим още по-добра скорост на развитие за този страхотен проект с отворен код. vektah и neelance имат основен принос, а neelance също пише graphql-go.

Така че нека се потопим в библиотечната семантика, ако приемем, че имате основни познания за GraphQL.

Акценти

Както гласи заглавието им,

Това е библиотека за бързо създаване на строго типизирани GraphQL сървъри в Golang.

Мисля, че това е най-обещаващото нещо за библиотеката: никога няма да видите map[string]interface{}тук, тъй като тя използва строго типизиран подход.

Отделно от това, той използва Schema first Approach : така че вие ​​дефинирате своя API с помощта на Graphql Schema Definition Language. Това има свои мощни инструменти за генериране на код, които автоматично ще генерират целия ви код на GraphQL и просто ще трябва да внедрите основната логика на този интерфейсен метод.

Разделих тази статия на две фази:

  • Основите: Конфигурация, Мутации, Заявки и Абонамент
  • Разширените: Удостоверяване, Зареждащи данни и Сложност на заявките

Фаза 1: Основите - конфигурация, мутации, заявки и абонаменти

Ще използваме сайт за публикуване на видеоклипове като пример, в който потребителят може да публикува видеоклип, да добавя екранни снимки, да добавя рецензия и да получава видеоклипове и свързани видеоклипове.

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

Създайте следната схема в корен на проекта:

Тук дефинирахме нашите основни модели и една мутация за публикуване на нови видеоклипове и една заявка за получаване на всички видеоклипове. Можете да прочетете повече за схемата graphql тук. Дефинирали сме и един персонализиран тип (скаларен), тъй като по подразбиране graphql има само 5 скаларни типа, които включват Int, Float, String, Boolean и ID.

Така че, ако искате да използвате персонализиран тип, тогава можете да дефинирате персонализиран скалар в schema.graphql(както дефинирахме Timestamp) и да предоставите дефиницията му в код. В gqlgen трябва да предоставите маршалски и немаршалски методи за всички персонализирани скалари и да ги картографирате gqlgen.yml.

Друга голяма промяна в gqlgen в последната версия е, че те премахнаха зависимостта от компилираните двоични файлове. Така че добавете следния файл към вашия проект под скриптове / gqlgen.go.

и инициализирайте Dep с:

dep init

Сега е време да се възползвате от функцията на кодегена на библиотеката, която генерира целия скучен (но интересен за няколко) скелетен код.

go run scripts/gqlgen.go init

което ще създаде следните файлове:

gqlgen.yml - Конфигурационен файл за управление на генерирането на код.

generated.go - Генерираният код, който може да не искате да видите.

models_gen.go - Всички модели за въвеждане и тип на предоставената от вас схема.

resolver.go - Трябва да напишете вашите реализации.

server / server.go - входна точка с http.Handler за стартиране на GraphQL сървъра.

Нека да разгледаме един от генерираните модели от Videoтипа:

Тук, както можете да видите, ID се дефинира като низ, а CreatedAt също е низ. Други свързани модели се картографират по съответния начин, но в реалния свят не искате това - ако използвате някакъв тип данни на SQL, искате вашето поле за идентификация като int или int64, в зависимост от вашата база данни.

Например използвам PostgreSQL за демонстрация, така че, разбира се, искам ID като int и CreatedAt като time.Time . Затова трябва да дефинираме собствен модел и да инструктираме gqlgen да използва нашия модел, вместо да генерира нов.

и актуализирайте gqlgen, за да използвате тези модели по следния начин:

И така, фокусна точка са персонализираните дефиниции за ID и Timestamp с маршалските и немаршалските методи и тяхното картографиране във файл gqlgen.yml. Сега, когато потребителят предостави низ като ID, UnmarshalID ще преобразува низ в int. Докато изпраща отговора, MarshalID ще преобразува int в низ. Същото важи и за Timestamp или всеки друг персонализиран скалар, който дефинирате.

Сега е време да внедрим истинска логика. Отворете resolver.goи предоставете дефиницията за мутация и заявки. Стъбчетата вече се генерират автоматично с неприложена операция за паника, така че нека заменим това.

и удари мутацията:

Ох, работи… .. но изчакайте, защо потребителят ми е празен ?? Така че тук има подобна концепция като мързеливо и нетърпеливо натоварване. Тъй като graphQL е разширяем, трябва да определите кои полета искате да попълвате с нетърпение и кои лениво.

Създадох това златно правило за моя организационен екип, работещ с gqlgen:

Не включвайте полетата в модел, който искате да заредите само при поискване от клиента.

За нашия случай на употреба искам да заредя сродни видеоклипове (и дори потребители) само ако клиент поиска тези полета. Но тъй като ние включихме тези полета в моделите, gqlgen ще приеме, че вие ​​ще предоставите тези стойности, докато решавате видео - така че в момента получаваме празна структура.

Понякога всеки път се нуждаете от определен тип данни, така че не искате да ги зареждате с друга заявка. По-скоро можете да използвате нещо като SQL присъединяване за подобряване на производителността. За един случай на употреба (който не е включен в статията) имах нужда от видео метаданни всеки път с видеото, което се съхранява на различно място. Така че, ако го заредих при поискване, ще ми трябва друга заявка. Но тъй като знаех изискванията си (че имам нужда от него навсякъде от страна на клиента), предпочетох да се зарежда с нетърпение, за да подобри производителността.

Така че нека пренапишем модела и регенерираме gqlgen кода. За по-голяма простота ще определим само методи за потребителя.

Така че добавихме UserID и премахнахме потребителската структура и регенерирахме кода:

go run scripts/gqlgen.go -v

Това ще генерира следните методи за интерфейс за разрешаване на недефинираните структури и трябва да дефинирате тези във вашия преобразувател:

И тук е нашето определение:

Сега резултатът трябва да изглежда по следния начин:

Така че това обхваща самите основи на graphql и трябва да започнете. Опитайте няколко неща с graphql и силата на Golang! Но преди това, нека да разгледаме абонамента, който трябва да бъде включен в обхвата на тази статия.

Абонаменти

Graphql предоставя абонамент като тип операция, която ви позволява да се абонирате за реални данни за плочки в GraphQL. gqlgen предоставя абонаментни събития в реално време, базирани на уеб сокет.

Трябва да определите абонамента си във schema.graphqlфайла. Тук се абонираме за събитието за видеоиздаване.

Повторно генериране на кода, като използвате: go run scripts/gqlgen.go -v.

Както беше обяснено по-рано, той ще направи един интерфейс в generated.go, който трябва да внедрите във вашия резолвер. В нашия случай изглежда така:

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.

I haven’t defined the related method logic and just returned the empty array. So related is empty in the output, but this should give you a clear idea about how to use the query complexity.

Final Notes

This code is on Github. You can play around with it, and if you have any questions or concerns let me know in the comment section.

Thanks for reading! A few (hopefully 50) claps? are always appreciated. I write about JavaScript, the Go Language, DevOps, and Computer Science. Follow me and share this article if you like it.

Reach out to me on @Twitter @Linkedin. Visit www.ridham.me for more.