نظرة عامة
يتيح إصدار Enterprise من Firestore عمليات الربط على النمط العلائقي من خلال الاستعلامات الفرعية المرتبطة. على عكس العديد من قواعد بيانات NoSQL التي تتطلّب غالبًا إلغاء تسوية البيانات أو تنفيذ طلبات متعدّدة من جهة العميل، تسمح لك الاستعلامات الفرعية بدمج البيانات وتجميعها من مجموعات أو مجموعات فرعية ذات صلة مباشرةً على الخادم.
الاستعلامات الفرعية هي تعبيرات تنفّذ مسارًا متداخلاً لكل مستند تتم معالجته بواسطة الاستعلام الخارجي. يتيح ذلك أنماطًا معقّدة لاسترداد البيانات، مثل جلب مستند إلى جانب عناصر المجموعة الفرعية المرتبطة به أو ربط البيانات المرتبطة منطقيًا عبر مجموعات جذر مختلفة.
المفاهيم
يعرض هذا القسم المفاهيم الأساسية لاستخدام الاستعلامات الفرعية لإجراء عمليات الربط في عمليات المسار.
الاستعلامات الفرعية كتعبيرات
الاستعلام الفرعي ليس مرحلة من المستوى الأعلى، بل هو تعبير يمكن استخدامه في أي مرحلة تقبل التعبيرات، مثل select(...)، add_fields(...)، where(...)، أو sort(...).
Cloud Firestore تتوفّر ثلاثة أنواع من الاستعلامات الفرعية في:
- الاستعلامات الفرعية للمصفوفات: يتم إنشاء مجموعة النتائج الكاملة للاستعلام الفرعي كمصفوفة من المستندات.
- الاستعلامات الفرعية للقيم العددية: يتم تقييمها بقيمة واحدة، مثل عدد أو متوسّط أو حقل معيّن من مستند ذي صلة.
- الاستعلامات الفرعية
subcollection(...): عمليات ربط مبسطة لعلاقة بين مستند رئيسي ومستندات فرعية متعددة.
النطاق والمتغيّرات
عند كتابة عملية ربط، يحتاج الاستعلام الفرعي المتداخل غالبًا إلى الإشارة إلى حقول من المستند "الخارجي" (المستند الرئيسي). لربط هذه النطاقات، يمكنك استخدام مرحلة
let(...) (يُشار إليها باسم define(...) في بعض
حِزم تطوير البرامج (SDK)) لتعريف المتغيّرات في النطاق الرئيسي التي يمكن الإشارة إليها بعد ذلك في
الاستعلام الفرعي باستخدام الدالة variable(...).
البنية
تقدّم الأقسام التالية نظرة عامة على بنية إجراء عمليات الربط.
مرحلة let(...)
مرحلة let(...) (يُشار إليها باسم define(...) في بعض
حِزم تطوير البرامج (SDK)) هي مرحلة غير فلترة تجلب البيانات بشكل صريح من النطاق الرئيسي
إلى متغيّر مُسمّى لاستخدامه في النطاقات المتداخلة اللاحقة.
الاستعلامات الفرعية للمصفوفات
الاستعلام الفرعي للمصفوفة هو حالة خاصة من الاستعلام الفرعي للتعبير الذي ينشئ مجموعة النتائج الكاملة للاستعلام الفرعي في مصفوفة. إذا لم يعرض الاستعلام الفرعي أي صفوف، يتم تقييمه كمصفوفة فارغة. ولا يعرض أبدًا مصفوفة null. تكون هذه الاستعلامات مفيدة عندما تكون النتائج الكاملة مطلوبة في النتيجة النهائية، مثل عند إنشاء مجموعة متداخلة أو مرتبطة.
يمكن للاستعلامات فلترة البيانات وترتيبها وتجميعها في الاستعلام الفرعي لتقليل مقدار البيانات التي يجب جلبها وعرضها أيضًا للمساعدة في خفض تكلفة الاستعلام. يتم الالتزام بترتيب الاستعلام الفرعي، ما يعني أنّ مرحلة sort(...) في الاستعلام الفرعي تتحكّم في ترتيب النتائج في المصفوفة النهائية.
استخدِم برنامج التغليف toArrayExpression() في حزمة تطوير البرامج (SDK) لتحويل استعلام إلى مصفوفة.
الاستعلامات الفرعية للقيم العددية
تُستخدَم الاستعلامات الفرعية للقيم العددية غالبًا في مرحلة select(...) أو
where(...) للسماح بفلترة نتيجة الاستعلام الفرعي أو عرضها بدون إنشاء الاستعلام الكامل مباشرةً.
سيتم تقييم الاستعلام الفرعي للقيم العددية الذي لا يعرض أي نتائج على أنّه null، بينما سيؤدي الاستعلام الفرعي الذي يتم تقييمه بعناصر متعدّدة إلى حدوث خطأ في وقت التشغيل.
عندما يعرض استعلام فرعي للقيم العددية حقلًا واحدًا فقط لكل نتيجة، يتم رفع الحقل ليكون النتيجة من المستوى الأعلى للاستعلام الفرعي. يظهر ذلك بشكل شائع عندما ينتهي الاستعلام الفرعي بـ select(field("user_name")) أو
aggregate(countAll().as("total")) حيث يكون مخطط الاستعلام الفرعي حقلًا واحدًا فقط. في ما عدا ذلك، عندما يمكن أن يعرض الاستعلام الفرعي حقولًا متعدّدة، يتم تضمينها في خريطة.
استخدِم برنامج التغليف toScalarExpression() في حزمة تطوير البرامج (SDK) لتحويل استعلام إلى تعبير عددي.
الاستعلامات الفرعية subcollection(...)
تسمح مرحلة الإدخال
subcollection(...) بإجراء عمليات الربط على نموذج البيانات الهرمي في Cloud Firestore، على الرغم من أنّها تُقدَّم كمرحلة. في النموذج الهرمي، تحتاج الاستعلامات غالبًا إلى استرداد مستند إلى جانب البيانات من مجموعاته الفرعية الخاصة. على الرغم من أنّه يمكنك تحقيق ذلك باستخدام مرحلة الإدخال
collection_group(...) متبوعة
بفلتر على المرجع الرئيسي، فإنّ subcollection(...) توفّر بنية أكثر إيجازًا.
بالإضافة إلى شرط الربط الضمني، تعمل هذه المرحلة بشكل مشابه للاستعلام الفرعي للمصفوفة، حيث تعرض نتيجة فارغة إذا لم تتم مطابقة أي مستندات، حتى إذا كانت المجموعة المتداخلة غير متوفّرة.
إنّها في الأساس تجميل لغوي: تستخدم تلقائيًا __name__ المستند في النطاق الخارجي كمفتاح الربط لحل العلاقة الهرمية. وهذا يجعلها الطريقة المفضّلة لإجراء عمليات البحث في المجموعات المرتبطة بعلاقة بين مستند رئيسي ومستند فرعي.
أمثلة
بيانات المثال
يؤدي ما يلي إلى تحميل مجموعة من بيانات الاختبار لاستخدامها في جميع الأمثلة التالية.
Node.js
// Load set of cities.
const cities = collection(db, "cities");
await setDoc(doc(cities, "SF"), {
name: "San Francisco",
state: "CA",
country: "USA",
});
await setDoc(doc(cities, "LA"), {
name: "Los Angeles",
state: "CA",
country: "USA"
});
await setDoc(doc(cities, "DC"), {
name: "Washington, D.C.",
state: null,
country: "USA"
});
await setDoc(doc(cities, "TOK"), {
name: "Tokyo",
state: null,
country: "Japan"
});
// Load restaurants in various cities.
const sfRestaurants = collection(db, "cities", "SF", "restaurants");
const laRestaurants = collection(db, "cities", "LA", "restaurants");
const dcRestaurants = collection(db, "cities", "DC", "restaurants");
const rest1 = await addDoc(sfRestaurants, {
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
});
const rest2 = await addDoc(sfRestaurants, {
name: "Bay Area Burger",
type: "burger",
owner_id: "Sarah Jenkins"
});
const rest3 = await addDoc(sfRestaurants, {
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
});
const rest4 = await addDoc(laRestaurants, {
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
});
const rest5 = await addDoc(laRestaurants, {
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano"
});
const rest6 = await addDoc(dcRestaurants, {
name: "Capitol Tacos",
type: "mexican",
owner_id: "Maria Garcia"
});
const rest7 = await addDoc(dcRestaurants, {
name: "Georgetown Coffee",
type: "cafe",
owner_id: "David Kim"
});
// Load collection of reviews.
const reviews = collection(db, "reviews");
await addDoc(reviews, { restaurant: rest1, rating: 5, reviewer_id "Alice" });
await addDoc(reviews, { restaurant: rest1, rating: 4, reviewer_id "Bob" });
await addDoc(reviews, { restaurant: rest2, rating: 4, reviewer_id "Charlie" });
await addDoc(reviews, { restaurant: rest3, rating: 5, reviewer_id "Diana" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Edward" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Fiona" });
// rest4 has 0 reviews
await addDoc(reviews, { restaurant: rest5, rating: 3, reviewer_id "George" });
await addDoc(reviews, { restaurant: rest6, rating: 5, reviewer_id "Hannah" });
await addDoc(reviews, { restaurant: rest6, rating: 4, reviewer_id "Ian" });
await addDoc(reviews, { restaurant: rest7, rating: 5, reviewer_id "Julia" });
البحث عن مستند في مجموعة أخرى
يُجري الاستعلام التالي على حزمة المجموعات reviews عملية بحث في حزمة المجموعات restaurant باستخدام مرجع المفتاح الأساسي.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("reviews")
.define(field("restaurant").as("restaurant_name"))
.addFields(db.pipeline()
.collectionGroup("restaurant")
.where(field("__name__").equal(variable("restaurant_name")))
.select("name", "type")
.toScalarExpression()
.as("restaurant")));
الردّ
{
rating: 5,
reviewer_id "Alice",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Bob",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Charlie",
restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
rating: 5,
reviewer_id "Diana",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Edward",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Fiona",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 3,
reviewer_id "George",
restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
rating: 5,
reviewer_id "Hannah",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 4,
reviewer_id "Ian",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 5,
reviewer_id "Julia",
restaurant: { name: "Georgetown Coffee", type: "cafe" }
}
دمج مجموعات متعدّدة
يجلب الاستعلام التالي جميع مطاعم البيتزا من مجموعة restaurants، ويستخدم استعلامًا فرعيًا للمصفوفة لجلب المراجعات المرتبطة بها وتضمينها مباشرةً في الردّ.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("reviews")));
الردّ
{
name: "Golden Gate Pizza",
reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
reviews: [
{ rating: 3, reviewer_id "George" }
]
}
التجميع عبر مجموعات متعدّدة
يستخدم الاستعلام التالي على مجموعة restaurants استعلامًا فرعيًا مرتبطًا للحصول على متوسّط التقييم لكل مطعم من مجموعة reviews.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(average("rating").as("avg_rating"))
.toScalarExpression()
.as("avg_rating")));
الردّ
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
أفضل N عنصر لكل مجموعة (استعلام فرعي مع حدّ)
يجلب الاستعلام التالي جميع المستندات من مجموعة restaurants، ويستخدم استعلامًا فرعيًا مرتبطًا لجلب أفضل مراجعتَين لكل مطعم من حيث التقييم.
يضمن ذلك ألا يصبح مصفوفة المراجعات كبيرة جدًا وأن لا تتجاوز حدّ الذاكرة للاستعلام.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.sort(field("rating").descending())
.limit(2)
.select("rating", "reviewer_id")
.toArrayExpression()
.as("top_reviews")));
الردّ
{
name: "Golden Gate Pizza",
top_reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Bay Area Burger",
top_reviews: [
{ rating: 4, reviewer_id "Charlie" }
]
},
{
name: "Sunset Taco",
top_reviews: [
{ rating: 5, reviewer_id "Diana" },
{ rating: 4, reviewer_id "Edward" }
]
},
{
name: "Hollywood Sushi",
top_reviews: []
},
{
name: "Venice Pizza",
top_reviews: [
{ rating: 3, reviewer_id "George" }
]
},
{
name: "Capitol Tacos",
top_reviews: [
{ rating: 5, reviewer_id "Hannah" },
{ rating: 4, reviewer_id "Ian" }
]
},
{
name: "Georgetown Coffee",
top_reviews: [
{ rating: 5, reviewer_id "Julia" }
]
}
ربط المجموعات الفرعية
يفحص الاستعلام التالي مجموعة cities ويستخدم مرحلة
subcollection(...) لربط المستندات ضمنيًا
من مجموعة متداخلة للعثور على عدد المطاعم لكل
مدينة.
Node.js
let results = await execute(db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count")));
الردّ
{
__name__: cities/SF,
name: "San Francisco",
state: "CA",
country: "USA",
restaurant_count: 3
},
{
__name__: cities/LA,
name: "Los Angeles",
state: "CA",
country: "USA",
restaurant_count: 2
},
{
__name__: cities/DC,
name: "Washington, D.C.",
state: null,
country: "USA",
restaurant_count: 2
},
{
__name__: cities/TOK,
name: "Tokyo",
state: null,
country: "Japan",
restaurant_count: 0
}
التعبير عن شروط ربط متعدّدة
يفحص الاستعلام التالي حزمة المجموعات restaurants ويُجري عملية ربط متعدّدة الحقول مع حزمة المجموعات reviews للعثور على مالكين يراجعون مطاعمهم.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("owner_id"), field("__name__"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("__name__")))
.where(field("author").equal(variable("owner_id")))
.aggregate(count().as("c"))
.toScalarExpression()
.greaterThan(0)));
الردّ
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
عملية الربط العكسي (NOT EXISTS)
يفحص الاستعلام التالي مجموعة restaurants ويجد جميع المطاعم التي لم تتلقَّ أي مراجعات بعد.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(count().as("review_count"))
.toScalarExpression()
.equal(0)));
الردّ
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
الاستعلام الفرعي كعملية ربط
يُسطّح الاستعلام التالي العلاقة بين كل مطعم بيتزا ومراجعاته. من خلال وضع الاستعلام الفرعي داخل مرحلة
unnest(...)، يكرّر الخادم مستند المطعم الخارجي
لكل مراجعة مطابقة، ما يؤدي إلى إنشاء مستندات مسطّحة ومربوطة
(مشابهة لـ INNER JOIN في SQL).
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.unnest(
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("review")));
الردّ
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
review: { rating: 5, reviewer_id "Alice" }
},
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi",
review: { rating: 4, reviewer_id "Bob" }
},
{
__name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
review: { rating: 3, reviewer_id "George" }
}
الاستعلام الفرعي غير المرتبط كفلتر
يُجري الاستعلام التالي على مجموعة reviews عمليات فلترة باستخدام استعلام فرعي غير مرتبط على نفسه للعثور على المراجعات التي تزيد عن متوسّط التقييم.
Node.js
let results = await execute(db.pipeline()
.collection("reviews")
// Average review rating is 4.3
.where(field("rating").greaterThan(db.pipeline()
.collection("reviews")
.aggregate(average("rating").as("avg"))
.toScalarExpression())))
.select("rating", "reviewer_id");
الردّ
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
أفضل الممارسات
- إدارة الذاكرة باستخدام
toArrayExpression(): يجب توخي الحذر عند استخدام الاستعلامات الفرعيةtoArrayExpression()، لأنّ إنشاء عدد كبير من المستندات قد يؤدي إلى استنفاد حدّ ذاكرة الاستعلام (128 ميغابايت). للتخفيف من ذلك، استخدِمselect(...)ضمن الاستعلام الفرعي لعرض الحقول الضرورية فقط وطبِّق فلاترwhere(...)للحدّ من عدد المستندات المعروضة. ننصحك باستخدامlimit(...)إذا كان ذلك مناسبًا للحدّ من عدد المستندات التي يعرضها الاستعلام الفرعي. - الفهرسة: تأكَّد من فهرسة الحقول المستخدَمة في
where(...)بند برمجي في الاستعلام الفرعي. تعتمد عمليات الربط عالية الأداء على إمكانية إجراء عمليات البحث في الفهرس بدلاً من عمليات الفحص الكامل للجدول.
لمزيد من أفضل الممارسات المتعلّقة بالاستعلامات، يُرجى الرجوع إلى دليل تحسين الاستعلامات.
القيود
subcollection(...)نطاق:لا تتوفّر مرحلة الإدخالsubcollection(...)إلا ضمن الاستعلامات الفرعية، لأنّها تتطلّب سياق مستند رئيسي لحل العلاقة الهرمية وإجراء عملية الربط.- عمق التداخل: يمكن أن تكون الاستعلامات الفرعية متداخلة بعمق يصل إلى 20 طبقة.
- استخدام الذاكرة: ينطبق الحدّ الأقصى البالغ 128 ميغابايت على البيانات التي تم إنشاؤها في الاستعلام بأكمله، بما في ذلك جميع المستندات المربوطة.