العمل مع البيانات عند بناء API على أساس GraphQL

الديباجة


بادئ ذي بدء ، تم تصميم هذه المقالة لأولئك القراء الذين هم بالفعل على دراية بـ 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'); // ... async resolve(source, args, context, info) { const requestedFields = fieldsList(info); return await database.query(`SELECT ${requestedFields.join(',')} FROM users`) } 

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, ) { // TODO: implement }, }, }); export const schema = new GraphQLSchema({ query: Query }); 

في هذه الحالة ، ستضيف 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`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info); /* RESULT: { edges: { node: { id: false, name: false, phone: false, } } } */ } 

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


 const { fieldsMap } = require(`graphql-fields-list`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info, 'edges.node'); /* RESULT: { id: false, name: false, phone: false, } */ } 

تحويل الأسماء على البطاقات غير معتمد حاليًا ويبقى تحت رحمة المطور.


طلب التجزئة


يدعم 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); 

Source: https://habr.com/ru/post/ar427399/


All Articles