نشرح في هذا المقال مفهوم GraphQL بالتفصيل وكيفية استخدامه تحسين أداء واجهة برمجة التطبيقات الخاصة بك، وتكتشف أبرز مزايا وعيوب استخدامه وتتمكن من تحديد فيما إذا كانت هي الأداة الأفضل لمشروع واجهة برمجة التطبيقات الخلفية الخاصة بك أم لا.
فإذا كنت تفكر في تصميم واجهة برمجة تطبيقات خلفية ولديك فكرة مسبقة بواجهة برمجة التطبيقات REST فستكون على دراية أيضًا بأن تقنية REST ليست مناسبة للتطبيقات المعقدة وقد تواجهك بعض الصعوبات في التعامل معها لاسيما عند الحاجة إلى الإجابة على الأسئلة التالية:
- ما مقدار البيانات التي يجب أن تعيدها مع كل نقطة وصول؟
- هل ستعيد نقطة الوصول نفس البيانات، سواء على الأجهزة المحمولة أو على أجهزة سطح المكتب؟
- هل ترغب في إجراء تحديثات فورية في الوقت الفعلي إذا تغير شيء ما؟ لذا من الأفضل لك في هذه الحالة التعامل مع GraphQL.
مشكلات استخدام معمارية REST
المصطلح REST هو اختصار لعبارة Representational state transfer أي نقل الحالة التمثيلية، وهو أسلوب لتصميم واجهات برمجة التطبيقات APIs، تنظم نقاط الوصول endpoints في معمارية REST بالاعتماد على مجموعة من الموارد كالمستخدمين والتعليقات وغيرها بحيث تتيح لك كل نقطة الوصول إلى مجموعة معينة من البيانات أو تمكنك من إجراء عملية معينة عليها. وبالرغم من كون معمارية REST مستخدمة من فترة طويلة، ولا تزال مستخدمة حتى اليوم على نطاق واسع لكتابة واجهات برمجة التطبيقات، إلا أنها تعاني من مشكلة رئيسية وهي تحديد البيانات التي يتوجب إعادتها بالضبط بواسطة نقاط الوصول الخاصة بك. فقد تواجه حالة تستلزم منك على سبيل المثال عرض قائمة بالمستخدمين، وقائمة بأحدث تعليقات كتبها كل مستخدم من هؤلاء المستخدمين، بمعنى آخر، ستحتاج في هذه الحالة لجلب قائمة من الكيانات، وجلب قائمة أخرى مرتبطة بكل كيان، وهناك طريقتان لحل هذه المشكلة باستخدام REST سنوضحهما تاليًا.
نمذجة نقاط الوصول بالاعتماد على الموارد
إن وجود نقطة وصول واحدة لكل مورد أو مصدر بيانات يسهل على المطورين صيانة وتطوير الواجهة الخلفية، ولكنه يعقد الأمور من طرف العميل، فالعميل سيحتاج إلى إجراء طلبات متعددة لواجهة برمجة التطبيقات API لجلب البيانات المطلوبة. على سبيل المثال لجلب كافة المستخدمين والتعليقات الخاصة بكل مستخدم يمكن أن يكون لديك نقاط الوصول التالية:
-
getUsers()
نقطة وصول تعيد لنا قائمة بكافة المستخدمين -
getCommentsByUserId(userId: string)
نقطة وصول تعيد قائمة بالتعليقات المرتبطة بمعرف مستخدم محدد.
لكن يمكن أن ينتج عن استخدام هذا الأسلوب مشكلة شائعة تعرف بمشكلة N + 1، حيث يحتاج العميل إلى استدعاء الخادم N + 1 مرة لجلب مجموعة واحدة من الموارد الأساسية، وجلب N مجموعة من الموارد الفرعية. يجلب الكود التالي قائمة المستخدمين، ثم يجلب التعليقات لكل مستخدم عن طريق استدعاءات HTTP إضافية. ومع أنه يعمل بشكل صحيح، فإنه يعاني من مشكلة الأداء، فاستدعاء التعليقات لكل مستخدم يتم بشكل متسلسل synchronous بدلاً من كونه متوازيًا parallel مما يتسبب في زمن انتظار طويل بسبب كل استدعاء منفصل.
const getUserWithComments = async (): Promise<User[]> => { // fetch a list of users let users = await getUsers(); // new HTTP request for each user for(let i = 0; i < users.length; i++){ const user = users[i]; // loading comments from the server user.comments = await getCommentsByUserId(user.id); } return users; }
نعم كما تتوقع، ستبطئ نقاط الوصول المعتمدة على الموارد من عملية عرض البيانات فتحميل كافة البيانات يستغرق وقتًا طويلًا، ويزيد الحمل على الخادم.
إرجاع الشكل الدقيق للبيانات المطلوبة
يمكن حل مشكلة زيادة حمل الخادم وبطء عرض البيانات بتنظيم نقاط الوصول بحيث تعيد البيانات الدقيقة التي يريد العميل التعامل معها بالضبط، بدل تحميل كافة البيانات. فقد يكون لديك نقطة وصول مثل ()getUsersWithComments
تعيد المستخدمين مع تعليقاتهم من خلال استدعاء واحد فقط عبر بروتوكول HTTP.
يتطلب هذا الأسلوب وجود صلة وثيقة بين العميل والخادم، فكلما احتجنا لإجراء تغيير ما في واجهة المستخدم من أجل عرض بيانات إضافية أو إزالة بيانات معينة، سيتعين على مطوري الواجهة الأمامية في هذه الحالة التواصل مع مطوري الواجهة الخلفية من جديد من أجل ضبط البيانات المعادة من نقطة الوصول المحددة.
لكن التعامل بهذا الأسلوب صعب أيضًا، لاسيما في حال وجود عدة نقاط وصول تعيد كل منها أنواعًا مختلفة من البيانات، أو في حال وجود نقاط نهاية منفصلة مخصصة للهواتف المحمولة، وقد ينجم عنه وجود العديد من نقاط الوصول التي تعيد مجموعة من الكائنات، وعدم معرفة ما هي النقاط المستخدمة فعليًا.
هل GraphQL خيار مناسب؟
في حال تفويض مسؤولية تحديد شكل البيانات المعادة إلى الخادم، سيصادف مطور الواجهة الأمامية واحدة من المشكلات السابقة في مرحلة ما من مراحل العمل على المشروع، وقد يصادف مشكلات إضافية عند استخدام معمارية REST مثل:
- الحاجة لإرسال بيانات أقل لمستخدمي الهواتف المحمولة
- الحاجة لوجود تواصل في الزمن الحقيقي بين الخادم والعميل لتحديث البيانات بشكل فوري
- نقص وجود وثائق واجهة برمجة التطبيقات API توضح كيفية استخدامها بشكل صحيح وبسبب وجود مثل هذه المشكلات عند التعامل مع معمارية REST ظهرت الحاجة لابتكار حلول أفضل مثل GraphQL
ما هي GraphQL؟
GraphQL هي لغة استعلام توكل مسؤولية تحديد شكل البيانات المعادة من الخادم إلى جانب العميل، وتوفر للخادم مساحة للعمل playground، ووثائق لواجهات برمجة التطبيقات API الخاصة به، كما تتيح للعميل الاطلاع على جميع التوابع المتاحة القابلة للتنفيذ executable ، كما تمكنه من استخدام الاستعلامات للحصول على بيانات محددة، أو تعديل البيانات، كما توفر ميزة تسمى الاشتراكات subscriptions التي تسهل التواصل في الزمن الحقيقي.
فعند استدعاء تابع من طرف الخادم، سيُضمَّن مع هذا الطلب هيكل بيانات جيسون JSON في حمولة الرسالة، وسيكون دور بيانات جيسون وصف كافة الحقول الخاصة بالكائن المطلوب إرجاعه، ويتوجب على الخادم أن يلبي الطلب ويعيد قيم الحقول المحددة ضمن هذا الطلب.
بهذه الطريقة يحل GraphQL مشكلة معرفة ما هي البيانات التي يرغب العميل في استردادها ويحددها بدقة داخل الطلب الذي يُرسله إلى الخادم ويجلب له هذه البيانات بالضبط دون زيادة أو نقصان، إذَا يحدد العميل بدقة ما هي الحقول التي يريد الحصول عليها في طلب HTTP، ويسترجع هذه البيانات من الخادم فقط.
العلاقات في نماذج GraphQL
تُمثل النماذج في models في GraphQL العلاقات بين الكائنات، وتتيح لك الاستعلام عن البيانات المتشابكة بكفاءة عبر استعلام واحد. فعند تمكين العلاقات بين النماذج في GraphQL ستتضمن الاستعلامات معلومات عن العلاقات بين الكيانات وتمكنك من استرجاع كافة البيانات المطلوبة في طلب HTTP واحد ينتقل مرة واحدة ذهابًا وإيابًا.
ما نقصده هنا بنمذجة العلاقة أنه إذا كان لدى كيان ما علاقة واحد إلى متعدد 1 إلى N مع كيان آخر، فيمكن طلب جميع البيانات المرتبطة بكيان معين في طلب واحد فقط مرسل إلى الخادم، وبهذا يمكنك الحصول على كافة البيانات المطلوبة دون الحاجة لإجراء عدة طلبات، وهذا يجعل استخدام GraphQL مفيدًا للاستعلام عن البيانات بشكل فعال وبسيط، خاصة عندما يكون هناك علاقات معقدة بين الكائنات.
المحللات Resolvers في GraphQL
عندما ترسل من العميل إلى خادم GraphQL، يحتاج الخادم إلى طريقة لمعرفة كيفية جلب البيانات المطلوبة، وتستخدم GraphQL تقنية المحللات Resolvers وهي دوال أو وظائف تحدد طريقة استجابة الخادم لاستعلامات GraphQL وهي لبنة البناء الرئيسية التي تجعلنا نفكر في استخدام GrapQL في المقام الأول. فالمحللات Resolvers هي المسؤولة عن ملء وتعبئة البيانات لحقل واحد في نوع الكائن، تستدعى المحللات لكل حقل في استعلام GraphQL، وتكون مسؤولية المحلل تحديد الطريقة المناسبة للحصول على البيانات المطلوبة وإرجاعها بالصيغة الصحيحة بناءً على العلاقات بين حقول الكائنات.
يمكن فهم المحللات Resolvers بأنها تحسين لتقنية GraphGL تسمح بإضافة حقول إضافية لفئة الكيانات إذا لم تكن متواجدة في قاعدة البيانات ولكن يتم حسابها على الخادم. لنفترض أن لدينا نموذج بيانات يحتوي على المستخدمين وتعليقاتهم، ولكن بعض المعلومات التي نحتاج إليها قد لا تكون مخزنة مباشرة في قاعدة البيانات، ويتعين حسابها على الخادم.
على سبيل المثال، قد يكون لدينا في قاعدة البيانات جداول لكل من المستخدمين وتعليقاتهم لكن لا يوجد حقل مباشر في جدول المستخدمين يخزن عدد التعليقات التي كتبها كل مستخدم، في هذه الحالة، يمكن استخدام المحللات Resolvers لحساب عدد التعليقات عند الطلب. وقد نحتاج أيضًا إلى حقل يحتوي على تفاصيل مستخدم معين مثل عنوان المستخدم والذي قد لا يكون مخزنًا مباشرة في قاعدة البيانات وفي هذه الحالة يمكن استخدام محلل Resolver لجلب هذا العنوان من مصدر خارجي.
إذا طورنا نظام GraphQL باستخدام إطار عمل NestJS على سبيل المثال، فيمكن في هذه الحالة تحديد الدوال المسؤولة عن حساب هذه المعلومات أو جلبها باستخدام دوال مسجلة على كمحللات Resolvers وتكمن فائدة المحللات في آلية تنفيذها، إذ لن تنفذ أي دالة مسجلة كمحلل Resolver حتى تستدعى في طلب GraphQL.
يعرض الكود التالي جزء من تطبيق يستخدم GraphQL وإطار عمل NestJS. وهو يعبر عن جزءًا من مخطط قاعدة البيانات UML السابق ويتضمن تعريفًا لصنف باسم Movie
ومحلل Resolver لحقل التعليقات MovieComment
يحدد أن الحقل الذي سيعاد هو بيانات التعليقات عن الفيلم بالاعتماد على المعرف id
الخاص بكل فيلم.
@ObjectType() export class Movie { @Field(() => Int) id: number; @Field(() => String) createdAt: Date; @Field(() => String) updatedAt: Date; @Field(() => String, { nullable: false, description: "User's title to the movie", defaultValue: '', }) title: string; @Field(() => String, { nullable: true, description: "User's description to the movie", }) description: string; } @Resolver(() => Movie) export class MovieResolver { constructor(private movieCommentService: MovieCommentService) {} @ResolveField('movieComment', () => [MovieComment]) async getMovieComment(@Parent() movie: Movie) { const { id } = movie; return this.movieCommentService.getAllMovieCommetsByMovieId(id); } }
` تكمن فائدة هذا الكود في توفير واجهة GraphQL تتيح للعميل الحصول على بيانات الفيلم وتعليقاته بشكل فعال، وبهذا يمكن للعميل الاستعلام عن الفيلم والتعليقات حوله في نفس الاستعلام دون الحاجة لطلب استعلامات متعددة.
يتطلب تحميل تعليقات كائن الفيلم عادة إجراء عملية استعلام مكلفة على قاعدة البيانات. أضف إلى ذلك فأنت لا تحتاج دائمًا إلى تحميل التعليقات الخاصة بالفيلم، وإنما تحتاج لتحميلها فقط عندما يطلبها المستخدم. لذا يفيد وضع المزخرف ResolveField@
قبل اسم الدالة في إطار العمل NestJS إلى تنفيذ المنطق الداخلي لهذه الدالة فقط عندما يطلب العميل بيانات لاسم الحقل movieComment
.
ملاحظة: يجب أن يوفر منشئ واجهة برمجة التطبيقات API في حال استخدام المحللات وصفًا لكافة الحقول التي توفرها هذه المحللات، لاسيما تلك التي تنفذ عمليات مكلفة بالنسبة للخادم، مثل الاستعلامات من قاعدة البيانات، كي لا يطلب المستخدمون حقولًا غير ضرورية دون داعٍ.
فوائد استخدام GraphQL
كما شرحنا سابقًا في حال استخدام واجهة برمجة تطبيقات REST API يكون السؤال الدائم ما هي البيانات التي ترجعها كل نقطة وصول؟ ويمكن توفير إجابة على هذا السؤال إما من خلال اختبار نقاط الوصول بواسطة أداة Postman التي تسمح بإنشاء طلبات HTTP مخصصة وإرسالها إلى خوادم الويب وتلقي الردود لاختبار الواجهات البرمجية، أو من خلال توفير إطار عمل Swagger لتوثيق كل ما يتعلق بالواجهات الخلفية للتطبيقات.
لا تضمن لك هذه الحلول معرفة البيانات المعادة من قبل كل نقطة وصول، فقد تكون البيانات المعادة من نقاط النهاية غير معرفة وغير واضحة، وقد تختلف عن الوثائق أو التوقعات.
يقدم استخدام GraphQL لك حلًا أفضل ويوفر تجربة تطوير محسنة والعديد من المميزات وأهمها:
- ميزة GraphQL Playground التي توفر بيئة تفاعلية لتجريب استعلامات GraphQL
- توفير مخطط مسبق التعريف Predefined schema
- إنشاء أنواع TypeScript تلقائيًا استنادًا إلى مخطط بيانات GraphQL
- التواصل في الزمن الحقيقي
- التكيف مع مختلف أنواع الأجهزة
- تنفيذ عدة استعلامات دفعة واحدة Batch query execution
سنشرح تباعًا كل ميزة من هذه الميزات بالتفصيل، ولعل أهم مميزات GraphQL هي ميزة فصل التبعية dependency بين الخادم والعميل، حيث يتلقى العميل البيانات التي يطلبها في استعلام GraphQL المرسل للخادم فقط، ولا يتلقى أي بيانات غير مطلوبة، فحتى إن كان لدى الخادم هيكلية بيانات معقدة للغاية تتضمن أكثر من 60 خاصية يمكن الاستعلام عنها، فلن يتلقى العميل سوى قيم الحقول التي تضمنها استعلام GraphQL وهذا سيقلل كمية البيانات المرسلة ويزيد كفاءة وسرعة استجابة الطلبات.
ميزة بيئة العمل التفاعلية في GraphQL
توفر GraphQL ميزة البيئة التفاعلية Playground وهي بيئة تطوير متكاملة طورتها شركة Prisma لتوفر بيئة لتنفيذ وتجربة الاستعلامات بشكل فوري، كما توفر وثائق للواجهات البرمجية API لكافة الاستعلامات عن البيانات Queries وتعديلها Mutations واشتراكاتها Subscriptions.
توفر هذه البيئة إمكانية فحص نوع البيانات التي يرجعها كل طلب، مما يسهل التأكد من صحة الاستجابة. كما يمكن اختبار وظائف الخادم مباشرة باستخدام واجهة GraphQL، مثل واجهة SpaceX مفتوحة المصدر مما يسهل تطوير التطبيقات التي تستخدم واجهات API ويوفر وثائق تعزز فهم كيفية استخدام API بشكل صحيح وكتابة الاستعلامات والاستفادة من البيانات المتاحة بالشكل الأمثل.
المخطط مسبق التعريف Predefined Schema
عند فحص أنواع بيانات GraphQL، قد تلاحظ أن بعض الحقول تتضمن علامة تعجب !
وهي بمثابة ضمان بأن الخادم يجب أن يوفر نوع البيانات الصحيح ولا يمكنه إرجاع قيمة خالية null
لهذا الحقل.
بإلقاء نظرة على الصورة المرفقة التي تستعلم عن حقول مثل User.id
و User.createdAt
، لن يرجع الخادم أبدًا قيمة null
لهذه الحقول، وفي حال حدث هذا الأمر فستتلقى الخطأ التالي من عملية الاستعلام.
{ "errors": [ { "message": "Cannot return null for non-nullable field User.id.", "statusCode": 500 } ], "data": null }
وعند استخدام علامة التعجب الخارجية مع المصفوفات ![ ]
كما في المثال التالي:
movieCommentsUserLeft: [MovieComment!]!،
فهذا يعني أن واجهة برمجة التطبيقات API ستعيد دومًا قائمة من القيم، وقد تكون إحدى هذه القيم null
لكن لن تكون القيمة النهاية المعادة null
أبدًا. من ناحية أخرى، تدل علامة التعجب الداخلية مع المصفوفات مثل [MovieComment!]
بأن العناصر الموجودة داخل القائمة ستكون دائمًا كائنات. ومن المستحيل أن تكون القيمة الخالية null
جزءًا من القائمة وإذا حدث ذلك، فسيفشل الاستعلام بأكمله، ولن يعيد أي بيانات.
توليد أنواع TypeScript
من الفوائد الرئيسية لمخطط GraphQL تمكين المطورين من تحديد استعلامات GraphQL واستخدام المكتبات المفيدة، مثل مكتبة توليد الكود graphql-code-generator، التي تسمح بإنشاء أنواع TypeScript من المخطط من جانب الخادم. ويؤدي استخدام مكتبة توليد الكود إلى منع الكتابة اليدوية لواجهات TypeScript لجميع استجابات واجهة برمجة التطبيقات API، وإذا تغير المخطط من جانب الخادم، فيمكن إعادة إنشاء المخطط بالكامل باستخدام أمر واحد فقط.
في الصورة أدناه، نرى مثالًا على هذه الميزة. فعلى اليسار، يوجد استعلام launchesPast الذي يتوقع إرجاع جزء يسمى LaunchLast. وعلى اليمين، نرى أنواع TypeScript التي أنشأتها المكتبة graphql-code-generator.
فباستخدام الأجزاء GraphQL fragments، يمكننا إعادة استخدام أجزاء من الاستعلامات والتعديلات في أماكن متعددة طالما كانت سترجع نفس نوع البيانات. وهذا يسهل علينا كتابة الاستعلامات وصيانتها. كما يمكننا استخدام الأجزاء المولدة لفحص الأنواع على جانب العميل للتأكد من توافق البيانات المستلمة مع الأنواع المتوقعة.
التواصل في الزمن الحقيقي
في حال استخدام معمارية REST التقليدية، ستحتاج لاستخدام WebSockets لتحقيق الاتصال في الوقت الفعلي كي تعرف باستمرار ما هي نقطة الوصول التي يجب أن تستمع إليها، وما هي البيانات التي ستحصل عليها، وهل الاتصال نشط أم لا، بدل الاستعلام المتكرر عن البيانات من الخادم، يمكن استخدام تقنية تسمى الاشتراكات للاستماع إلى الأحداث المنبعثة غير المتزامنة، حيث تحافظ الاشتراكات على اتصال نشط بالخادم، وتستمع وتنتظر حتى يقوم الخادم بإرسال التحديثات.
الفائدة الرئيسية لاستخدام الاشتراكات في GraphQL هي زيادة الأمان. فهي تتيح للمستخدم تحديد الحقول التي سيستقبل فيها القيم المحدثة، تمامًا كما يحدث في الاستعلامات. وهذا يسمح بتقليل البيانات غير المرغوب فيها و غير الضرورية ويجعل التفاعل مع البيانات أكثر فعالية.
التوافق مع مختلف أنواع الأجهزة
عند العمل على جهاز ذي شاشة صغيرة، مثل الهاتف المحمول، لن نحتاج في الغالب لعرض كمية كبيرة من البيانات مقارنة بجهاز ذي شاشة كبيرة مثل حاسوب مكتبي أو محمول، لذا ليس من الضروري استرجاع البيانات بكمية كبيرة في الشاشات الصغيرة.
الحل المقترح في هذه الحالة هو إنشاء استعلامين منفصلين لنفس العملية، أحدهما لجهاز سطح المكتب يسترجع كمية كبيرة من البيانات والثاني للهاتف المحمول يسترجع بيانات أقل واستخدام العبارات الشرطية لتنفيذها بحسب جهاز العميل.
تنفيذ عدة استعلامات بنفس الوقت
تتيح لك واجهة برمجة التطبيقات GraphQL تنفيذ عدة استعلامات في وقت واحد عن طريق إرسال طلب HTTP واحد. على سبيل المثال في حال واجهة برمجة التطبيقات SpaceX، يمكنك تحديد عدة استعلامات مستقلة واسترجاع البيانات في طلب واحد كما في المثال التالي:
{ launchesPast(limit: 3) { mission_name launch_date_local launch_site { site_name } } historiesResult(limit: 3) { data { details } } missions(limit: 3) { name twitter } }
سلبيات GraphQL
لا تخلو أي تقنية من بعض الجوانب السلبية. دعونا نلقي نظرة على بعض الاعتبارات التي تحتاج للانتباه لها عند استخدام معمارية GraphQL.
مشكلة N+1
عندما تبدأ في تعلم GraphQL، فإن أول مشكلة ستسمع عنها هي مشكلة N+1 ولتفهم كيفية حدوث هذه المشكلة لنأخذ المثال التالي حول واجهة برمجة التطبيقات SpaceX فإذا كنت تريد استرجاع الإصدارات السابقة من جدول واحد، وتريد الحصول على اسم الموقع لكل منها من جدول آخر باستخدام REST، قد تكون لديك نقطة وصول كما يلي:
route: '/launches/past', method: 'GET', queryParam: 'limit'
هذا يمكن أن يؤدي إلى الاستعلامات التالية في قاعدة البيانات:
SELECT * FROM launches limit = {limit}; -- pass the fetched launches.ids the the second query SELECT * FROM launch_sites WHERE launch_id in (1, 2, 3);
يمكنك الحصول على النتيجة المطلوبة باستخدام استعلامين. لكن انتبه لأن هذا يعمل لأنك قمت بالفعل بتحميل قائمة من الإصدارات، ومررت معرفات الإصدارات إلى الاستعلام الثاني. وفي حال GraphQL، لتحميل launch_sites
من جدول مختلف، يحتمل أن تستخدم ملحقة بكيان Launch، كما في الكود التالي.
type Query { launchesPast(limit: Int!): [Launch!]! } # from table launches type Launch { id: Int! mission_name: String launch_site: [Launch_sites!]! } # from table launch_sites type Launch_sites { id: Int! site_name: String } }` resolvers = { Query: { launchesPast: async (_: null, args: { limit: number }) => { return ORM.getAllLaunches(limit) } } Launch: { launch_site: async (launchObj, args) => { return ORM.getLaunchSitesById(launchObj.id) } }, }
في هذه الحالة، ستجري استعلامات متعددة للحصول على اسم الموقع لكل إصدار من قاعدة البيانات، لأن كل محلل في GraphQL يعرف فقط الآباء المرتبطين به، وستنفذ وظيفة محلل منفصلة لكل حقل.
بمعنى آخر، تعالج GraphQL كل حقل من البيانات بشكل منفصل بناء على العلاقات بين الحقول، مما يتطلب تنفيذ استعلامات متعددة للحصول على المعلومات المطلوبة بشكل صحيح.
query LaunchesPastQuery($limit: Int!) { launchesPast(limit: $limit) { # fetches Launch entities (1 query) id mission_name launch_site { # fetches Sites for each Launch (N queries for N Launches) site_name } } } # Therefore = N + 1 round trips
سيترجم الكود أعلاه إلى كود SQL الزائف التالي:
SELECT * FROM launches limit = {limit}; -- pass the fetched launches.ids the the second query SELECT * FROM launch_sites WHERE launch_id in (1); SELECT * FROM launch_sites WHERE launch_id in (2); SELECT * FROM launch_sites WHERE launch_id in (3);
تتفاقم مشكلة N+1 بشكل أكبر في حال معمارية GraphQL إذ لا يمكن للعملاء أو الخوادم التنبؤ بمدى تكلفة الطلب حتى يتم تنفيذه. أما في معمارية REST، فيمكن التنبؤ بالكلفة نظرًا لوجود رحلة واحدة مطلوبة لكل نقطة وصول. في حين توجد نقطة وصول واحدة في GraphQL لكنها لا تملك دلالة على الحجم المحتمل للطلبات الواردة إليها.
قدمت فيسبوك حلاً لمشكلة N+1 من خلال مكتبة تسمى DataLoader، وهي مكتبة مخصصة لجافا سكريبت تجمع الطلبات معًا. وأنشأت شوبيفاي مكتبة مشابهة باسم GraphQL Batch Ruby. تعمل هذه المكتبات عن طريق الانتظار حتى ينتهي تحميل كافة البيانات في مفاتيحها، ثم تنفذ طلب واحد إلى قاعدة البيانات بدلاً من إجراء طلبات متعددة. وبهذا يساعد تكديس الطلبات على تحسين الأداء وتقليل عدد الاستعلامات المنفذة.
تبعية المخطط مسبق التعريف
إن وجود مخطط محدد مسبقًا بين الخادم والمستهلك يمكن أن يكون مفيدًا، فهذا المخطط سيحدد لنا البيانات المتوقعة بشكل واضح، لكن عند استخدام علامة التعجب !
في حقول الكتابة لمعرفة الحقول التي لن تحتوي أبدًا على قيمة خالية null
فيمكن في هذه الحالة لعلامات التعجب أو القيم غير الخالية أن تتسبب في فشل الاستعلام بأكمله.
لنأخذ مثالاً على هذا الاستعلام عن الإصدارات، فعمود المعرف id
المحفوظ في قاعدة البيانات، أو أي عمود يجب أن يحتوي على قيمة سيصبح فجأة فارغًا.
schema = `{ type Query { launchesPast: [Launch!]! } # from table launches type Launch { id: Int! mission_name: String } }` resolvers = { Query: { launchesPast: async () => { const allLaunches = await ORM.getAllLaunches() allLaunches[3].id = null; // <-- will cause the query to fail return allLaunches; } } }
فإذا طلب المستخدم قائمة بالإصدارات من خلال استعلام LaunchPast، فسيفشل الاستعلام بأكمله وستحصل على رسالة الخطأ التالية التي تشير لأنه لا يمكن إرجاع قيمة خالية للحقل Launch.id:
Cannot return null for non-nullable field Launch.id
أخيرًا، إذا استعلمت عن قائمة من الكائنات، وكان واحد منها فقط لا يتطابق مع المخطط مسبق التعريف لكونه يحتوي على قيمة خالية null
لحقل مسبوق بعلامة التعجب !
، فسيفشل استعلامك بالكامل، ولن تتلقى عندها أي بيانات.
التعاودية المتداخلة Nested Recursion
نظرًا لكون GraphQL لغة استعلام لواجهات برمجة التطبيقات، فهي توفر القدرة على تنفيذ استعلامات متداخلة وتمكن المطورين من الحصول على ما يحتاجونه من بيانات بالضبط. ولكن ماذا لو أرسل العميل استعلامًا يطلب فيه العديد من البيانات المتداخلة كما في المثال التالي:
authors { name books { title authors { name books { title authors { name } } } } }
لاشك أن المشكلة الناجمة عن هذا المثال واضحة هنا، لذا يجب تصميم واجهة برمجة تطبيقات GraphQL بعناية لمنع تحميل بيانات متداخلة، كما ينصح باستخدام مكتبات مثل graphql-cost-analysis لتعيين حد الكلفة العظمى maximumCost
لمخططك بالكامل وتحديد مقدار التعقيد complexity
لكل عملية.
رموز حالة HTTP
عندما يواجه الخادم Apollo Server أخطاء معينة أثناء معالجة استعلامات GraphQL، ستتضمن الاستجابة التي يعيدها للعميل مصفوفة أخطاء تحتوي على كل خطأ وقع. وبالرغم من حدوث خطأ، فإن رمز الحالة لكل طلب سيكون دائمًا 200 مما يعني استلام الطلب بنجاح. ومع ذلك، ستتضمن الحمولة المعادة returned payload قائمة بالأخطاء التي حدثت مع statusCode
يساوي 500.
وقد لا يكون رمز الخطأ 500 أفضل طريقة لإخبار العميل بحدوث خطأ ما، لذا قد ترغب باستخدام رموز الحالة خاصة بالطلبات غير المصرح بها، وأخرى خاصة بعمليات إعادة التوجيه.
توسيع نطاق الخادم من خلال الاشتراكات
كما شرحنا سابقًا، تعتبر الاشتراكات Subscriptions في GraphQL ميزة مهمة للتواصل مع الخادم في الزمن الحقيقي، فهي تسمح لك بتلقي تحديثات فورية من الخادم عندما تحدث تغييرات. وهذه الميزة مفيدة بشكل خاص للتعامل مع الأحداث غير المتزامنة، مثل التحديثات أو التغييرات في البيانات.
لكن تظهر المشكلة هنا عندما يحتاج الخادم إلى التوسع أفقيًا -أي إضافة مثيلات متعددة من الخادم- من أجل التعامل معل زيادة حجم الطلبات. حيث يحتاج الخادم إلى آلية لتوزيع الطلبات ومعالجة البيانات عبر عدة مثيلات، وهذا يزيد من تعقيد تنفيذ الاشتراكات بفعالية.
فإذا كنت تستخدم الاشتراكات من أجل إرسال تحديثات أو إشعارات إلى عدة عملاء في وقت واحد، وكان لديك مثيلات متعددة لتطبيق الواجهة الخلفية الخاص بك، فلن ترغب في أن يتصل العملاء بمثيل معين من الخادم، لأن ذلك قد يؤدي لعدم مزامنة التحديثات بين المثيلات المختلفة. وفي مثل هذه الحالة يمكن أن تفكر في استخدام Redis لتوزيع الحمل.
متى أحتاج لخادم GraphQL؟
احتدمت المنافسة بين GraphQL و REST باعتبارهما أشهر نمطين لواجهات برمجة التطبيقات، إذ يقدم كل منهما أساليب مختلفة للتعامل مع نقل البيانات عبر بروتوكولات الإنترنت. وهذا قد يدفعك للتساؤل هل أختار REST أم GraphQL، يعتمد القرار على متطلبات المشروع مثل حجم البيانات، والتحديثات الفورية، والمرونة في الاستعلامات.
فعليًا، لن يكون هناك أي زيادة في الأداء بمجرد الانتقال إلى GraphQL بالنسبة للتطبيقات العادية، لكن عندما يتوسع التطبيق ستظهر فائدة الانتقال لاستخدام GraphQL الذي يوفر حلاً قويًا ومرنًا مقارنة باستخدام REST API التقليدية.
كما تتمثل فائدة GraphQL في تبسيط التعامل مع الأنظمة المعقدة أو البنى التحتية القديمة، مثل واجهات برمجة التطبيقات الضخمة وصعبة الصيانة التابعة لجهات خارجية. فهي تتيح لك إرسال استعلامات مرنة وتحميل البيانات اللازمة فقط وتقلل حاجتك للتعامل مع كميات كبيرة من البيانات، وقد اعتمدت شركات كبرى مثل PayPal و Shopify على GraphQL لتحسين كفاءة واجهات برمجة التطبيقات الخاصة بها ولعل هذه الأمثلة تؤكد لنا على فعالية GraphQL في تحسين أداء التطبيقات مقارنة بالأساليب التقليدية.
الخلاصة
لا شك أن اختيار التقنية المناسبة لتطبيقك ليس أمرًا سهلًا، حيث يتوجب عليك التفكير في المشكلات التي تحلها كل تقنية ومميزاتها وسلبياتها لاتخاذ أفضل قرار. نأمل أن يكون هذا المقال قد قدم لك فهمًا أفضل لتقنية GraphQL، ووضح لك مميزاتها وسبب تفضيلها من قبل المطورين لاسيما مطوري الواجهة الأمامية.
ترجمة وبيتصرف لمقال GraphQL: Everything You Need to Know لكاتبه Eduard Krivanek
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.