ترقيم الصفحات والترتيب
ترقيم الصفحات والترتيب
حين ينمو الجدول إلى آلاف أو ملايين السجلات، تحميل كل سجل في الذاكرة لطلب واحد أمر بطيء ومُكلف. تحلّ Spring Data JPA هذه المشكلة بأناقة عبر مجردَّتين — Pageable وSort — وتُغلّف النتيجة في كائن Page<T> غني يحمل البيانات وبيانات التنقّل التي تحتاجها واجهة المستخدم.
المجردّات الأساسية
تُقدّم Spring Data ثلاثة أنواع تعمل معًا:
Sort— تصف جملة ORDER BY: خاصية واحدة أو أكثر، لكل منها اتجاه (ASCأوDESC). غير قابلة للتغيير وقابلة للتركيب.Pageable— تجمع رقم الصفحة (يبدأ من صفر) وحجم الصفحة وكائنSortاختياري. التنفيذ الأكثر شيوعًا هوPageRequest.Page<T>— نتيجة استعلام مُرقَّم. تمتد منSlice<T>وتُضيف إجمالي العناصر وعدد الصفحات، مما يستلزم استعلامCOUNT(*)إضافيًا.
Page<T> دائمًا استعلام COUNT ثانيًا ليعرف إجمالي السجلات والصفحات. أما Slice<T> فيتخطى العدّ ولا يعرف إلا ما إذا كانت هناك شريحة تالية. بالنسبة لواجهات التمرير اللانهائي أو التنقل بالمؤشر، Slice أرخص. أما للمرقّم التقليدي الذي يعرض "صفحة 3 من 47" فأنت بحاجة إلى Page.
تفعيل ترقيم الصفحات في Repository
أضف Pageable كمعامل لأي دالة في الـ repository وأعد Page<T> أو Slice<T>. تتولى Spring Data JPA الباقي — تُحقن LIMIT وOFFSET (أو ما يعادلهما) واستعلام COUNT الملفوف لـ Page.
تُوفّر قاعدة JpaRepository بالفعل findAll(Pageable)، لذا لا تحتاج إلى تعريفها إلا حين تُضيف معاملات تصفية.
بناء PageRequest
استخدم الدوال المصنعية الساكنة في PageRequest:
PageRequest.of():
PageRequest.of(apiPage - 1, size).
التعامل مع نتيجة Page
يحتوي كائن Page<T> على كل ما تحتاجه واجهة القائمة:
في متحكم Spring MVC أو REST يمكنك إعادة هذا الكائن مباشرةً:
يُحوّل مُسلسل Jackson في Spring كائن Page<T> إلى غلاف JSON يتضمن content وtotalElements وtotalPages وnumber والمزيد — وهو بالضبط ما تحتاجه الواجهة الأمامية لعرض مرقّم الصفحات.
الترتيب بدون ترقيم الصفحات
أحيانًا تريد نتائج مرتبة بدون حد للصفحات. مرّر كائن Sort مباشرةً:
استخدام @Query مع ترقيم الصفحات
تقبل الاستعلامات المخصصة بـ JPQL أيضًا Pageable. ضع تعليق @Query على الدالة وأضف معامل Pageable كآخر وسيط:
تُحقن Spring Data جملة ORDER BY وLIMIT من Pageable في SQL المولَّد تلقائيًا. لاستعلام العدّ تُغلّف JPQL في SELECT COUNT(p) ما لم تُوفّر خاصية countQuery مخصصة.
Sort.by("description") على عمود TEXT بملايين السجلات قاعدةَ البيانات على إجراء filesort كامل. أنشئ فهارس للأعمدة التي تُرتّب بها — خاصة createdAt وحقول الحالة التي كثيرًا ما تُستخدم هدفًا للترتيب في واجهات القوائم.
الربط التلقائي مع Web MVC بـ Pageable
يستطيع Spring MVC ربط Pageable مباشرةً من معاملات الطلب إذا أضفت @EnableSpringDataWebSupport إلى إعداداتك (يُفعَّل تلقائيًا في Spring Boot):
يمكن أن يظهر معامل sort أكثر من مرة للترتيب متعدد الأعمدة. هذا يتيح للواجهة الأمامية التحكم في الترقيم والترتيب بدون أي كود خاص في المتحكم.
ملاحظات الأداء
- تكلفة OFFSET: يقرأ
LIMIT 20 OFFSET 10000ويتجاهل 10,000 صف في معظم قواعد البيانات. للصفحات العميقة جدًا، فكّر في ترقيم الصفحات بالمؤشر (keyset pagination) بدلًا من ذلك. - استعلام COUNT: كل نتيجة
Pageتُطلق استعلامَي SQL. إذا كان العدّ الإجمالي مكلفًا (مثل الانضمام إلى عدة جداول)، انتقل إلىSliceوأخفِ الإجمالي عن المستخدمين، أو خزّن العدد في الذاكرة المؤقتة. - Fetch joins وترقيم الصفحات: يُسجّل Hibernate تحذيرًا حين تمزج fetch join (الذي يُضخّم الصفوف) مع
Pageable. يُنفّذ الترقيم في الذاكرة بدلًا من SQL، مما قد يكون كارثيًا على مجموعات البيانات الكبيرة. الحل: رقّم المعرّفات أولًا، ثم جلب الكيانات الكاملة بتلك المعرّفات.
الخلاصة
PageRequest.of(page, size, sort) هي نقطة دخولك. مرّرها إلى أي دالة في الـ repository تُعيد Page<T> وستحصل على البيانات وحالة التنقل والإجماليات في كائن واحد. استخدم Sort.by() وحده حين تحتاج الترتيب بدون حد للصفحات. انتبه للمطبَّين الخاصَّين بالأداء — تكلفة OFFSET العميق وتعارضات fetch join — وستمتلك طبقة بيانات قابلة للتوسع وسهلة الصيانة.