أصبحت تقنية GraphQL على مدى السنوات القليلة الماضية ، بعد أن قامت شركة Facebook بنقلها إلى فئة المصادر المفتوحة ، بشعبية كبيرة. يقول مؤلف هذه المادة ، التي ننشرها اليوم ، إنه حاول العمل مع GraphQL في Node.js ، ومن تجربته الخاصة ، كان مقتنعًا أن هذه التكنولوجيا ، نظرًا لقدراتها الرائعة وبساطتها ، لا تجذب الكثير من الاهتمام عن طريق الخطأ. في الآونة الأخيرة ، أثناء مشاركته في مشروع جديد ، انتقل من Node.js إلى Golang. ثم قرر اختبار تعاون Golang و GraphQL.

معلومات أولية
يمكنك أن تتعلم من تعريف GraphQL الرسمي أن هذه لغة استعلام لواجهة برمجة التطبيقات ووقت تشغيل لتنفيذ مثل هذه الاستعلامات على البيانات الموجودة. يوفر GraphQL وصفًا كاملاً ومفهومًا للبيانات في واجهة برمجة تطبيقات معينة ، ويسمح للعملاء بطلب المعلومات التي يحتاجون إليها بالضبط ، ولا شيء أكثر من ذلك ، يبسط عملية تطوير واجهة برمجة التطبيقات مع مرور الوقت ويمنح المطورين أدوات قوية.
لا توجد العديد من مكتبات GraphQL الخاصة بـ Golang. على وجه الخصوص ، جربت مكتبات مثل
Thunder و
graphql و
graphql-go و
gqlgen . أود أن أشير إلى أن أفضل ما حاولت هو مكتبة gqlgen.
مكتبة gqlgen لا تزال في مرحلة تجريبية ، في وقت كتابة هذه المادة كانت النسخة
0.7.2 . المكتبة تتطور بسرعة.
هنا يمكنك معرفة المزيد عن خطط تطويرها. الآن الراعي الرسمي لـ gqlgen هو مشروع
99designs ، مما يعني أن هذه المكتبة ، على الأرجح ، ستتطور بشكل أسرع من ذي قبل. المطورين الرئيسيين لهذه المكتبة هم
vektah و
neelance ، بينما neelance ، بالإضافة إلى ذلك ، يعمل على مكتبة graphql-go.
دعونا نتحدث عن مكتبة gqlgen بناءً على افتراض أن لديك بالفعل معرفة أساسية بـ GraphQL.
ميزات Gqlgen
في وصف gqlgen ، يمكنك معرفة ما أمامنا هو مكتبة لإنشاء خوادم GraphQL مكتوبة بدقة في Golang. هذه العبارة تبدو لي واعدة للغاية ، حيث إنها تعني أنه عند العمل مع هذه المكتبة ، لن أواجه شيئًا مثل
map[string]interface{}
، حيث يتم استخدام نهج قائم على الكتابة الصارمة هنا.
بالإضافة إلى ذلك ، تستخدم هذه المكتبة أسلوبًا يعتمد على مخطط البيانات. هذا يعني أن واجهات برمجة التطبيقات موصوفة باستخدام
لغة تعريف مخطط GraphQL. تحتوي هذه اللغة على أدوات إنشاء التعليمات البرمجية القوية الخاصة بها والتي تنشئ رمز GraphQL تلقائيًا. في هذه الحالة ، يمكن للمبرمج فقط تنفيذ المنطق الأساسي لأساليب الواجهة المقابلة.
ينقسم هذا المقال إلى قسمين. الأول مخصص لأساليب العمل الأساسية ، والثاني للطرق المتقدمة.
طرق العمل الرئيسية: الإعداد ، طلبات استلام وتغيير البيانات ، الاشتراكات
نحن كتطبيق تجريبي ، سوف نستخدم موقعًا يمكن للمستخدمين من خلاله نشر مقاطع فيديو وإضافة لقطات شاشة ومراجعات والبحث عن مقاطع الفيديو وعرض قوائم السجلات المرتبطة بالسجلات الأخرى. لنبدأ العمل في هذا المشروع:
mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/
قم
schema.graphql
ملف مخطط البيانات التالي (
schema.graphql
) في الدليل الجذر للمشروع:
type User { id: ID! name: String! email: String! } type Video { id: ID! name: String! description: String! user: User! url: String! createdAt: Timestamp! screenshots: [Screenshot] related(limit: Int = 25, offset: Int = 0): [Video!]! } type Screenshot { id: ID! videoId: ID! url: String! } input NewVideo { name: String! description: String! userId: ID! url: String! } type Mutation { createVideo(input: NewVideo!): Video! } type Query { Videos(limit: Int = 25, offset: Int = 0): [Video!]! } scalar Timestamp
يصف نماذج البيانات الأساسية ، طفرة واحدة (
Mutation
، وصف لطلب تغيير البيانات) ، والتي تُستخدم لنشر ملفات فيديو جديدة على الموقع ، واستعلام واحد (
Query
) للحصول على قائمة بجميع ملفات الفيديو. اقرأ المزيد عن مخطط GraphQL
هنا . بالإضافة إلى ذلك ، أعلنا هنا أحد أنواع البيانات العددية الخاصة بنا. نحن غير راضين عن
أنواع البيانات القياسية الخمسة (
Int
و
Float
و
String
و
Boolean
و
ID
) الموجودة في GraphQL.
إذا كنت بحاجة إلى استخدام
schema.graphql
الخاصة ، يمكنك
schema.graphql
في
schema.graphql
(في حالتنا ، هذا النوع هو
Timestamp
) وتقديم تعريفاتهم في الكود. عند استخدام مكتبة gqlgen ، يجب عليك توفير طرق
gqlgen.yml
العددية وتكوين التعيين باستخدام
gqlgen.yml
.
تجدر الإشارة إلى أنه في أحدث إصدار من المكتبة كان هناك تغيير مهم واحد. وهي ، تمت إزالة التبعية على الملفات الثنائية المترجمة من ذلك. لذلك ، يجب إضافة ملف
scripts/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
(file
generated_video.go
):
type Video struct { ID string `json:"id"` Name string `json:"name"` User User `json:"user"` URL string `json:"url"` CreatedAt string `json:"createdAt"` Screenshots []*Screenshot `json:"screenshots"` Related []Video `json:"related"` }
هنا يمكنك أن ترى أن
ID
هو سلسلة ،
CreatedAt
هو أيضا سلسلة. يتم تكوين النماذج الأخرى ذات الصلة وفقا لذلك. ومع ذلك ، في تطبيقات حقيقية هذا ليس ضروريا. إذا كنت تستخدم أي نوع من بيانات SQL ، فأنت بحاجة ، على سبيل المثال ، إلى أن يكون حقل
ID
، وفقًا لقاعدة البيانات المستخدمة ، من
int64
int
أو
int64
.
على سبيل المثال ، أستخدم PostgreSQL في هذا التطبيق التجريبي ، لذلك بالطبع أحتاج إلى أن يكون حقل
ID
من النوع
int
و
CreatedAt
نوع
time.Time
. هذا يؤدي إلى حقيقة أننا بحاجة إلى تحديد نموذجنا الخاص وإخبار gqlgen أننا بحاجة إلى استخدام نموذجنا بدلاً من إنشاء نموذج جديد. فيما يلي محتويات ملف
models.go
:
type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` User User `json:"user"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` Related []Video }
نقول للمكتبة أنه ينبغي لها استخدام هذه النماذج (ملف
gqlgen.yml
):
schema: - schema.graphql exec: filename: generated.go model: filename: models_gen.go resolver: filename: resolver.go type: Resolver models: Video: model: github.com/ridhamtarpara/go-graphql-demo/api.Video ID: model: github.com/ridhamtarpara/go-graphql-demo/api.ID Timestamp: model: github.com/ridhamtarpara/go-graphql-demo/api.Timestamp
الهدف من كل هذا هو أن لدينا الآن
gqlgen.yml
Timestamp
مع طرق تنظيم
gqlgen.yml
ملف
gqlgen.yml
. الآن بعد أن قدم المستخدم السلسلة
UnmarshalID()
، تقوم الطريقة
UnmarshalID()
بتحويل هذه السلسلة إلى عدد صحيح. عند إرسال استجابة ، يقوم الأسلوب
MarshalID()
بتحويل الرقم إلى سلسلة. يحدث الشيء نفسه مع
Timestamp
أو مع أي نوع قياسي آخر أعلنه المبرمج.
الآن حان الوقت لتنفيذ منطق التطبيق. افتح ملف
resolver.go
وأضف أوصاف الطفرات والاستعلامات فيه. يوجد بالفعل رمز boilerplate تم إنشاؤه تلقائيًا نحتاج إلى ملئه بالمعنى. هنا هو رمز لهذا الملف:
func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { newVideo := api.Video{ URL: input.URL, Name: input.Name, CreatedAt: time.Now().UTC(), } rows, err := dal.LogAndQuery(r.db, "INSERT INTO videos (name, url, user_id, created_at) VALUES($1, $2, $3, $4) RETURNING id", input.Name, input.URL, input.UserID, newVideo.CreatedAt) defer rows.Close() if err != nil || !rows.Next() { return api.Video{}, err } if err := rows.Scan(&newVideo.ID); err != nil { errors.DebugPrintf(err) if errors.IsForeignKeyError(err) { return api.Video{}, errors.UserNotExist } return api.Video{}, errors.InternalServerError } return newVideo, nil } func (r *queryResolver) Videos(ctx context.Context, limit *int, offset *int) ([]api.Video, error) { var video api.Video var videos []api.Video rows, err := dal.LogAndQuery(r.db, "SELECT id, name, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2", limit, offset) defer rows.Close(); if err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } for rows.Next() { if err := rows.Scan(&video.ID, &video.Name, &video.URL, &video.CreatedAt, &video.UserID); err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } videos = append(videos, video) } return videos, nil }
الآن دعونا اختبار الطفرة.
طفرة خلقفيديوإنه يعمل! ولكن لماذا لا يوجد شيء في معلومات
user
(كائن
user
)؟ عند العمل مع GraphQL ، تكون مفاهيم مشابهة للتحميل "الكسول" (الكسول) و "الجشع" (حريصة) قابلة للتطبيق. نظرًا لأن هذا النظام قابل للتوسيع ، تحتاج إلى تحديد الحقول التي يجب ملؤها "جشعًا" وأيها "كسول".
اقترحت على الفريق في المؤسسة التي أعمل فيها "القاعدة الذهبية" التي تنطبق عند العمل مع gqlgen: "لا تدرج في النموذج الحقول التي تحتاج إلى تحميلها فقط إذا طلب العميل ذلك".
في حالتنا ، أحتاج إلى تنزيل بيانات حول مقاطع الفيديو ذات الصلة (وحتى معلومات المستخدم) فقط إذا طلب العميل هذه الحقول. ولكن بما أننا قمنا بإدراج هذه الحقول في النموذج ، فإن gqlgen يفترض أننا نقدم هذه البيانات عن طريق تلقي معلومات حول الفيديو. نتيجة لذلك ، الآن نحصل على هياكل فارغة.
يحدث أحيانًا أن هناك حاجة إلى نوع معين من البيانات في كل مرة ، لذلك من غير العملي تنزيلها باستخدام طلب منفصل. لهذا ، من أجل تحسين الأداء ، يمكنك استخدام شيء مثل صلات SQL. بمجرد أن (لا ينطبق هذا على المثال الموضح هنا) ، كنت بحاجة إلى تحميل بيانات التعريف الخاصة به مع الفيديو. تم تخزين هذه الكيانات في أماكن مختلفة. نتيجةً لذلك ، إذا تلقى نظامي طلبًا لتنزيل مقطع فيديو ، كان عليّ تقديم طلب آخر للحصول على بيانات التعريف. ولكن ، بما أنني كنت أعرف هذا المطلب (أي أنني كنت أعلم أن العميل والفيديو مطلوبان دائمًا من جانب العميل) ، فقد فضلت استخدام تقنية التحميل الجشع لتحسين الأداء.
دعونا إعادة كتابة النموذج وإنشاء رمز gqlgen مرة أخرى. من أجل عدم تعقيد القصة ، نكتب فقط طرقًا لحقل
user
(file
models.go
):
type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` UserID int `json:"-"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` }
أضفنا
UserID
User
وأزلنا بنية
User
. جدد الآن الكود:
go run scripts/gqlgen.go -v
بفضل هذا الأمر ، سيتم إنشاء طرق الواجهة التالية لحل الهياكل غير المحددة. بالإضافة إلى ذلك ، ستحتاج إلى تحديد ما يلي في محلل البيانات (ملف gener.go):
type VideoResolver interface { User(ctx context.Context, obj *api.Video) (api.User, error) Screenshots(ctx context.Context, obj *api.Video) ([]*api.Screenshot, error) Related(ctx context.Context, obj *api.Video, limit *int, offset *int) ([]api.Video, error) }
فيما يلي التعريف (ملف
resolver.go
):
func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { rows, _ := dal.LogAndQuery(r.db,"SELECT id, name, email FROM users where id = $1", obj.UserID) defer rows.Close() if !rows.Next() { return api.User{}, nil } var user api.User if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return api.User{}, errors.InternalServerError } return user, nil }
الآن ، سوف تظهر نتائج اختبار الطفرة كما هو موضح أدناه.
طفرة خلقفيديوما ناقشناه للتو هو أساسيات GraphQL ، بعد التمكن من ، يمكنك بالفعل كتابة شيء خاص بك. ومع ذلك ، قبل الانخراط في تجارب مع GraphQL و Golang ، سيكون من المفيد التحدث عن الاشتراكات ، والتي ترتبط ارتباطًا مباشرًا بما نقوم به هنا.
riptions الاشتراكات
يوفر GraphQL القدرة على الاشتراك في تغييرات البيانات التي تحدث في الوقت الحقيقي. تتيح مكتبة gqlgen ، في الوقت الحقيقي ، باستخدام مآخذ الويب ، العمل مع أحداث الاشتراك.
يلزم وصف الاشتراك في ملف
schema.graphql
. فيما يلي وصف الاشتراك في حدث نشر الفيديو:
type Subscription { videoPublished: Video! }
الآن ، قم بتشغيل توليد الشفرة التلقائي مرة أخرى:
go run scripts/gqlgen.go -v
كما سبق ذكره ، أثناء الإنشاء التلقائي للرمز في ملف
generated.go
، يتم إنشاء واجهة يجب تنفيذها في أداة التعرف. في حالتنا ، يبدو هذا (ملف
resolver.go
):
var videoPublishedChannel map[string]chan api.Video func init() { videoPublishedChannel = map[string]chan api.Video{} } type subscriptionResolver struct{ *Resolver } func (r *subscriptionResolver) VideoPublished(ctx context.Context) (<-chan api.Video, error) { id := randx.String(8) videoEvent := make(chan api.Video, 1) go func() { <-ctx.Done() }() videoPublishedChannel[id] = videoEvent return videoEvent, nil } func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) {
الآن ، عند إنشاء فيديو جديد ، تحتاج إلى تشغيل حدث. في مثالنا ، يتم ذلك في السطر
for _, observer := range videoPublishedChannel
.
الآن حان الوقت للتحقق من اشتراكك.
تحقق من الاشتراكلدى GraphQL ، بالطبع ، قدرات قيمة معينة ، ولكن كما يقولون ، ليس كل ما يلمع هو الذهب. أي أننا نتحدث عن حقيقة أن الشخص الذي يستخدم GraphQL يحتاج إلى رعاية التفويض وتعقيد الطلبات والتخزين المؤقت ومشكلة طلبات N + 1 وتقييد سرعة تنفيذ الاستعلام وبعض الأشياء الأخرى. خلاف ذلك ، قد يواجه نظام تم تطويره باستخدام GraphQL انخفاضًا خطيرًا في الأداء.
التقنيات المتقدمة: المصادقة ، تحميل البيانات ، تعقيد الاستعلام
في كل مرة أقرأ كتيبات مثل هذه ، أشعر أني أتقن كل شيء أحتاج إلى معرفته عن تقنية معينة والحصول على القدرة على حل المشكلات بأي تعقيد ، بعد إتقانها.
لكن عندما أبدأ العمل في مشاريعي الخاصة ، عادةً ما أواجه مواقف غير متوقعة تبدو وكأنها أخطاء في الخادم أو مثل الطلبات التي كانت تعمل على مر العصور ، أو مثل بعض حالات الجمود الأخرى. ونتيجة لذلك ، من أجل القيام بذلك ، يجب أن أتعمق في الحديث عما بدا مفهوما تمامًا. في هذا الدليل نفسه ، آمل أن يتم تجنب ذلك. هذا هو السبب في هذا القسم سنلقي نظرة على بعض التقنيات المتقدمة للعمل مع GraphQL.
▍ المصادقة
عند العمل مع REST API ، لدينا نظام مصادقة وأدوات تفويض قياسية عند العمل مع نقطة نهاية معينة. ولكن عند استخدام GraphQL ، يتم استخدام نقطة نهاية واحدة فقط ، وبالتالي ، يمكن حل مهام المصادقة باستخدام توجيهات المخطط. قم بتحرير ملف
schema.graphql
كما يلي:
type Mutation { createVideo(input: NewVideo!): Video! @isAuthenticated } directive @isAuthenticated on FIELD_DEFINITION
لقد أنشأنا التوجيه
createVideo
اشتراك
createVideo
. بعد جلسة إنشاء الشفرة التلقائية التالية ، تحتاج إلى تحديد تعريف لهذا التوجيه. الآن يتم تنفيذ التوجيهات في شكل أساليب الهياكل ، وليس في شكل واجهات ، لذلك نحن بحاجة إلى وصفها. لقد قمت بتحرير الكود الذي تم إنشاؤه تلقائيًا والموجود في ملف
server.go
وقمت بإنشاء طريقة تُرجع تكوين
server.go
لملف
server.go
. هنا هو ملف
resolver.go
:
func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, }
هنا هو الملف
server.go
:
rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db) ) ) http.Handle("/query", auth.AuthMiddleware(rootHandler))
نقرأ
ID
المستخدم من السياق. لا تجد هذا غريب؟ كيف دخل هذا المعنى في السياق ولماذا ظهر في السياق؟ الحقيقة هي أن gqlgen يوفر سياقات الطلب فقط على مستوى التنفيذ ، لذلك ليس لدينا طريقة لقراءة أي بيانات طلب HTTP ، مثل الرؤوس أو ملفات تعريف الارتباط ، في أدوات التعرف أو التوجيهات. نتيجة لذلك ، تحتاج إلى إضافة آليات الوسيطة الخاصة بك إلى النظام ، وتلقي هذه البيانات ووضعها في السياق.
نحتاج الآن إلى وصف آلية المصادقة الوسيطة الخاصة بنا للحصول على بيانات المصادقة من الطلب والتحقق منها.
لا يوجد منطق محدد هنا. بدلاً من ذلك ، للحصول على بيانات الترخيص ، لأغراض العرض التوضيحي ،
ID
ببساطة تمرير
ID
المستخدم هنا. ثم يتم دمج هذه الآلية في
server.go
مع طريقة تحميل التكوين الجديدة.
الآن وصف التوجيه المنطقي. لا نقوم بمعالجة طلبات المستخدم غير المصرح به في رمز الوسيطة ، حيث سيتم معالجة هذه الطلبات بواسطة التوجيه. هنا هو كيف يبدو.
العمل مع مستخدم غير مصرح بهالعمل مع مستخدم مصرح لهعند العمل مع توجيهات المخطط ، يمكنك حتى تمرير الوسائط:
directive @hasRole(role: Role!) on FIELD_DEFINITION enum Role { ADMIN USER }
رافعات البيانات
يبدو لي أن كل هذا يبدو مثيرا للاهتمام. يمكنك تنزيل البيانات عند الحاجة إليها. العملاء لديهم القدرة على إدارة البيانات ؛ بالضبط ما هو مطلوب يؤخذ من التخزين. ولكن كل شيء له ثمن.
ما هو الثمن الذي يجب دفعه مقابل هذه الفرص؟ ألقِ نظرة على سجلات التنزيل لجميع مقاطع الفيديو. وهي نتحدث عن حقيقة أن لدينا 8 مقاطع فيديو و 5 مستخدمين.
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 $2 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1
ما الذي يحدث هنا؟ لماذا هناك 9 طلبات (يرتبط طلب واحد بجدول الفيديو و 8 - بجدول المستخدم)؟ يبدو مروعا. توقف قلبي تقريبًا عندما ظننت أن واجهة برمجة التطبيقات الحالية الخاصة بنا يجب استبدالها بهذا ... صحيحًا ، يمكن لوادر تحميل البيانات التعامل مع هذه المشكلة تمامًا.
يُعرف هذا بمشكلة N + 1. نحن نتحدث عن حقيقة أن هناك استعلامًا واحدًا للحصول على جميع البيانات ، ولكل جزء من البيانات (N) سيكون هناك استعلام آخر إلى قاعدة البيانات.
هذه مشكلة خطيرة للغاية عندما يتعلق الأمر بالأداء والموارد: على الرغم من أن هذه الطلبات متوازية ، إلا أنها تستنزف موارد النظام.
لحل هذه المشكلة ، سوف نستخدم مكتبة
dataloaden من مؤلف مكتبة gqlgen. تسمح لك هذه المكتبة بإنشاء كود Go. أولاً ، قم بإنشاء محمل بيانات لكيان
User
:
go get github.com/vektah/dataloaden dataloaden github.com/ridhamtarpara/go-graphql-demo/api.User
لدينا تحت
userloader_gen.go
، والذي يحتوي على طرق مثل
Fetch
و
LoadAll
و
Prime
.
الآن ، من أجل الحصول على نتائج عامة ، نحتاج إلى تحديد طريقة
Fetch
(ملف
dataloader.go
):
func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userloader := UserLoader{ wait : 1 * time.Millisecond, maxBatch: 100, fetch: func(ids []int) ([]*api.User, []error) { var sqlQuery string if len(ids) == 1 { sqlQuery = "SELECT id, name, email from users WHERE id = ?" } else { sqlQuery = "SELECT id, name, email from users WHERE id IN (?)" } sqlQuery, arguments, err := sqlx.In(sqlQuery, ids) if err != nil { log.Println(err) } sqlQuery = sqlx.Rebind(sqlx.DOLLAR, sqlQuery) rows, err := dal.LogAndQuery(db, sqlQuery, arguments...) defer rows.Close(); if err != nil { log.Println(err) } userById := map[int]*api.User{} for rows.Next() { user:= api.User{} if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return nil, []error{errors.InternalServerError} } userById[user.ID] = &user } users := make([]*api.User, len(ids)) for i, id := range ids { users[i] = userById[id] i++ } return users, nil }, } ctx := context.WithValue(r.Context(), CtxKey, &userloader) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) }
هنا ننتظر 1 مللي ثانية. قبل تنفيذ الطلب وجمع الطلبات في حزم تصل إلى 100 طلب. الآن ، بدلاً من تنفيذ طلب لكل مستخدم على حدة ، سينتظر المُحمل الوقت المحدد قبل الوصول إلى قاعدة البيانات. بعد ذلك ، تحتاج إلى تغيير منطق أداة التعرّف من خلال إعادة تكوينه باستخدام طلب استخدام أداة تحميل البيانات (ملف
resolver.go
):
func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { user, err := ctx.Value(dataloaders.CtxKey).(*dataloaders.UserLoader).Load(obj.UserID) return *user, err }
إليك كيف تبدو السجلات بعد ذلك في موقف مشابه للحالة الموضحة أعلاه:
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5)
يتم تنفيذ استعلامات قاعدة بيانات اثنين فقط هنا ، نتيجة لذلك ، الكل سعيد الآن. من المثير للاهتمام ملاحظة أنه يتم إرسال 5 معرّفات مستخدمين فقط إلى الطلب ، على الرغم من أن البيانات مطلوبة من أجل 8 مقاطع فيديو. هذا يشير إلى أن أداة تحميل البيانات تزيل السجلات المكررة.
▍
GraphQL API , . , API DOS-.
, .
Video
, . GraphQL
Video
. . — .
, — :
{ 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 } } } } }
100, . (, , ) , .
gqlgen , . , (
handler.ComplexityLimit(300)
) GraphQL (300 ). , (
server.go
):
rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db)), handler.ComplexityLimit(300) ), )
, , . 12. , , , ( , , , , ).
resolver.go
:
func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, }
, , .
, ,
related
. , , , , .
النتائج
, ,
GitHub . . , , .
أعزائي القراء! GraphQL , Go?
