يا هلا بالشباب! اليوم بنتكلم عن مشكلة الكل يطيح فيها لما يتعامل مع البيانات الكبيرة: كيف تجلب عشرات الآلاف أو حتى الملايين من السجلات بدون ما ينهار السيرفر أو السكربت؟ الحل هو الـ Pagination.
ليش نحتاج Pagination أصلاً؟
تخيل إن عندك قاعدة بيانات فيها مليون عميل، وكل عميل له تفاصيله. لو حاولت تجيب كل بيانات المليون عميل في طلب واحد، وش بيصير؟
كارثة! استهلاك ذاكرة خرافي في السيرفر والعميل، بطء استجابة يخلي المستخدم ينام ويصحى، وضغط مهول على قاعدة البيانات ممكن يوقفها. غير كذا، شبكياً الموضوع بيكون بطيء جداً.
الـ Pagination هنا يجي كمنقذ. فكرته بسيطة: بدل ما تجيب الكل، جيب جزء صغير (صفحة) في كل مرة.
أنواع الـ Pagination والطرق الشائعة
1. Offset/Limit (الترقيم التقليدي للصفحات)
هذي الطريقة هي الأكثر شيوعاً وسهولة في التطبيق. تعتمد على بارامترين أساسيين:
LIMIT: يحدد كم سجل تبغى تجلب في الصفحة الواحدة.OFFSET: يحدد كم سجل تبغى تتخطاه من البداية (يعني من أي سجل تبدأ الجلب).
مثال في SQL:
SELECT * FROM products
LIMIT 10 OFFSET 20;
هنا، أنا أقول لقاعدة البيانات: جيب لي 10 منتجات، بس ابدأ العد من بعد أول 20 منتج. يعني بجيب المنتجات من 21 إلى 30.
عيوبها: مع البيانات الكبيرة جداً (مئات الآلاف أو الملايين)، الـ
OFFSETممكن يكون بطيء جداً. ليش؟ لأن قاعدة البيانات لازالت تحتاج تعد السجلات اللي قبل الـOFFSETعشان تعرف من وين تبدأ، وهذا يستهلك وقت وموارد.
2. Cursor-based Pagination (الترقيم المبني على المؤشر)
هذي الطريقة أفضل للأداء مع البيانات الكبيرة جداً، خصوصاً في تطبيقات الـ Infinite Scroll (التمرير اللانهائي). الفكرة هنا هي إنك ما تستخدم رقم صفحة أو OFFSET، بل تحدد 'آخر سجل' تم جلبه وتستخدمه كنقطة بداية للطلب التالي.
غالباً نستخدم عمود فريد ومفهرس مثل id أو created_at.
مثال في SQL:
SELECT * FROM products
WHERE id > [last_id]
ORDER BY id ASC
LIMIT 10;
هنا، أنا أقول لقاعدة البيانات: جيب لي 10 منتجات، بس اللي الـ id حقها أكبر من [last_id] اللي جبته في الطلب السابق، ورتبها تصاعدياً حسب الـ id.
مميزاتها: أسرع بكثير لأنها تستخدم الفهارس مباشرة، وما تحتاج تعد السجلات السابقة. مثالية للـ Infinite Scroll حيث المستخدم ما يهتم برقم الصفحة، بل بالبيانات التالية.
عيوبها: أصعب شوي في تطبيقها مع ترقيم الصفحات التقليدي (اللي فيه أزرار 1، 2، 3...). ما تقدر تنتقل لصفحة معينة مباشرة إلا إذا عرفت آخر
idلتلك الصفحة.
كيف نطبقها في الباك إند (مثال Python/Node.js)
في العادة، الـ API يستقبل بارامترات من الفرونت إند. مثلاً، لـ Offset/Limit ممكن يستقبل page و limit، ولـ Cursor-based ممكن يستقبل last_id و limit.
مثال لـ Offset/Limit (Python Flask):
from flask import Flask, request, jsonify
app = Flask(__name__)
# بيانات وهمية
products_db = [{"id": i, "name": f"Product {i}"} for i in range(1, 101)]
@app.route('/api/products_offset')
def get_products_offset():
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 10))
if page < 1: page = 1
if limit < 1: limit = 10
offset = (page - 1) * limit
paginated_products = products_db[offset : offset + limit]
total_products = len(products_db)
total_pages = (total_products + limit - 1) // limit
return jsonify({
"products": paginated_products,
"page": page,
"limit": limit,
"total_products": total_products,
"total_pages": total_pages
})
if __name__ == '__main__':
app.run(debug=True)
مثال لـ Cursor-based (Python Flask):
from flask import Flask, request, jsonify
app = Flask(__name__)
# بيانات وهمية (مهم تكون مرتبة حسب الـ id)
products_db = sorted([{"id": i, "name": f"Product {i}"} for i in range(1, 101)], key=lambda x: x['id'])
@app.route('/api/products_cursor')
def get_products_cursor():
last_id = int(request.args.get('last_id', 0))
limit = int(request.args.get('limit', 10))
if limit < 1: limit = 10
# فلترة المنتجات اللي الـ id حقها أكبر من last_id
filtered_products = [p for p in products_db if p['id'] > last_id]
# جلب العدد المطلوب فقط
paginated_products = filtered_products[:limit]
# تحديد الـ last_id الجديد للطلب القادم
next_last_id = paginated_products[-1]['id'] if paginated_products else last_id
return jsonify({
"products": paginated_products,
"last_id": next_last_id,
"limit": limit,
"has_more": len(filtered_products) > limit # هل يوجد المزيد من البيانات بعد هذه الصفحة؟
})
if __name__ == '__main__':
app.run(debug=True)
التعامل معها في الفرونت إند (مثال JavaScript)
سواء كنت تستخدم أزرار ترقيم صفحات أو Infinite Scroll، الفكرة هي إرسال البارامترات الصحيحة للباك إند وتحديث الواجهة بالبيانات الجديدة.
مثال لـ Offset/Limit (أزرار ترقيم):
let currentPage = 1;
const limit = 10;
const fetchProductsOffset = async () => {
const response = await fetch(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">/api/products_offset?page=${currentPage}&limit=${limit}</code>);
const data = await response.json();
const productsDiv = document.getElementById('products-list');
productsDiv.innerHTML = ''; // مسح القديم
data.products.forEach(product => {
const p = document.createElement('p');
p.textContent = <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">ID: ${product.id}, Name: ${product.name}</code>;
productsDiv.appendChild(p);
});
document.getElementById('current-page').textContent = <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">الصفحة: ${data.page} من ${data.total_pages}</code>;
document.getElementById('prev-btn').disabled = (data.page === 1);
document.getElementById('next-btn').disabled = (data.page === data.total_pages);
};
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
fetchProductsOffset();
}
});
document.getElementById('next-btn').addEventListener('click', () => {
// تحتاج تجلب total_pages أولاً أو من الـ API
// نفترض إنها متاحة في 'data' بعد أول جلب
if (currentPage < /* total_pages من الـ API */ 10) { // مثال بسيط
currentPage++;
fetchProductsOffset();
}
});
// استدعاء أول مرة عند تحميل الصفحة
// fetchProductsOffset();
مثال لـ Cursor-based (Infinite Scroll):
let lastProductId = 0; // تبدأ من 0 أو من آخر ID معروف لديك
const limit = 10;
let isLoading = false;
let hasMore = true;
const fetchProductsCursor = async () => {
if (isLoading || !hasMore) return;
isLoading = true;
const response = await fetch(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">/api/products_cursor?last_id=${lastProductId}&limit=${limit}</code>);
const data = await response.json();
const productsDiv = document.getElementById('products-list-infinite');
data.products.forEach(product => {
const p = document.createElement('p');
p.textContent = <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">ID: ${product.id}, Name: ${product.name}</code>;
productsDiv.appendChild(p);
});
if (data.products.length > 0) {
lastProductId = data.products[data.products.length - 1].id;
}
hasMore = data.has_more;
isLoading = false;
};
window.addEventListener('scroll', () => {
// إذا وصل المستخدم لآخر الصفحة
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) { // -100 بكسل عشان يجلب قبل ما يوصل للآخر بالضبط
fetchProductsCursor();
}
});
// استدعاء أول مرة عند تحميل الصفحة
// fetchProductsCursor();
نصائح ذهبية للتعامل مع الـ Pagination
- التحقق من المدخلات (Input Validation): دائماً تحقق من قيم
page،limit،last_idاللي تجيك من الفرونت إند. تأكد إنها أرقام صحيحة وإيجابية وضمن نطاق معقول عشان تتجنب أي مشاكل أمنية أو أخطاء في السكربت. - الفهارس (Indexes): تأكد إن الأعمدة اللي تستخدمها في جملة
ORDER BYأوWHERE(مثلidأوcreated_at) عليها فهارس في قاعدة البيانات. هذا بيسرع عمليات الجلب بشكل خيالي، خصوصاً مع الـ Cursor-based pagination. - تجربة المستخدم (UX): وضح للمستخدم عدد الصفحات الكلي (إذا كنت تستخدم Offset/Limit) أو إذا كان فيه بيانات زيادة ممكن يجلبها (لـ Infinite Scroll). لا تخليه يتساءل هل فيه المزيد أو لا.
- التعامل مع الفشل (Error Handling): كيف السكربت يتصرف لو ما لقى بيانات أو صار خطأ في الاتصال بقاعدة البيانات؟ دائماً جهز رسائل خطأ واضحة.
- الحماية من هجمات DoS: لا تسمح بقيم
limitكبيرة جداً (مثلاً،limit=1000000). حدد أقصى قيمة مسموحة للـlimitفي الباك إند.
الـ Pagination مو بس ميزة، هي ضرورة لأي تطبيق يتعامل مع كميات كبيرة من البيانات. بتخلي تطبيقك أسرع، أكثر استقراراً، وأكثر قابلية للتوسع. يلا طبقوها صح واستمتعوا بأداء أفضل!