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

افترض أن المستهلك لدينا هو تطبيق أو مكون معين من "دفتر الهاتف" الذي يطلب من API لدينا فقط معرف واسم ورقم هاتف المستخدمين المخزنين من قبلنا. في الوقت نفسه ، فإن واجهة برمجة التطبيقات لدينا هي أكثر شمولاً بكثير ، وسوف تسمح بالوصول إلى البيانات الأخرى ، مثل العنوان الفعلي للسكن وعنوان البريد الإلكتروني للمستخدمين.
عند نقطة تبادل البيانات بين المستهلك وواجهة برمجة التطبيقات ، يقوم GraphQL بعمل كل ما نحتاجه تمامًا - سيتم إرسال البيانات المطلوبة فقط استجابة للطلب. تكمن المشكلة في هذه الحالة في نقطة أخذ عينات البيانات من قاعدة البيانات - أي في التنفيذ الداخلي لخادمنا ، ويتكون من حقيقة أنه لكل طلب وارد ، نختار جميع بيانات المستخدم من قاعدة البيانات ، على الرغم من أننا لا نحتاج إلى بعض منها. هذا يولد حمولة زائدة على قاعدة البيانات ويؤدي إلى تداول حركة المرور الزائدة داخل النظام. مع وجود عدد كبير من الاستعلامات ، يمكنك الحصول على تحسين كبير من خلال تغيير نهج أخذ عينات البيانات واختيار الحقول المطلوبة فقط. في الوقت نفسه ، لا يهم على الإطلاق ما هو مصدر البيانات لدينا - قاعدة بيانات علائقية أو تقنية NoSQL أو خدمة أخرى (داخلية أو خارجية). قد يتأثر أي سلوك غير مثالي بأي تنفيذ. يتم تحديد MySQL في هذه الحالة ببساطة كمثال.
الحل
من الممكن تحسين سلوك الخادم هذا إذا قمنا بتحليل الوسيطات التي تأتي إلى وظيفة resolve()
:
async resolve(source, args, context, info) {
هذه هي الحجة الأخيرة ، info
، التي تهمنا بشكل خاص ، في هذه الحالة. ننتقل إلى التوثيق ونحلل بالتفصيل ما تتكون منه وظيفة resolve()
والحجة التي نهتم بها:
type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any type GraphQLResolveInfo = { fieldName: string, fieldNodes: Array<Field>, returnType: GraphQLOutputType, parentType: GraphQLCompositeType, schema: GraphQLSchema, fragments: { [fragmentName: string]: FragmentDefinition }, rootValue: any, operation: OperationDefinition, variableValues: { [variableName: string]: any }, }
لذا ، فإن الوسيطات الثلاثة الأولى التي تم تمريرها إلى المحلل هي source
- البيانات التي تم تمريرها من العقدة الأصلية في شجرة GraphQL للمخطط ، والحجج - وسيطات الطلب (التي تأتي من الاستعلام) ، context
- كائن سياق التنفيذ المحدد من قبل المطور ، غالبًا ما يتم استدعاؤه لنقل بعض البيانات العالمية في "المحللون". وأخيرًا ، الحجة الرابعة هي المعلومات الوصفية حول الطلب.
ما الذي يمكننا استخراجه من GraphQLResolveInfo
لحل مشكلتنا؟
الأجزاء الأكثر إثارة للاهتمام هي:
fieldName
هو اسم الحقل الحالي لمخطط GraphQL. على سبيل المثال يتوافق مع اسم الحقل المحدد في مخطط هذا المحلل. إذا قبضنا على كائن info
في حقل users
، كما في المثال أعلاه ، فإن "المستخدمين" هم الذين سيتم fieldName
كقيمة fieldName
fieldNodes
- مجموعة (صفيف) العقد التي تم طلبها في الاستعلام. فقط ما هو مطلوب!fragments
- مجموعة من أجزاء الطلب (في حالة تم تجزئة الطلب). معلومات مهمة أيضا لاسترجاع حقول البيانات النهائية.
لذا ، كحل ، نحتاج إلى تحليل أداة info
وتحديد قائمة الحقول التي جاءت إلينا من الاستعلام ، ثم تمريرها إلى استعلام SQL. للأسف ، لا تعطينا حزمة GraphQL من Facebook "خارج الصندوق" أي شيء لتبسيط هذه المهمة. بشكل عام ، كما أظهرت الممارسة ، فإن هذه المهمة ليست تافهة للغاية ، نظرًا لحقيقة أنه يمكن تجزئة الطلبات. بالإضافة إلى ذلك ، فإن مثل هذا التحليل له حل عالمي ، والذي يتم نسخه لاحقًا ببساطة من مشروع إلى آخر.
لذا قررت كتابتها كمكتبة مفتوحة المصدر بموجب ترخيص مركز الدراسات الدولي . بمساعدتها ، يتم حل حل تحليل حقول الاستعلام الواردة بكل بساطة ، على سبيل المثال ، في حالتنا كما يلي:
const { fieldsList } = require('graphql-fields-list');
fieldsList(info)
في هذه الحالة تقوم بكل العمل من أجلنا وترجع مجموعة "مسطحة" من الحقول الفرعية لهذا المحلل ، أي سيبدو استعلام SQL النهائي كما يلي:
SELECT id, name, phone FROM users;
إذا قمنا بتغيير الطلب الوارد إلى:
query UserListQuery { users { id name phone email } }
ثم يتحول استعلام SQL إلى:
SELECT id, name, phone, email FROM users;
ومع ذلك ، ليس من الممكن دائمًا القيام بهذا التحدي البسيط. غالبًا ما تكون التطبيقات الحقيقية أكثر تعقيدًا في البنية. في بعض عمليات التنفيذ ، قد نحتاج إلى وصف المحلل في المستوى العلوي فيما يتعلق بالبيانات في مخطط GraphQL النهائي. على سبيل المثال ، إذا قررنا استخدام مكتبة Relay ، فإننا نرغب في استخدام آلية جاهزة لتقسيم مجموعات كائنات البيانات إلى صفحات ، مما يؤدي إلى حقيقة أن مخطط GraphQL سيتم بناؤه وفقًا لقواعد معينة. على سبيل المثال ، نعيد صياغة مخططنا بهذه الطريقة (TypeScript):
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { connectionDefinitions, connectionArgs, nodeDefinitions, fromGlobalId, globalIdField, connectionFromArray, GraphQLResolveInfo, } from 'graphql-relay'; import { fieldsList } from 'graphql-fields-list'; export const { nodeInterface, nodeField } = nodeDefinitions(async (globalId: string) => { const { type, id } = fromGlobalId(globalId); let node: any = null; if (type === 'User') { node = await database.select(`SELECT id FROM user WHERE id="${id}"`); } return node; }); const User = new GraphQLObjectType({ name: 'User', interfaces: [nodeInterface], fields: { id: globalIdField('User', (user: any) => user.id), name: { type: GraphQLString }, email: { type: GraphQLString }, phone: { type: GraphQLString }, address: { type: GraphQLString }, } }); export const { connectionType: userConnection } = connectionDefinitions({ nodeType: User }); const Query = new GraphQLObjectType({ name: 'Query', fields: { node: nodeField, users: { type: userConnection, args: { ...connectionArgs }, async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) {
في هذه الحالة ، ستضيف connectionDefinition
من Relay edges
node
و pageInfo
وعقد cursor
إلى المخطط ، أي سنحتاج الآن إلى إعادة بناء استفساراتنا بشكل مختلف (لن نتطرق إلى ترقيم الصفحات الآن):
query UserListQuery { users { edges { node { id name phone email } } } }
وهذا يعني ، resolve()
الوظيفة التي يتم تنفيذها على عقدة users
، والتي يجب أن تحدد الآن الحقول المطلوبة ليس لنفسها ، ولكن بالنسبة إلى العقدة الفرعية المتداخلة الخاصة بها ، والتي ، كما نراها ، تتعلق users
على طول مسار edges.node
.
fieldsList
من مكتبة graphql-fields-list
ستساعدك في حل هذه المشكلة ، لذلك يجب عليك تمرير خيار path
المقابل لها. على سبيل المثال ، إليك التنفيذ في حالتنا:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node' }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
أيضًا في العالم الحقيقي ، قد يكون في مخطط GraphQL قد حددنا اسم حقل واحد فقط ، وفي مخطط قاعدة البيانات تتوافق أسماء الحقول الأخرى معها. على سبيل المثال ، افترض أن جدول المستخدم في قاعدة البيانات تم تعريفه بشكل مختلف:
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) );
في هذه الحالة ، يجب إعادة تسمية الحقول من استعلام GraphQL قبل تضمينها في استعلام SQL. سوف تساعدك قائمة fieldsList
في هذا إذا قمت fieldsList
خريطة ترجمة اسم في خيار transform
المقابل:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node', transform: { phone: 'phoneNumber', name: 'fullName' }, }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
ومع ذلك ، في بعض الأحيان ، لا يكفي التحويل إلى مجموعة مسطحة من الحقول (على سبيل المثال ، إذا أعاد مصدر البيانات بنية معقدة مع التداخل). في هذه الحالة ، fieldsMap
وظيفة graphql-fields-list
مكتبة graphql-fields-list
إلى الإنقاذ ، والتي تُرجع شجرة الحقول المطلوبة بالكامل ككائن:
const { fieldsMap } = require(`graphql-fields-list`);
إذا افترضنا أن المستخدم موصوف ببنية معقدة ، فسوف نحصل عليه بالكامل. يمكن أن تأخذ هذه الطريقة أيضًا وسيطة path
الاختيارية ، والتي تتيح لك الحصول على خريطة للفرع الضروري فقط من الشجرة بأكملها ، على سبيل المثال:
const { fieldsMap } = require(`graphql-fields-list`);
تحويل الأسماء على البطاقات غير معتمد حاليًا ويبقى تحت رحمة المطور.
طلب التجزئة
يدعم GraphQL تجزئة الاستعلام ، على سبيل المثال ، يمكننا أن نتوقع من المستهلك أن يرسل إلينا مثل هذا الاستعلام (هنا نشير إلى المخطط الأصلي ، وهو أمر بعيد المنال ، ولكنه صحيح من الناحية النحوية):
query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment }
لا داعي للقلق في هذه الحالة ، fieldsList(info)
، و fieldsMap(info)
في هذه الحالة النتيجة المتوقعة ، نظرًا لأنها تأخذ في الاعتبار إمكانية تجزئة الطلبات. لذلك ، fieldsList(info)
بإرجاع ['id', 'name', 'phone', 'email', 'address']
fieldsMap(info)
على التوالي:
{ id: false, name: false, phone: false, email: false, address: false }
ملاحظة
آمل أن تكون هذه المقالة قد ساعدت في تسليط الضوء على بعض الفروق الدقيقة في العمل مع GraphQL على الخادم ، ويمكن أن تساعدك مكتبة قائمة الحقول Graphql في إنشاء حلول مثالية في المستقبل.
تحديث 1
تم إصدار الإصدار 1.1.0 من المكتبة - تمت إضافة دعم @skip
و @include
في الطلبات. بشكل افتراضي ، يتم تمكين الخيار ، إذا لزم الأمر ، قم بتعطيله على النحو التالي:
fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false);