واجهات GraphQL

أنماط الترقيم في GraphQL

17 دقيقة الدرس 13 من 35

تنفيذ الترقيم في GraphQL

الترقيم ضروري للتعامل مع مجموعات البيانات الكبيرة بكفاءة. يدعم GraphQL استراتيجيات ترقيم متعددة، من الترقيم البسيط القائم على الإزاحة إلى الترقيم الأكثر تطوراً القائم على المؤشر بأسلوب Relay.

الترقيم القائم على الإزاحة

أبسط نهج للترقيم باستخدام الحد والإزاحة:

type Query { posts(limit: Int = 10, offset: Int = 0): PostConnection! } type PostConnection { posts: [Post!]! total: Int! hasMore: Boolean! } type Post { id: ID! title: String! content: String! createdAt: DateTime! }

محلل ترقيم الإزاحة

const resolvers = { Query: { posts: async (_, { limit = 10, offset = 0 }) => { // التحقق من معاملات الترقيم if (limit < 1 || limit > 100) { throw new Error('Limit must be between 1 and 100'); } if (offset < 0) { throw new Error('Offset cannot be negative'); } // جلب المنشورات بالحد والإزاحة const posts = await Post.find() .sort({ createdAt: -1 }) .skip(offset) .limit(limit); // الحصول على العدد الإجمالي لمعلومات الترقيم const total = await Post.countDocuments(); return { posts, total, hasMore: offset + limit < total }; } } }; // مثال على الاستعلام: // query { // posts(limit: 20, offset: 40) { // posts { // id // title // } // total // hasMore // } // }
ملاحظة: ترقيم الإزاحة بسيط ولكن لديه مشاكل في الأداء مع الإزاحات الكبيرة. استخدم الترقيم القائم على المؤشر للحصول على أداء أفضل مع مجموعات البيانات الكبيرة.

الترقيم القائم على المؤشر

ترقيم أكثر كفاءة باستخدام المؤشرات بدلاً من الإزاحات الرقمية:

type Query { posts( first: Int after: String last: Int before: String ): PostConnection! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Post { id: ID! title: String! content: String! createdAt: DateTime! }

تنفيذ ترقيم المؤشر

const { Buffer } = require('buffer'); // ترميز/فك ترميز المؤشر function encodeCursor(value) { return Buffer.from(value.toString()).toString('base64'); } function decodeCursor(cursor) { return Buffer.from(cursor, 'base64').toString('utf-8'); } const resolvers = { Query: { posts: async (_, { first, after, last, before }) => { // التحقق من الوسائط if (first && last) { throw new Error('Cannot use first and last together'); } if (first && first < 1) { throw new Error('first must be positive'); } if (last && last < 1) { throw new Error('last must be positive'); } // بناء الاستعلام let query = {}; if (after) { const afterDate = new Date(decodeCursor(after)); query.createdAt = { $lt: afterDate }; } if (before) { const beforeDate = new Date(decodeCursor(before)); query.createdAt = { ...query.createdAt, $gt: beforeDate }; } // تحديد الحد وترتيب الفرز const limit = first || last || 10; const sortOrder = last ? 1 : -1; // جلب المنشورات let posts = await Post.find(query) .sort({ createdAt: sortOrder }) .limit(limit + 1); // جلب واحد إضافي للتحقق من المزيد من الصفحات // التحقق من المزيد من الصفحات const hasMore = posts.length > limit; if (hasMore) { posts = posts.slice(0, limit); } // عكس الترتيب إذا كان جلب الأخير if (last) { posts = posts.reverse(); } // إنشاء الحواف const edges = posts.map(post => ({ node: post, cursor: encodeCursor(post.createdAt.toISOString()) })); // الحصول على العدد الإجمالي const totalCount = await Post.countDocuments(); return { edges, pageInfo: { hasNextPage: first ? hasMore : false, hasPreviousPage: last ? hasMore : false, startCursor: edges.length > 0 ? edges[0].cursor : null, endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null }, totalCount }; } } };
نصيحة: قم بترميز المؤشرات كسلاسل base64 لإخفاء تفاصيل التنفيذ. يمكن أن تستند المؤشرات إلى المعرفات أو الطوابع الزمنية أو القيم المركبة.

نمط اتصال بأسلوب Relay

اتباع مواصفات Relay للحصول على ترقيم متسق عبر واجهة برمجة التطبيقات الخاصة بك:

// مثال على استعلام Relay query GetPosts { posts(first: 10, after: "Y3JlYXRlZEF0OjIwMjYtMDItMTU=") { edges { cursor node { id title author { name } } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } // جلب الصفحة التالية query GetNextPage { posts(first: 10, after: "Y3JlYXRlZEF0OjIwMjYtMDItMDU=") { edges { cursor node { id title } } pageInfo { hasNextPage endCursor } } } // جلب الصفحة السابقة query GetPreviousPage { posts(last: 10, before: "Y3JlYXRlZEF0OjIwMjYtMDItMTU=") { edges { cursor node { id title } } pageInfo { hasPreviousPage startCursor } } }

مساعد ترقيم قابل لإعادة الاستخدام

// utils/pagination.js class PaginationHelper { static encodeCursor(value) { return Buffer.from(String(value)).toString('base64'); } static decodeCursor(cursor) { return Buffer.from(cursor, 'base64').toString('utf-8'); } static async paginate(model, args, options = {}) { const { first, after, last, before, sortField = 'createdAt', sortOrder = -1 } = { ...args, ...options }; // بناء الاستعلام const query = { ...options.where }; if (after) { const afterValue = this.decodeCursor(after); query[sortField] = { $lt: afterValue }; } if (before) { const beforeValue = this.decodeCursor(before); query[sortField] = { ...query[sortField], $gt: beforeValue }; } // تحديد الحد const limit = first || last || 10; const sort = last ? -sortOrder : sortOrder; // جلب المستندات let docs = await model .find(query) .sort({ [sortField]: sort }) .limit(limit + 1); // التحقق من المزيد من الصفحات const hasMore = docs.length > limit; if (hasMore) { docs = docs.slice(0, limit); } if (last) { docs = docs.reverse(); } // إنشاء الحواف const edges = docs.map(doc => ({ node: doc, cursor: this.encodeCursor(doc[sortField]) })); // الحصول على العدد الإجمالي const totalCount = await model.countDocuments(options.where || {}); return { edges, pageInfo: { hasNextPage: first ? hasMore : false, hasPreviousPage: last ? hasMore : false, startCursor: edges[0]?.cursor || null, endCursor: edges[edges.length - 1]?.cursor || null }, totalCount }; } } // الاستخدام في المحلل const resolvers = { Query: { posts: async (_, args) => { return await PaginationHelper.paginate(Post, args); }, userPosts: async (_, { userId, ...args }) => { return await PaginationHelper.paginate(Post, args, { where: { userId } }); } } }; module.exports = PaginationHelper;
تحذير: احرص دائمًا على تحديد حجم الصفحة الأقصى لمنع مشاكل الأداء. الحد الشائع هو 100 عنصر لكل صفحة.

الترقيم ثنائي الاتجاه

دعم الترقيم الأمامي والخلفي:

const resolvers = { Query: { posts: async (_, { first, after, last, before }) => { // الترقيم الأمامي if (first) { const query = after ? { createdAt: { $lt: decodeCursor(after) } } : {}; const posts = await Post.find(query) .sort({ createdAt: -1 }) .limit(first + 1); const hasNextPage = posts.length > first; const nodes = hasNextPage ? posts.slice(0, first) : posts; return { edges: nodes.map(post => ({ node: post, cursor: encodeCursor(post.createdAt) })), pageInfo: { hasNextPage, hasPreviousPage: !!after, startCursor: nodes[0] ? encodeCursor(nodes[0].createdAt) : null, endCursor: nodes[nodes.length - 1] ? encodeCursor(nodes[nodes.length - 1].createdAt) : null }, totalCount: await Post.countDocuments() }; } // الترقيم الخلفي if (last) { const query = before ? { createdAt: { $gt: decodeCursor(before) } } : {}; const posts = await Post.find(query) .sort({ createdAt: 1 }) .limit(last + 1); const hasPreviousPage = posts.length > last; const nodes = hasPreviousPage ? posts.slice(0, last) : posts; nodes.reverse(); return { edges: nodes.map(post => ({ node: post, cursor: encodeCursor(post.createdAt) })), pageInfo: { hasNextPage: !!before, hasPreviousPage, startCursor: nodes[0] ? encodeCursor(nodes[0].createdAt) : null, endCursor: nodes[nodes.length - 1] ? encodeCursor(nodes[nodes.length - 1].createdAt) : null }, totalCount: await Post.countDocuments() }; } throw new Error('Must provide either first or last'); } } };
تمرين:
  1. نفذ ترقيم قائم على الإزاحة لاستعلام users مع التحقق من حجم الصفحة
  2. أنشئ نظام ترقيم قائم على المؤشر باستخدام معرفات المنشورات بدلاً من الطوابع الزمنية
  3. ابنِ مساعد ترقيم قابل لإعادة الاستخدام يعمل مع أي نموذج Mongoose
  4. نفذ ترقيم ثنائي الاتجاه يدعم كلاً من first/after و last/before
  5. أضف إمكانيات التصفية إلى الاستعلامات المرقمة (على سبيل المثال، التصفية حسب الفئة أثناء ترقيم المنشورات)