يا هلا بيك يا سيدي، لو انت شغال مبرمج ويب (Web Developer) وبتعمل داشبورد (Dashboard) أو تطبيق فيه جدول بيانات بيتحدث طول الوقت (Real-time Data)، غالباً قابلتك المشكلة الكابوسية دي: اليوزر بيكون واقف في الصفحة الأولى، وفجأة يتم إضافة سجلات جديدة (Records) في قاعدة البيانات (Database)، اليوزر يدوس عشان يروح الصفحة التانية، يلاقي نفسه شاف نفس البيانات القديمة متكررة، أو بالعكس، فيه بيانات طارت وراحت عليه! المشكلة دي بتعمل صداع، وتعالالي هنا أقولك ليه الطرق التقليدية للـ تقليب بين الصفحات (Pagination) بتجيب ورا في البيانات الحية، وإزاي نبني نظام محترف يحل المعضلة دي من جذورها.
Table of contents [Show]
إيه هي مشكلة الـ Pagination التقليدي مع البيانات اللحظية؟
عشان نفهم "الوجع"، لازم نعرف إحنا متعودين نعمل إيه. الطريقة الكلاسيكية للتقليب بتعتمد على حاجتين أساسيتين: حاجة اسمها Offset وحاجة اسمها Limit. يعني مثلاً بتقول لقاعدة البيانات: "هات لي 10 سجلات، واعمل تخطي (Skip) لأول 20 سجل عشان أنا في الصفحة التالته".
الطريقة دي زي الفل وزي الحلاوة لو الجدول ثابت ومبيتغيرش. إنما لو الجدول شغال وفي إشعارات بتيجي، أو مبيعات بتتسجل، أو طلبات دليفري بتنزل كل ثانية، هنا الخازوق بيشتغل! تخيل معايا:
- أنت واقف في الصفحة الأولى (أول 10 سجلات).
- تم إضافة 3 سجلات جديدة في رأس الجدول (قاعدة البيانات رتبت الجدول بالأحدث فـ الأقدم عن طريق
ORDER BY created_at DESC). - دلوقتي السجلات الثلاثة الجداد خدوا أول 3 أماكن، والسجلات القديمة اتقطعت ونزلت تحت.
- لما تيجي تدوس على الصفحة التانية وتطلب الـ 10 سجلات اللي بعدهم مع
Offset 10، قاعدة البيانات هتجيب لك سجلات من النص، وتلاقي نفسك يا إما شوفت سجلات متكررة (Duplicated Records)، يا إما فيه سجلات كاملة اتنسخت ومظهرتش قدام اليوزر نهائياً (Missing Records).
المشكلة دي بتدمر تجربة المستخدم (User Experience) وبتخلي التطبيق يبان غير احترافي بالمرة.
الحل السحري: الانتقال للـ Cursor-based Pagination
عشان نطلع من المأزق ده، لازم نرمي الـ Offset Pagination في الزبالة ونعتمد على تقنية تانية اسمها Cursor-based Pagination أو التقليب المبني على المؤشر. الفكرة هنا بسيطة وعبقرية: بدل ما بنقول للسيستم "هات الصفحة التانية بتخطّي 20 سجل"، بنقوله "هات لي الـ 10 سجلات اللي أقدم من أو أحدث من السجل الفلاني اللي أنا واقف عنده حالياً".
الـ Cursor ده غالباً بيكون هو الـ ID بتاع السجل الأخير اللي ظاهر قدام اليوزر على الشاشة، أو التاريخ والوقت بدقة (Timestamp). وبما إن السجلات الجديدة بتنزل فوق أو تحت، فالـ Cursor بتاعنا ثابت ومعلم على نقطة معينة، فمفيش أي بيانات هتتكرر أو تضيع.
تعال نشوف شكل استعلام الـ SQL بيبقى عامل إزاي:
-- الطريقة التقليدية (اللي بتجيب صداع)
SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 20;
-- طريقة الـ Cursor-based المحترفة
SELECT * FROM orders
WHERE id < 15482 -- ده الـ Cursor (آخر ID شافه اليوزر)
ORDER BY id DESC
LIMIT 10;
بالطريقة دي، حتى لو نزل 100 سجيل جديد في الجدول، الاستعلام بتاعنا هيفضل دقيق جداً وهيجيب السجلات اللي أقدم من الـ ID رقم 15482 من غير أي لخبطة.
دمج الـ WebSockets مع الـ Cursor Pagination للبيانات الحية
طيب، لو إحنا شغالين نظام لحظي حقيقي (Real-time Application) زي تطبيق دردشة، أو منصة تداول عملات، أو نظام متابعة طلبات، اليوزر مش مفروض يعمل تحديث (Refresh) بايده عشان يشوف الجديد. هنا بيجي دور الـ WebSockets أو تقنيات الـ Server-Sent Events (SSE).
القاعدة الذهبية في الـ Real-time Pagination هي فصل جزأين أساسيين:
- البيانات القديمة (Historical Data): دي بنجيبها بالـ Cursor Pagination العادي لما اليوزر يعمل سكرول لتحت (Infinite Scroll).
- البيانات الجديدة اللحظية (Incoming Real-time Data): دي بتجيلنا في الخلفية عن طريق الـ WebSocket، وبنحطها فوق خالص في الـ State بتاعة الواجهة (Frontend State) من غير ما نلعب في الـ Pagination الحالي بتاع اليوزر عشان ميتشتتش.
لما اليوزر يفتح الصفحة، بنبعت له أحدث بيانات بالـ API العادي، ونفتح اتصال WebSocket عشان لو فيه أي سجلات جديدة نزلت، تظهر له في زرار فوق كده بيقول: "فيه 5 طلبات جديدة، اضغط لعرضها"، فلما يدوس عليه بنضيفهم للجدول بسلاسة من غير ما نلخبط الـ Cursor اللي هو واقف عنده.
مثال عملي بلغة JavaScript (Frontend Logic)
عشان نقرب الفكرة أكتر، ده مثال مبسط لشرح إزاي بنتعامل مع الـ Infinite Scroll والـ Cursor في الواجهة الأمامية:
let lastCursor = null;
let isLoading = false;
async function loadMoreData() {
if (isLoading) return;
isLoading = true;
try {
let url = `/api/posts?limit=10`;
if (lastCursor) {
url += `&cursor=${lastCursor}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.items.length > 0) {
// تحديث الـ Cursor بالقيمة الأخيرة
lastCursor = data.items[data.items.length - 1].id;
// عرض البيانات في الجدول
appendDataToTable(data.items);
}
} catch (error) {
console.error("حصل خطأ أثناء تحميل البيانات:", error);
} finally {
isLoading = false;
}
}
// استدعاء الدالة عند وصول اليوزر لنهاية الصفحة
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
loadMoreData();
}
});
خاتمة ونصيحة أخوية
يا صاحبي، بناء أنظمة الـ Real-time Pagination ممكن تحسه مكعبر في الأول، وممكن تسترخص وتعمل Offset وتريح دماغك، بس صدقني، مع أول عميل يحصل عنده مشكلة في فقدان البيانات أو تكرارها هتحس إنك محتاج تعيد كتابة الكود من الأول. استثمر وقتك وافهم الـ Cursor-based Pagination وافهم إزاي تتعامل مع قواعد البيانات بذكاء. المبرمج الشاطر مش اللي بيعمل كود بيشتغل وخلاص، المبرمج الشاطر هو اللي بيعمل كود يستحمل الضغط وبيعيش قدام المشاكل. بالتوفيق ليك، ومستنيك في مقالات تانية نمزع فيها كود مع بعض!