Cursor Pagination في التطوير المحمول — ما هو المبدأ والتنفيذ

المؤلف: IT Sectr نُشر: 2026-03-11 وقت القراءة: 9 دق

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

الخلاصة

  • Cursor Pagination هي طريقة تصفح حيث كل سجل له معرف مؤشر فريد للتنقل.
  • المؤشر هو علامة موضع فريدة في مجموعة البيانات (عادة ID، UUID، طابع زمني) لا تتغير عند الإضافات.
  • الاستقرار — السجلات الجديدة المضافة بين الطلبات لا تحرك المؤشر، مما يلغي التكرارات والفواصل.
  • الأداء — استعلام WHERE id > cursor يستخدم الفهرس بكفاءة دون فقدان السرعة على مجموعات البيانات الكبيرة.
  • القيد — التصفح بالمؤشر لا يدعم التنقل برقم الصفحة (لا يمكنك القفز إلى الصفحة 5).

ما هو Cursor Pagination؟

Cursor Pagination هي طريقة تحميل مقسمة إلى صفحات حيث يعيد الخادم مؤشراً خاصاً مع البيانات. يستخدم العميل هذا المؤشر في الطلب التالي للحصول على الدفعة التالية من السجلات. المؤشر هو معرف فريد للعنصر الأخير في الصفحة الحالية.

على عكس التصفح بـ Offset، حيث يقول العميل «أعطني الصفحة 5 بـ 20 سجلاً»، يعمل التصفح بالمؤشر بشكل مختلف: «أعطني 20 سجلاً بعد السجل ذي ID = 83». ينفذ الخادم استعلاماً بـ WHERE id > 83 و LIMIT 20. هذا النهج يضمن أن كل سجل يقع بالضبط في صفحة واحدة بغض النظر عن الإضافات.

انتشر مفهوم التصفح بالمؤشر على نطاق واسع بفضل مواصفة Relay Connection (GraphQL)، التي جعلت التصفح القائم على المؤشر معياراً لواجهات API الحديثة. تحدد Relay تنسيق الاستجابة: edges (مصفوفة من السجلات مع المؤشرات)، pageInfo (hasNextPage، hasPreviousPage، startCursor، endCursor).

تاريخ النشأة

التصفح بالمؤشر ليس تقنية جديدة — فقد كانت تُستخدم في قواعد البيانات قبل ظهور الويب بوقت طويل. في SQL يُسمى keyset pagination أو طريقة seek. أصبحت هذه الطريقة شائعة في APIs بعد نشر مواصفة Relay في 2015، التي وحدت تنسيق المؤشر كسلسلة مشفرة بـ base64 للتوحيد عبر نقل HTTP.

كيف يعمل التصفح بالمؤشر

المبدأ الأساسي للتصفح بالمؤشر هو أن الاستعلام يستخدم شرط WHERE على حقل مفهرس لتحديد الموضع، وليس إزاحة. للاتجاه الأمامي يُستخدم WHERE id > last_id، وللاتجاه الخلفي WHERE id < first_id. فهرس B-tree يجد أول سجل بعد المؤشر في O(log n)، مما يعطي وقت استجابة مستقراً.

sql
-- جلب 20 سجلًا بعد المؤشر '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- جلب 20 سجلًا قبل المؤشر '83' (للخلف)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

تنسيق المؤشر

يمكن أن يكون المؤشر بسيطاً (قيمة ID) أو معقداً (مركباً من عدة حقول). المؤشرات البسيطة هي المفتاح الأساسي للسجل، مثلاً id تزايدي تلقائي أو UUID. تُستخدم المؤشرات المركبة للفرز حسب حقول غير فريدة، مثلاً (created_at, id)، حيث يضمن id التفرد عندما تكون الأختام الزمنية متطابقة.

التنسيق النموذجي لواجهة API هو مؤشر كسلسلة مشفرة بـ base64. يفك الخادم ترميز المؤشر، ويستخرج القيمة، ويبني استعلام SQL. ترميز Base64 يخفي البنية الداخلية للمؤشر عن العميل ويسمح بتغيير التنسيق دون كسر التوافق العكسي. يتلقى العميل المؤشرات في حقل endCursor من الاستجابة ويمررها كسلسلة في الطلب التالي.

التنقل للأمام والخلف

يدعم التصفح بالمؤشر التنقل ثنائي الاتجاه. للحركة للأمام (next)، يُستخدم مؤشر آخر عنصر في الصفحة الحالية؛ للخلف (previous)، مؤشر العنصر الأول. المعاملان after و before في الطلب يحددان الاتجاه: after يأخذ السجلات بعد المؤشر، before يأخذ السجلات قبل المؤشر.

