استعلامات Firestore المتقدمة: الشروط المركبة والترقيم
في درس Firestore كتبت استعلاماً بـ where و orderBy. يكفي ذلك لتطبيق تجريبي، لكن التطبيق الحقيقي يحتاج أكثر: شروط مركبة، وقائمة تُحمَّل عشرة عشرة مع التمرير بدل تحميل آلاف المستندات دفعة واحدة، وعدّاد لا يكلفك قراءة كل مستند. في هذا الدرس ستتعلم هذه الأدوات، وهي ما يجعل تطبيقك سريعاً وفاتورتك صغيرة.
سنعمل على مجموعة students وفيها سبعة طلاب: سارة (القاهرة، 92)، وعمر (الإسكندرية، 78)، ومنى (طنطا، 85)، ويوسف (القاهرة، 64)، وليلى (أسيوط، 71)، وأحمد (الإسكندرية، 88)، وهدى (القاهرة، 95). ولكل طالب مصفوفة hobbies.
دمج الشروط
Section titled “دمج الشروط”عدة where في الاستعلام الواحد تعني «و»: يجب أن تتحقق كلها.
const col = collection(db, 'students');
const q = query(col, where('city', '==', 'القاهرة'), where('score', '>=', 90));const result = await getDocs(q);result.forEach((d) => console.log(d.data().name, d.data().score));سارة 92هدى 95ولمدى بين قيمتين اكتب شرطين على الحقل نفسه:
query(col, where('score', '>=', 70), where('score', '<', 90), orderBy('score'))ليلى 71عمر 78منى 85أحمد 88or: هذا أو ذاك
Section titled “or: هذا أو ذاك”للشرط «أو» استخدم الدالة or، وللدمج بينهما and:
import { or, and } from 'firebase/firestore';
query(col, or( where('city', '==', 'طنطا'), where('score', '>=', 95)))منى 85هدى 95query(col, and( where('score', '>=', 80), or(where('city', '==', 'القاهرة'), where('city', '==', 'طنطا'))))منى 85سارة 92هدى 95«درجته 80 فأكثر، وهو من القاهرة أو طنطا».
in: ضمن قائمة
Section titled “in: ضمن قائمة”عندما تقارن الحقل نفسه بعدة قيم، in أقصر من سلسلة or:
query(col, where('city', 'in', ['طنطا', 'أسيوط']))ليلى 71منى 85وعكسها not-in. والقائمة لها حد أقصى: جرّبنا 31 قيمة فرفض Firestore الاستعلام بالخطأ invalid-argument ورسالة: 'IN' supports up to 30 comparison values.
البحث في المصفوفات
Section titled “البحث في المصفوفات”حقل hobbies مصفوفة. من يهوى القراءة؟
query(col, where('hobbies', 'array-contains', 'قراءة'))الناتج: هدى ومنى وسارة. و array-contains-any تبحث عن أي قيمة من قائمة:
query(col, where('hobbies', 'array-contains-any', ['شطرنج', 'رسم']))الناتج: أحمد ومنى ويوسف.
| المعامل | يتحقق إذا |
|---|---|
in |
قيمة الحقل إحدى قيم القائمة |
not-in |
قيمة الحقل ليست في القائمة |
array-contains |
المصفوفة تحتوي القيمة |
array-contains-any |
المصفوفة تحتوي أي قيمة من القائمة |
الترقيم: صفحة صفحة
Section titled “الترقيم: صفحة صفحة”تحميل كل المستندات مرة واحدة بطيء ومكلف: Firestore تحاسبك على كل مستند يُقرأ. الحل أن تجلب صفحة صغيرة، ثم التالية عند الحاجة. هذا اسمه الترقيم (Pagination)، وهو ما يحدث حين تمرر قائمة لا تنتهي في أي تطبيق.
الصفحة الأولى بـ limit:
import { limit, startAfter } from 'firebase/firestore';
const page1 = await getDocs(query(col, orderBy('score', 'desc'), limit(3)));page1.forEach((d) => console.log(d.data().name, d.data().score));هدى 95سارة 92أحمد 88وللصفحة التالية نحتفظ بآخر مستند وصلنا إليه، ونطلب ما بعده بـ startAfter:
const last = page1.docs[page1.docs.length - 1];
const page2 = await getDocs(query(col, orderBy('score', 'desc'), startAfter(last), limit(3)));منى 85عمر 78ليلى 71آخر مستند يعمل مثل الإشارة المرجعية في كتاب: تكمل القراءة من حيث توقفت، دون إعادة عدّ الصفحات من البداية.
وكيف تعرف أن النتائج انتهت؟ الصفحة الثالثة أعادت مستنداً واحداً (يوسف 64)، أي أقل من الحد 3. والرابعة جاءت فارغة: size يساوي 0 و empty يساوي true. عندها أخفِ زر «تحميل المزيد».
العدّ والجمع من الخادم
Section titled “العدّ والجمع من الخادم”تريد عرض «عدد الطلاب: 7». جلب المستندات كلها لتعدّها يعني سبع قراءات، وفي مجموعة حقيقية مئة ألف قراءة! الدالة getCountFromServer تجعل الخادم يعدّ ويرسل لك الرقم فقط:
import { getCountFromServer, getAggregateFromServer, sum, average } from 'firebase/firestore';
const all = await getCountFromServer(col);console.log(all.data().count);
const cairo = await getCountFromServer(query(col, where('city', '==', 'القاهرة')));console.log(cairo.data().count);
const stats = await getAggregateFromServer(col, { total: sum('score'), avg: average('score')});console.log(stats.data().total, stats.data().avg);73573 81.85714285714286وتعمل مع أي استعلام، و sum و average للمجموع والمتوسط بالطريقة نفسها.
المجموعات الفرعية
Section titled “المجموعات الفرعية”المستند يمكن أن يحتوي مجموعات بداخله. درجات كل طالب في المواد:
await setDoc(doc(db, 'students', 'sara', 'grades', 'math'), { subject: 'رياضيات', mark: 95 });await setDoc(doc(db, 'students', 'sara', 'grades', 'science'), { subject: 'علوم', mark: 89 });
const grades = await getDocs(query( collection(db, 'students', 'sara', 'grades'), orderBy('mark', 'desc')));grades.forEach((d) => console.log(d.data().subject, d.data().mark));رياضيات 95علوم 89المسار يتناوب: مجموعة، مستند، مجموعة، مستند. وقراءة مستند سارة لا تجلب درجاتها: حقوله ما زالت city و hobbies و name و score فقط. المجموعة الفرعية تُقرأ عند طلبها، ولهذا تناسب البيانات الكثيرة مثل الرسائل داخل محادثة.
وللبحث في مجموعات grades عند كل الطلاب معاً:
import { collectionGroup } from 'firebase/firestore';
query(collectionGroup(db, 'grades'), where('subject', '==', 'رياضيات'))أعاد درجتي الرياضيات لسارة (95) ولعمر (70) من مجموعتين فرعيتين مختلفتين. واستعلام كهذا يحتاج قاعدة أمان خاصة مسارها match /{path=**}/grades/{gradeId}.
قيود يجب أن تعرفها
Section titled “قيود يجب أن تعرفها”Firestore سريعة لأنها تجيب عن كل استعلام من فهرس. ولهذا ثمن:
- الفهارس المركبة: استعلام يجمع
whereعلى حقل معorderByعلى حقل آخر يحتاج فهرساً مركباً. المحاكي لا يطلبه، لكن المشروع الحقيقي يرفض الاستعلام برسالة فيها رابط ينشئ الفهرس المطلوب بضغطة. - الحقل الغائب: أضفنا طالبة بلا حقل
score، فأعادorderBy('score')سبعة مستندات من ثمانية. المستند الذي لا يحتوي حقل الترتيب لا يظهر في النتيجة. - لا بحث نصياً: لا يوجد مقابل لـ
LIKE '%كلمة%'. للبحث داخل النصوص تُستخدم خدمة بحث متخصصة.
جرّب بنفسك
Section titled “جرّب بنفسك”- اعرض طلاب الإسكندرية الذين درجتهم أقل من 80.
- اعرض الطلاب الذين يهوون «كرة القدم» أو «سباحة» باستعلام واحد.
- اعرض الطلاب مرتبين بالاسم، صفحتين متتاليتين في كل منهما طالبان.
- تحدٍّ: اكتب دالة
loadMore()تحتفظ بآخر مستند في متغير، وتطبع الصفحة التالية في كل استدعاء، وتطبع «انتهت النتائج» عندما تأتي الصفحة فارغة.
أخطاء شائعة
Section titled “أخطاء شائعة”- جلب الكل ثم التصفية في الكود: تدفع قراءة كل مستند وتبطئ التطبيق. اجعل Firestore تصفّي بـ
whereوlimit. - العدّ بـ
getDocs(...).size: يقرأ كل المستندات. استخدمgetCountFromServer. - ترقيم بلا
orderBy: أو بترتيب يختلف بين الصفحات، فتتكرر عناصر وتضيع أخرى. - تجاهل رسالة الفهرس: خطأ
failed-preconditionمع رابط ليس عطلاً. افتح الرابط وأنشئ الفهرس وانتظر دقائق.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الذي نمرره إلى startAfter لجلب الصفحة التالية؟
Firestore تكمل من بعد مستند محدد، فلا تعيد قراءة ما سبق ولا تحاسبك عليه.
سؤال
لماذا نفضّل getCountFromServer على جلب المستندات وعدّها؟
التكلفة في Firestore بعدد المستندات المقروءة، والعدّ من الخادم يتفادى قراءتها.
الخلاصة
Section titled “الخلاصة”- عدة
whereتعني «و»، وorوandتبنيان شروطاً مركبة. inوnot-inلقائمة قيم، وarray-containsللبحث داخل المصفوفات.- الترقيم:
orderByثمlimit، والصفحة التالية بـstartAfter(آخر مستند). getCountFromServerوgetAggregateFromServerيحسبان على الخادم دون قراءة المستندات.- في الدرس القادم ستتعلم تخزين الصور والملفات بخدمة Cloud Storage.