Cloud Firestore — الاستعلامات والفلاتر والترقيم الصفحي
Cloud Firestore — الاستعلامات والفلاتر والترقيم الصفحي
Firestore ليست مجرد مخزن مفتاح-قيمة — بل هي قاعدة بيانات مستندات NoSQL قوية تمتلك واجهة استعلام غنية. يتناول هذا الدرس كيفية بناء استعلامات مركبة ودقيقة باستخدام where() وorderBy() وlimit()، وكيفية تنفيذ الترقيم الصفحي القائم على المؤشر مع startAfterDocument()، ولماذا تُعدّ الفهارس المركبة ضرورية في تطبيقات الإنتاج.
الفلترة الأساسية باستخدام where()
تقوم دالة where() بفلترة المستندات استناداً إلى قيم الحقول. يدعم Firestore مشغلات المقارنة التالية:
isEqualTo،isNotEqualToisLessThan،isLessThanOrEqualToisGreaterThan،isGreaterThanOrEqualToarrayContains،arrayContainsAnywhereIn،whereNotIn
استعلام where() بسيط
import 'package:cloud_firestore/cloud_firestore.dart';
Future<List<Map<String, dynamic>>> getPublishedPosts() async {
final snapshot = await FirebaseFirestore.instance
.collection('posts')
.where('status', isEqualTo: 'published')
.where('viewCount', isGreaterThan: 100)
.get();
return snapshot.docs
.map((doc) => {'id': doc.id, ...doc.data()})
.toList();
}
where() يُنشئ منطق AND بين الشروط. لا يدعم Firestore عملية OR بين حقول مختلفة بصورة أصيلة — لهذا النمط تحتاج إلى تشغيل استعلامين منفصلين ودمج النتائج في Dart.ترتيب النتائج باستخدام orderBy()
تقوم دالة orderBy() بفرز المستندات المُعادة. يمكنك ربط عدة استدعاءات لـ orderBy() لتطبيق مفاتيح فرز ثانوية. مرر descending: true لعكس الترتيب.
where() مع مشغل عدم المساواة (isLessThan، isGreaterThan، إلخ) على حقل ما، فيجب أن يُرتّب أول orderBy() في السلسلة على ذلك الحقل ذاته بالضرورة. عدم الالتزام بذلك سيُلقي استثناءً في وقت التشغيل.استعلام مركب مع orderBy() و limit()
Future<QuerySnapshot<Map<String, dynamic>>> getTopRatedProducts() async {
return FirebaseFirestore.instance
.collection('products')
.where('inStock', isEqualTo: true)
.where('rating', isGreaterThanOrEqualTo: 4.0)
.orderBy('rating', descending: true) // يجب الترتيب بحقل عدم المساواة أولاً
.orderBy('createdAt', descending: true) // ثم الترتيب الثانوي
.limit(20)
.get();
}
الترقيم الصفحي القائم على المؤشر مع startAfterDocument()
لا يدعم Firestore الترقيم الصفحي القائم على الإزاحة (offset: n). عوضاً عن ذلك، يستخدم Firestore مؤشرات الاستعلام للتنقل عبر النتائج بكفاءة. الدالة الرئيسية هي startAfterDocument(lastDoc)، التي تستأنف الاستعلام ابتداءً من المستند الذي يلي آخر مستند جلبته.
startAfterDocument(doc)— البدء بعد المستند المحدد (حصري)startAtDocument(doc)— البدء عند المستند المحدد (شامل)endBeforeDocument(doc)— التوقف قبل المستندendAtDocument(doc)— التوقف عند المستند (شامل)
خلاصة أخبار مُرقَّمة مع startAfterDocument()
class PostFeed {
static const int _pageSize = 10;
DocumentSnapshot? _lastDocument;
bool _hasMore = true;
final List<Map<String, dynamic>> _posts = [];
Future<void> loadNextPage() async {
if (!_hasMore) return;
Query<Map<String, dynamic>> query = FirebaseFirestore.instance
.collection('posts')
.where('status', isEqualTo: 'published')
.orderBy('publishedAt', descending: true)
.limit(_pageSize);
// تطبيق المؤشر إذا لم تكن هذه الصفحة الأولى
if (_lastDocument != null) {
query = query.startAfterDocument(_lastDocument!);
}
final snapshot = await query.get();
if (snapshot.docs.length < _pageSize) {
_hasMore = false; // وصلنا إلى نهاية المجموعة
}
if (snapshot.docs.isNotEmpty) {
_lastDocument = snapshot.docs.last;
_posts.addAll(snapshot.docs.map((d) => {'id': d.id, ...d.data()}));
}
}
List<Map<String, dynamic>> get posts => List.unmodifiable(_posts);
bool get hasMore => _hasMore;
}
DocumentSnapshot بالكامل كمؤشر — وليس فقط معرّف المستند أو قيمة حقل. يستخدم Firestore داخلياً اللقطة لتحديد موضع المؤشر بدقة، بما في ذلك كسر التعادل في حقول الفرز الثانوية. فقدان مرجع اللقطة يعني عدم القدرة على الترقيم الصحيح.الفهارس المركبة
كل استدعاء orderBy() مقروناً بـ where() على حقل مختلف يتطلب فهرساً مركباً في Firestore. بدونه، يُلقي SDK خطأ FAILED_PRECONDITION ويتضمن في رسالة الخطأ رابطاً مباشراً لإنشاء الفهرس في Firebase Console بنقرة واحدة.
- تُنشأ فهارس الحقل الفردي تلقائياً بواسطة Firestore.
- يجب تعريف الفهارس المركبة (متعددة الحقول) صراحةً في
firestore.indexes.jsonأو عبر Console. - الفهارس خاصة بالمجموعة وتتسق تدريجياً — قد تستغرق دقائق بعد الإنشاء.
- نشر الفهارس يتم بالأمر:
firebase deploy --only firestore:indexes.
firestore.indexes.json وأضفها لنظام التحكم بالإصدار حتى يتشارك أعضاء الفريق وبيئة CI نفس إعدادات الفهارس.الترقيم الصفحي في الوقت الفعلي مع StreamBuilder
للخلاصات ذات التحديث الفوري، ادمج Query مع StreamBuilder. احتفظ بمرجع تفاعلي للاستعلام الحالي وحدّثه عندما يطلب المستخدم المزيد من البيانات. بما أن كل صفحة هي لقطة مجرى منفصلة، فإن دمج الصفحات يتطلب الحفاظ على قائمة محلية تجمع النتائج عبر الصفحات.
الخلاصة
تغطي واجهة الاستعلام في Firestore — where() وorderBy() وlimit() والترقيم الصفحي القائم على المؤشر — الغالبية العظمى من احتياجات جلب البيانات في العالم الحقيقي. الانضباط الأساسي هو التخطيط للفهارس مسبقاً، والترقيم دائماً بلقطات المستندات بدلاً من الإزاحات، واحترام القيد القائل إن فلاتر عدم المساواة يجب أن تأتي أولاً في سلسلة orderBy(). تتكامل هذه الأنماط بسلاسة سواء كنت تبني جلب Future لمرة واحدة أو Stream ذا تحديث فوري.