مقارنة Cursor و Offset

الاختيار بين التصفح بالمؤشر و Offset هو أحد القرارات المعمارية الرئيسية عند تصميم API. لكل طريقة نقاط قوة وضعف تحدد قابلية تطبيقها. Cursor pagination تتفوق في السيناريوهات ذات البيانات الديناميكية؛ Offset تتفوق في سيناريوهات التنقل العشوائي.

الخاصيةCursorOffset
الاستقرار عند الإضافاتعالي (بدون تكرار)منخفض (إزاحة الصفحات)
الأداء على المجموعات الكبيرةO(log n) — مستقرO(n) — يتدهور مع النمو
التنقل برقم الصفحةلانعم (page=5)
تعقيد التنفيذمتوسطمنخفض
دعم RESTcursor/before/afterpage/offset
دعم GraphQLمعيار Relayغير موصى به

لماذا يفشل Offset على نطاق واسع

يقوم التصفح بـ Offset بفحص كامل للجدول حتى موضع OFFSET. عند offset=100000، تقرأ قاعدة البيانات وتتخطى 100000 صف، حتى لو كان LIMIT يساوي 20. MySQL و PostgreSQL لا يمكنهما تحسين OFFSET — هذه خاصية تنفيذ LIMIT/OFFSET في SQL. يستخدم التصفح بالمؤشر فهرس B-tree الذي يجد الموضع في O(log n).

مشكلة إضافية لـ Offset هي «تخطي» السجلات عند التصفح للخلف. إذا قام مستخدم بتحميل الصفحة 5 وفي تلك اللحظة أُضيفت سجلات جديدة، عند طلب الصفحة 6 سيرى السجل من الصفحة 5 مرة أخرى أو سيفوته الجديد. Cursor pagination تلغي هذا السيناريو تماماً: المؤشر يشير إلى مكان محدد في المجموعة، والإضافات لا تغير الموضع.

تنفيذ Cursor Pagination

لنلق نظرة على تنفيذ التصفح بالمؤشر على الخادم (Kotlin + Spring) وعلى العميل (Android + Retrofit). يقبل الخادم المعاملات after، before، limit ويعيد قائمة بالسجلات مع المؤشرات و pageInfo. الاستجابة النموذجية تحتوي على hasNextPage و hasPreviousPage لإدارة واجهة التصفح.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

تنفيذ العميل على Android

على العميل، يتم تنفيذ التصفح بالمؤشر عبر PagingSource من Paging 3، حيث المفتاح هو مؤشر (Long). يتلقى PagingSource.load LoadParams.key — مؤشر آخر سجل محمل. LoadResult.Page يعيد البيانات و nextKey — المؤشر للصفحة التالية. عندما nextKey = null، يكتمل التصفح.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource مع مفتاح المؤشر
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

تنفيذ GraphQL عبر Relay

في GraphQL، يتم تنفيذ التصفح بالمؤشر عبر نمط Relay Connection. لكل نوع Connection (مع pageInfo و edges) و Edge (node + cursor). يمرر الاستعلام المعاملات first، after، last، before. يعيد الخادم مصفوفة من edges مع المؤشرات و pageInfo مع hasNextPage/hasPreviousPage.

متى تستخدم Cursor Pagination

يوصى باستخدام التصفح بالمؤشر لواجهات API التي تعمل مع البيانات الديناميكية حيث تُضاف السجلات أو تُحذف بشكل متكرر. أمثلة كلاسيكية: خلاصة الأخبار في شبكة اجتماعية، رسائل الدردشة، سجل المعاملات، تعليقات المنشورات. في كل هذه السيناريوهات، الاتساق وعدم وجود تكرارات مهمان.

  • الدردشات والمراسلة — كل رسالة جديدة تُضاف إلى أعلى القائمة. التصفح بـ Offset يضطرب مع كل رسالة جديدة.
  • الشبكات الاجتماعية والخلاصات — المنشورات تُنشر باستمرار. التصفح بالمؤشر يضمن ألا يفوت المستخدم أي منشور.
  • سجل الطلبات والمعاملات — البيانات تتغير بشكل أقل، لكن الاتساق ضروري للتقارير المالية.
  • APIs ذات أحجام بيانات كبيرة — ملايين السجلات. Cursor pagination تحافظ على الأداء حيث يبدأ Offset بالتباطؤ.
  • APIs GraphQL — معيار Relay يلزم باستخدام التصفح القائم على المؤشر للامتثال للمواصفة.
  • التطبيقات المحمولة مع التمرير اللانهائي — المستخدم يمرر للأسفل محمّلاً دفعات جديدة. نهج المؤشر يعطي تجربة مستخدم سلسة بدون تكرار.

