تخطَّ إلى المحتوى

استعلامات Firestore المتقدمة: الشروط المركبة والترقيم

الدرس 7 من 9 · Firebase

في درس Firestore كتبت استعلاماً بـ where و orderBy. يكفي ذلك لتطبيق تجريبي، لكن التطبيق الحقيقي يحتاج أكثر: شروط مركبة، وقائمة تُحمَّل عشرة عشرة مع التمرير بدل تحميل آلاف المستندات دفعة واحدة، وعدّاد لا يكلفك قراءة كل مستند. في هذا الدرس ستتعلم هذه الأدوات، وهي ما يجعل تطبيقك سريعاً وفاتورتك صغيرة.

سنعمل على مجموعة students وفيها سبعة طلاب: سارة (القاهرة، 92)، وعمر (الإسكندرية، 78)، ومنى (طنطا، 85)، ويوسف (القاهرة، 64)، وليلى (أسيوط، 71)، وأحمد (الإسكندرية، 88)، وهدى (القاهرة، 95). ولكل طالب مصفوفة hobbies.

عدة 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
أحمد 88

للشرط «أو» استخدم الدالة or، وللدمج بينهما and:

import { or, and } from 'firebase/firestore';
query(col, or(
where('city', '==', 'طنطا'),
where('score', '>=', 95)
))
الناتج
منى 85
هدى 95
query(col, and(
where('score', '>=', 80),
or(where('city', '==', 'القاهرة'), where('city', '==', 'طنطا'))
))
الناتج
منى 85
سارة 92
هدى 95

«درجته 80 فأكثر، وهو من القاهرة أو طنطا».

عندما تقارن الحقل نفسه بعدة قيم، in أقصر من سلسلة or:

query(col, where('city', 'in', ['طنطا', 'أسيوط']))
الناتج
ليلى 71
منى 85

وعكسها not-in. والقائمة لها حد أقصى: جرّبنا 31 قيمة فرفض Firestore الاستعلام بالخطأ invalid-argument ورسالة: 'IN' supports up to 30 comparison values.

حقل 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 المصفوفة تحتوي أي قيمة من القائمة

تحميل كل المستندات مرة واحدة بطيء ومكلف: 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. عندها أخفِ زر «تحميل المزيد».

تريد عرض «عدد الطلاب: 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);
الناتج
7
3
573 81.85714285714286

وتعمل مع أي استعلام، و sum و average للمجموع والمتوسط بالطريقة نفسها.

المستند يمكن أن يحتوي مجموعات بداخله. درجات كل طالب في المواد:

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}.

Firestore سريعة لأنها تجيب عن كل استعلام من فهرس. ولهذا ثمن:

  • الفهارس المركبة: استعلام يجمع where على حقل مع orderBy على حقل آخر يحتاج فهرساً مركباً. المحاكي لا يطلبه، لكن المشروع الحقيقي يرفض الاستعلام برسالة فيها رابط ينشئ الفهرس المطلوب بضغطة.
  • الحقل الغائب: أضفنا طالبة بلا حقل score، فأعاد orderBy('score') سبعة مستندات من ثمانية. المستند الذي لا يحتوي حقل الترتيب لا يظهر في النتيجة.
  • لا بحث نصياً: لا يوجد مقابل لـ LIKE '%كلمة%'. للبحث داخل النصوص تُستخدم خدمة بحث متخصصة.
  1. اعرض طلاب الإسكندرية الذين درجتهم أقل من 80.
  2. اعرض الطلاب الذين يهوون «كرة القدم» أو «سباحة» باستعلام واحد.
  3. اعرض الطلاب مرتبين بالاسم، صفحتين متتاليتين في كل منهما طالبان.
  4. تحدٍّ: اكتب دالة loadMore() تحتفظ بآخر مستند في متغير، وتطبع الصفحة التالية في كل استدعاء، وتطبع «انتهت النتائج» عندما تأتي الصفحة فارغة.
  • جلب الكل ثم التصفية في الكود: تدفع قراءة كل مستند وتبطئ التطبيق. اجعل Firestore تصفّي بـ where و limit.
  • العدّ بـ getDocs(...).size: يقرأ كل المستندات. استخدم getCountFromServer.
  • ترقيم بلا orderBy: أو بترتيب يختلف بين الصفحات، فتتكرر عناصر وتضيع أخرى.
  • تجاهل رسالة الفهرس: خطأ failed-precondition مع رابط ليس عطلاً. افتح الرابط وأنشئ الفهرس وانتظر دقائق.

سؤال

ما الذي نمرره إلى startAfter لجلب الصفحة التالية؟

سؤال

لماذا نفضّل getCountFromServer على جلب المستندات وعدّها؟

  • عدة where تعني «و»، و or و and تبنيان شروطاً مركبة.
  • in و not-in لقائمة قيم، و array-contains للبحث داخل المصفوفات.
  • الترقيم: orderBy ثم limit، والصفحة التالية بـ startAfter(آخر مستند).
  • getCountFromServer و getAggregateFromServer يحسبان على الخادم دون قراءة المستندات.
  • في الدرس القادم ستتعلم تخزين الصور والملفات بخدمة Cloud Storage.