GraphQL مع Golang: الغوص العميق من الأساسيات إلى المستوى المتقدم

أصبحت GraphQL كلمة طنانة على مدار السنوات القليلة الماضية بعد أن جعلها Facebook مفتوحة المصدر. لقد جربت GraphQL مع Node.js ، وأنا أتفق مع كل الضجة حول مزايا وبساطة GraphQL.

إذن ما هي GraphQL؟ هذا ما يقوله تعريف GraphQL الرسمي:

GraphQL هي لغة استعلام لواجهات برمجة التطبيقات ووقت التشغيل للوفاء بهذه الاستعلامات ببياناتك الحالية. توفر GraphQL وصفًا كاملاً ومفهومًا للبيانات الموجودة في واجهة برمجة التطبيقات الخاصة بك ، وتمنح العملاء القدرة على طلب ما يحتاجون إليه بالضبط وليس أكثر من ذلك ، وتسهيل تطوير واجهات برمجة التطبيقات بمرور الوقت ، وتمكين أدوات المطور القوية.

انتقلت مؤخرًا إلى 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{}هنا أبدًا ، لأنها تستخدم أسلوبًا مكتوبًا بدقة.

بصرف النظر عن ذلك ، فإنه يستخدم النهج الأول للمخطط: لذلك تحدد واجهة برمجة التطبيقات الخاصة بك باستخدام لغة تعريف مخطط الرسم البياني. يحتوي هذا على أدوات إنشاء التعليمات البرمجية القوية الخاصة به والتي ستنشئ تلقائيًا جميع رموز GraphQL الخاصة بك وستحتاج فقط إلى تنفيذ المنطق الأساسي لطريقة الواجهة.

لقد قسمت هذه المقالة إلى مرحلتين:

  • الأساسيات: التكوين والطفرات والاستعلامات والاشتراك
  • المتقدم: المصادقة ، ومحمل البيانات ، وتعقيد الاستعلام

المرحلة 1: الأساسيات - التكوين والطفرات والاستعلامات والاشتراكات

سنستخدم موقع نشر الفيديو كمثال يمكن فيه للمستخدم نشر مقطع فيديو وإضافة لقطات شاشة وإضافة مراجعة والحصول على مقاطع فيديو ومقاطع فيديو ذات صلة.

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

أنشئ المخطط التالي في جذر المشروع:

هنا حددنا نماذجنا الأساسية وطفرة واحدة لنشر مقاطع فيديو جديدة واستعلام واحد للحصول على جميع مقاطع الفيديو. يمكنك قراءة المزيد عن مخطط الرسم البياني هنا. لقد حددنا أيضًا نوعًا مخصصًا واحدًا (عدديًا) ، حيث يحتوي Graphql افتراضيًا على 5 أنواع قياسية فقط تتضمن Int و Float و String و Boolean و ID.

لذلك إذا كنت تريد استخدام نوع مخصص ، فيمكنك تحديد عدد قياسي مخصص في schema.graphql(كما حددنا Timestamp) وتقديم تعريفه في التعليمات البرمجية. في gqlgen ، تحتاج إلى توفير أساليب تنظيمية وغير منظمة لجميع المقاييس المخصصة وتعيينها إلى gqlgen.yml.

تغيير رئيسي آخر في gqlgen في الإصدار الأخير هو أنهم أزالوا التبعية على الثنائيات المترجمة. لذا أضف الملف التالي إلى مشروعك تحت scripts / gqlgen.go.

وتهيئة dep بـ:

dep init

حان الوقت الآن للاستفادة من ميزة الترميز الخاصة بالمكتبة والتي تولد كل التعليمات البرمجية الهيكلية المملة (ولكنها مثيرة للاهتمام للبعض).

go run scripts/gqlgen.go init

والتي ستنشئ الملفات التالية:

gqlgen.yml - ملف التكوين للتحكم في توليد التعليمات البرمجية.

created.go - الرمز الذي تم إنشاؤه والذي قد لا ترغب في رؤيته.

Models_gen.go - جميع نماذج الإدخال ونوع المخطط المقدم.

resilver.go - تحتاج إلى كتابة تطبيقاتك.

server / server.go - نقطة دخول مع http.Handler لبدء خادم GraphQL.

دعنا نلقي نظرة على أحد النماذج المولدة من Videoالنوع:

هنا ، كما ترى ، يتم تعريف ID كسلسلة و CreatedAt هي أيضًا سلسلة. يتم تعيين النماذج الأخرى ذات الصلة وفقًا لذلك ، ولكن في العالم الحقيقي لا تريد ذلك - إذا كنت تستخدم أي نوع من بيانات SQL ، فأنت تريد أن يكون حقل المعرف الخاص بك int أو int64 ، اعتمادًا على قاعدة البيانات الخاصة بك.