متى لا يناسب التصفح بالمؤشر

هناك سيناريوهات يكون فيها التصفح بـ Offset أكثر ملاءمة: لوحات الإدارة حيث تحتاج التنقل برقم الصفحة؛ البحث مع التصفح حيث قد تتغير النتائج؛ التقارير والتحليلات حيث تحتاج رابطاً ثابتاً للصفحة 5. في هذه الحالات، لا تفوق مزايا المؤشر تعقيد التنفيذ.

لا يدعم التصفح بالمؤشر «القفز» إلى صفحة عشوائية — لا يمكن للمستخدم النقر على «الصفحة 5» والذهاب إليها. هذا قيد معماري: حساب العدد الإجمالي للصفحات يتطلب استعلام COUNT منفصلاً، قد يكون مكلفاً للجداول الكبيرة. في مثل هذه الحالات، نهج هجين: مؤشر للبيانات + count للتصفح.

الأسئلة الشائعة

ما هو المؤشر في Cursor Pagination؟

المؤشر هو معرف فريد للسجل يشير إلى موضع في مجموعة البيانات. يمكن أن يكون بسيطاً (ID السجل) أو مركباً (عدة حقول). يتلقى العميل مؤشر آخر سجل في الصفحة ويمرره في الطلب التالي للحصول على الدفعة التالية.

لماذا Cursor Pagination أفضل من Offset؟

Cursor pagination غير عرضة للإزاحة عند إضافة سجلات جديدة — كل عنصر يقع بالضبط في صفحة واحدة. كما تحافظ على السرعة على الأحجام الكبيرة باستخدام الفهارس بدلاً من مسح أول n صف. Offset أبسط لكن غير مستقر للبيانات الديناميكية.

هل يمكن تنفيذ التصفح بالمؤشر بدون GraphQL؟

نعم، التصفح بالمؤشر غير مرتبط بـ GraphQL. يمكن تنفيذه في أي API REST بتمرير المؤشر كمعامل استعلام ?after=83&limit=20. يجب أن تحتوي الاستجابة على pageInfo مع endCursor و hasNextPage — هذا يسمح للعميل بإدارة التحميل دون معرفة البنية الداخلية للمؤشر.

أي مؤشر استخدام — ID أم UUID أم طابع زمني؟

ID التزايدي التلقائي هو الخيار الأمثل: يتزايد بشكل رتيب، لا يتغير، يُفهرس بكفاءة. UUID v7 (المرتب زمنياً) مناسب أيضاً. الأختام الزمنية قد تعطي تكرارات في نفس الوقت، لذا اجمعه مع ID: (created_at, id) لضمان تفرد المؤشر.

كيف تعرف العدد الإجمالي للصفحات مع التصفح بالمؤشر؟

التصفح بالمؤشر لا يوفر العدد الإجمالي للصفحات — هذا قيده. إذا كنت بحاجة لمعلومات العدد الإجمالي، نفذ استعلام COUNT منفصلاً بنفس المرشحات. للجداول الكبيرة، استخدم العد التقريبي عبر EXPLAIN أو إجمالي مخبأ من التحليلات.

الملخص

  • Cursor Pagination هي طريقة تصفح مع تنقل بواسطة معرف سجل فريد بدلاً من الإزاحة.
  • المؤشر يضمن استقرار المجموعة عند الإضافات: السجلات الجديدة لا تحرك الصفحات المحملة بالفعل.
  • الأداء على أحجام البيانات الكبيرة يبقى عالياً (O(log n)) بفضل استخدام فهرس B-tree.
  • Cursor pagination مناسبة للبيانات الديناميكية: الدردشات، خلاصات الأخبار، المعاملات، التعليقات.
  • القيد الرئيسي هو عدم وجود تنقل برقم الصفحة وعدم القدرة على القفز إلى صفحة عشوائية.
  • التنفيذ يستخدم WHERE id بعد المؤشر، معاملات after/before و pageInfo في الاستجابة.
  • المعيار — Relay Connection GraphQL، لكن APIs REST مع معاملات المؤشر واسعة الانتشار أيضاً.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا