Graphql - تحسين استعلامات قاعدة البيانات

عند العمل مع قواعد البيانات ، هناك مشكلة تسمى "SELECT N + 1" - عندما يقوم تطبيق ، بدلاً من استعلام واحد إلى قاعدة البيانات ، والذي يحدد جميع البيانات الضرورية من عدة جداول ومجموعات ذات صلة ، بعمل استعلام فرعي إضافي لكل صف من نتائج الاستعلام الأول ، للحصول على البيانات ذات الصلة. على سبيل المثال ، نحصل أولاً على قائمة بالطلاب الجامعيين الذين يتم تحديد تخصصهم فيه من خلال معرف ، ثم نقوم بعمل استعلام فرعي إضافي لكل جدول من الطلاب أو إلى مجموعة من التخصصات من أجل الحصول على اسم التخصص من خلال معرف التخصص. نظرًا لأن كل استعلام فرعي قد يتطلب استعلامًا فرعيًا آخر واستعلامًا فرعيًا آخر - يبدأ عدد الاستعلامات إلى قاعدة البيانات في النمو بشكل كبير.

عند العمل باستخدام Graphql ، من السهل جدًا إنشاء مشكلة "SELECT N + 1" إذا قمت بإجراء استعلام فرعي على الجدول المرتبط في وظيفة المحلل. أول ما يتبادر إلى الذهن هو تقديم طلب على الفور مع مراعاة جميع البيانات ذات الصلة ، ولكن هذا يجب أن توافق ، غير منطقي إذا لم يطلب العميل البيانات ذات الصلة.

سيتم النظر في أحد الحلول لمشكلة "SELECT N + 1" ل Graphql في هذا المنشور.

على سبيل المثال ، خذ مجموعتين: "المؤلفين" (المؤلف) و "الكتب" (الكتاب). العلاقة ، كما يفترض المرء ، الكثير لكثير. يمكن أن يحتوي مؤلف واحد على العديد من الكتب ، ويمكن كتابة كتاب واحد من قبل العديد من المؤلفين. لتخزين المعلومات سنستخدم قاعدة بيانات mongodb ومكتبة mongoose.js

ندرك العلاقة بين مجموعات كثيرة باستخدام مجموعة BookAuthor الإضافية والمجالات الافتراضية.

// Author.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ name: String }); schema.virtual('books', { ref: 'BookAuthor', localField: '_id', foreignField: 'author' }); module.exports = schema; 

 // Book.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ title: String }); schema.virtual('authors', { ref: 'BookAuthor', localField: '_id', foreignField: 'book' }); module.exports = schema; 

 // BookAuthor.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, book: { type: mongoose.Schema.Types.ObjectId, ref: 'Book' } }); module.exports = schema; 

 // mongoSchema.js const mongoose = require('mongoose'); const Author = require('./Author'); const Book = require('./Book'); const BookAuthor = require('./BookAuthor'); mongoose.connect('mongodb://localhost:27017/books') mongoose.set('debug', true); exports.Author = mongoose.model('Author', Author); exports.Book = mongoose.model('Book', Book); exports.BookAuthor = mongoose.model('BookAuthor', BookAuthor); 

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

 // graphqlType.js exports.Author = require('./Author'); exports.Book = require('./Book'); 

 // Author.js const graphql = require('graphql') const graphqlType = require('./index') module.exports = new graphql.GraphQLObjectType({ name: 'author', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, name: { type: graphql.GraphQLString, }, books: { type: new graphql.GraphQLList(graphqlType.Book), resolve: obj => obj.books && obj.books.map(book => book.book) } }) }); 

 // Book.js const graphql = require('graphql') const graphqlType = require('./index') module.exports = new graphql.GraphQLObjectType({ name: 'book', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, title: { type: graphql.GraphQLString, }, authors: { type: new graphql.GraphQLList(graphqlType.Author), resolve: obj => obj.authors && obj.authors.map(author => author.author) } }) }); 

الآن نحدد طلب المؤلفين ، ربما بقائمة كتبهم ، وربما بقائمة المؤلفين (المؤلفون المشاركون) لهذه الكتب.

 const graphql = require('graphql'); const getFieldNames = require('graphql-list-fields'); const graphqlType = require('../graphqlType'); const mongoSchema = require('../mongoSchema'); module.exports = { type: new graphql.GraphQLList(graphqlType.Author), args: { _id: { type: graphql.GraphQLString } }, resolve: (_, {_id}, context, info) => { const fields = getFieldNames(info); const where = _id ? {_id} : {}; const authors = mongoSchema.Author.find(where) if (fields.indexOf('books.authors.name') > -1 ) { authors.populate({ path: 'books', populate: { path: 'book', populate: {path: 'authors', populate: {path: 'author'}} } }) } else if (fields.indexOf('books.title') > -1 ) { authors.populate({path: 'books', populate: {path: 'book'}}) } return authors.exec(); } }; 

من أجل تحديد طلب الحقول الذي جاء من العميل ، يتم استخدام مكتبة حقول قائمة Graphql. وإذا جاء الطلب بكائنات متداخلة ، فسيتم استدعاء طريقة التعبئة () لمكتبة النمس.

الآن دعنا نجرب الاستعلامات. أقصى طلب ممكن للتنفيذ لدينا:

 { author { _id name books { _id title authors { _id name } } } } 

سيتم إجراء 5 مكالمات إلى قاعدة البيانات:

 authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} }) في': [ObjectId ( "5b0fcab305b15d38f672357d")، ObjectId ( "5b0fcabd05b15d38f672357e")، ObjectId ( "5b0fcac405b15d38f672357f")، ObjectId ( "5b0fcad705b15d38f6723580")، ObjectId ( "5b0fcae305b15d38f6723581")، ObjectId authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} }) 

كما ترى ، لا تستخدم الدالة mongoose.js - تعبئة () - ميزة mongodb الجديدة نسبيًا - بحث $ ، ولكنها تنشئ طلبات إضافية. ولكن هذه ليست مشكلة "SELECT N + 1" منذ ذلك الحين لم يتم إنشاء استعلام جديد لكل صف ، ولكن للمجموعة بأكملها. (كانت الرغبة في التحقق من كيفية عمل دالة mongoose.js () في الواقع - مع طلب واحد أو عدة - أحد دوافع اختيار قاعدة بيانات غير ارتباطية لهذا المثال).

إذا استخدمنا استعلام أضيق الحدود:

 { author { _id name } } 

ثم ستشكل مكالمة واحدة فقط لقاعدة البيانات:

  authors.find({}, { fields: {} }) 

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

لراحة القراء ، يقع المثال العامل في المستودع .

الملحق بتعليق joniks

أشار مستخدم joniks في الخلاصة إلى المكتبة https://github.com/facebook/dataloader . دعونا نرى كيف تسمح لك هذه المكتبة بالتعامل مع مشكلة "SELECT N + 1"

بالنظر إلى هذه المكتبة ، سيبدو تعريف نوع مؤلف الرسوم البيانية على النحو التالي:

 // Autors.js const graphql = require('graphql') const DataLoader = require('dataloader') const graphqlType = require('./index') const mongoSchema = require('../mongoSchema'); const bookLoader = new DataLoader(async ids => { const data = await mongoSchema.Book.find({ _id: { $in: ids }}).populate('authors').exec(); const books = data.reduce((obj, item) => (obj[item._id] = item) && obj, {}) const response = ids.map(id => books[id]); return response; }); module.exports = new graphql.GraphQLObjectType({ name: 'authors', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, name: { type: graphql.GraphQLString, }, books: { type: new graphql.GraphQLList(graphqlType.Books), resolve: obj => obj.books && obj.books.map(book => bookLoader.load(book.book)) } }) }); 


ما الفائدة من استخدام هذه المكتبة: يتم تجميع طلبات bookLoader.load (id) الفردية وإرسالها للمعالجة باستخدام مجموعة من المعرّفات التي تعمل على bookLoader = new DataLoader (async ids => {...
عند الإخراج ، يجب أن نعيد وعد الصفيف أو مجموعة الوعود التي تقع في نفس ترتيب صفيف إدخال ids.

الآن يمكننا إعادة كتابة طلبنا للمؤلفين على النحو التالي:

 // authors.js const graphql = require('graphql'); const getFieldNames = require('graphql-list-fields'); const graphqlType = require('../graphqlType'); const mongoSchema = require('../mongoSchema'); module.exports = { type: new graphql.GraphQLList(graphqlType.Authors), args: { _id: { type: graphql.GraphQLString } }, resolve: (_, {_id}, context, info) => { const fields = getFieldNames(info); const where = _id ? {_id} : {}; const authors = mongoSchema.Author.find(where).populate('books') return authors.exec(); } }; 


ونتيجة لذلك ، يمكننا الاستعلام عن كائنات ذات صلة بمستوى تداخل عشوائي دون القلق بشأن مشكلة SELECT N + 1 (على الرغم من تكلفة مكالمة غير مشروطة للتعبئة () حتى في حالة عدم الحاجة إليها):

 { authors { _id name books { _id title authors { _id name books { _id title authors { _id name } } } } } } 


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

apapacy@gmail.com
31 مايو 2018

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


All Articles