على سبيل المثال ، أنا أستخدم PostgreSQL للعرض التوضيحي ، لذا بالطبع أريد المعرف باعتباره int و CreatedAt كوقت . لذلك نحن بحاجة إلى تحديد نموذجنا الخاص وإرشاد gqlgen لاستخدام نموذجنا بدلاً من إنشاء نموذج جديد.

وتحديث gqlgen لاستخدام هذه النماذج مثل هذا:

لذا ، فإن النقطة المحورية هي التعريفات المخصصة للمعرف والطابع الزمني باستخدام أساليب التنظيم وغير التنظيم وتعيينهما في ملف gqlgen.yml. الآن عندما يقدم المستخدم سلسلة كمعرف ، سيقوم UnmarshalID بتحويل سلسلة إلى int. أثناء إرسال الاستجابة ، سيقوم MarshalID بتحويل int إلى سلسلة. الأمر نفسه ينطبق على الطابع الزمني أو أي عدد قياسي مخصص آخر تحدده.

حان الوقت الآن لتطبيق المنطق الحقيقي. افتح resolver.goوقدم التعريف للطفرة والاستفسارات. يتم إنشاء الأجزاء الجذرية تلقائيًا مع بيان ذعر لم يتم تنفيذه ، لذلك دعونا نتجاوز ذلك.

وضرب الطفرة:

أوه لقد نجحت ... لكن انتظر لماذا المستخدم الخاص بي فارغ ؟؟ لذا يوجد هنا مفهوم مشابه مثل التحميل البطيء والمتوق. نظرًا لأن GraphQL قابلة للتوسعة ، فأنت بحاجة إلى تحديد الحقول التي تريد ملؤها بشغف وأيها بتكاسل.

لقد أنشأت هذه القاعدة الذهبية لفريق مؤسستي الذي يعمل مع gqlgen:

لا تقم بتضمين الحقول في النموذج الذي تريد تحميله فقط عند طلب العميل.

بالنسبة لحالة الاستخدام الخاصة بنا ، أريد تحميل مقاطع الفيديو ذات الصلة (وحتى المستخدمين) فقط إذا طلب العميل هذه الحقول. ولكن نظرًا لتضمين هذه الحقول في النماذج ، ستفترض gqlgen أنك ستوفر هذه القيم أثناء تحليل الفيديو - لذلك نحصل حاليًا على بنية فارغة.

تحتاج أحيانًا إلى نوع معين من البيانات في كل مرة ، لذلك لا تريد تحميله باستعلام آخر. بدلاً من ذلك ، يمكنك استخدام شيء مثل انضمام SQL لتحسين الأداء. لحالة استخدام واحدة (غير مدرجة في المقالة) ، كنت بحاجة إلى بيانات وصفية للفيديو في كل مرة مع الفيديو المخزن في مكان مختلف. لذلك إذا قمت بتحميله عند الطلب ، سأحتاج إلى استعلام آخر. ولكن نظرًا لأنني كنت أعرف متطلباتي (التي أحتاجها في كل مكان على جانب العميل) ، فقد فضلت تحميلها بشغف لتحسين الأداء.

لذلك دعونا نعيد كتابة النموذج ونجدد كود gqlgen. من أجل البساطة ، سنحدد فقط طرقًا للمستخدم.

لذلك أضفنا UserID وأزلنا بنية المستخدم وأعدنا إنشاء الكود:

go run scripts/gqlgen.go -v

سيؤدي هذا إلى إنشاء طرق الواجهة التالية لحل البنى غير المحددة وتحتاج إلى تحديد تلك الموجودة في المحلل الخاص بك:

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.

لم أحدد منطق الأسلوب المرتبط وأعدت للتو المصفوفة الفارغة. لذا فإن ذات الصلة فارغة في الإخراج ، ولكن هذا يجب أن يمنحك فكرة واضحة حول كيفية استخدام تعقيد الاستعلام.

ملاحظات نهائية

هذا الرمز موجود على جيثب. يمكنك التلاعب بها ، وإذا كان لديك أي أسئلة أو مخاوف ، فأخبرني بذلك في قسم التعليقات.

شكرا للقراءة! عدد قليل (نأمل 50) يصفق؟ دائما موضع تقدير. أنا أكتب عن جافا سكريبت، اللغة العودة، DevOps، وعلوم الحاسوب. تابعني وشارك هذا المقال اذا اعجبك

تواصل معي على TwitterLinkedin. قم بزيارة www.ridham.me للمزيد.