نظرة عامة
يتيح إصدار Firestore Enterprise عمليات الربط المستندة إلى العلاقات من خلال الاستعلامات الفرعية المرتبطة. على عكس العديد من قواعد بيانات NoSQL التي تتطلّب غالبًا إلغاء تسوية البيانات أو تنفيذ طلبات متعددة من جهة العميل، تتيح لك الاستعلامات الفرعية دمج البيانات وتجميعها من المجموعات أو المجموعات الفرعية ذات الصلة مباشرةً على الخادم.
الاستعلامات الفرعية هي عبارات تنفّذ مسارًا متداخلاً لكل مستند تتم معالجته بواسطة الاستعلام الخارجي. يتيح ذلك أنماطًا معقّدة لاسترداد البيانات، مثل جلب مستند مع عناصر المجموعة الفرعية ذات الصلة أو ربط البيانات المرتبطة منطقيًا من خلال مجموعات جذرية مختلفة.
المفاهيم
يعرض هذا القسم المفاهيم الأساسية لاستخدام الطلبات الفرعية لتنفيذ عمليات الربط في عمليات Pipeline.
الاستعلامات الفرعية كتعبيرات
الاستعلام الفرعي ليس مرحلة على المستوى الأعلى، بل هو تعبير يمكن استخدامه في أي مرحلة تقبل التعبيرات، مثل select(...) أو add_fields(...) أو where(...) أو sort(...).
يتيح Cloud Firestore ثلاثة أنواع من الطلبات الفرعية:
- الاستعلامات الفرعية للمصفوفات: يتم إنشاء مجموعة النتائج الكاملة للاستعلام الفرعي كمصفوفة من المستندات.
- الطلبات الفرعية العددية: يتم تقييمها إلى قيمة واحدة، مثل عدد أو متوسط أو حقل معيّن من مستند ذي صلة.
subcollection(...)الطلبات الفرعية: عمليات ربط مبسطة لعلاقة العنصر الرئيسي بالعناصر الثانوية من النوع واحد إلى متعدد.
النطاق والمتغيرات
عند كتابة عملية ربط، غالبًا ما تحتاج الاستعلام الفرعي المتداخل إلى الإشارة إلى حقول من المستند "الخارجي" (العنصر الرئيسي). لربط هذه النطاقات، يمكنك استخدام مرحلة let(...) (يُشار إليها باسم define(...) في بعض حِزم تطوير البرامج (SDK)) لتحديد المتغيرات في النطاق الرئيسي التي يمكن الرجوع إليها بعد ذلك في الاستعلام الفرعي باستخدام الدالة variable(...).
البنية
تقدّم الأقسام التالية نظرة عامة على صيغة تنفيذ عمليات الربط.
المرحلة let(...)
المرحلة let(...) (المشار إليها باسم define(...) في بعض حِزم SDK) هي مرحلة غير فلترة تجلب البيانات بشكل صريح من النطاق الرئيسي إلى متغيّر مسمّى لاستخدامه في النطاقات المتداخلة اللاحقة.
الاستعلامات الفرعية للمصفوفات
استعلام فرعي للمصفوفة هو حالة خاصة من الاستعلام الفرعي للتعبير الذي يحوّل مجموعة النتائج الكاملة للاستعلام الفرعي إلى مصفوفة. إذا عرضت الاستعلام الفرعي صفر صفوف، سيتم تقييمه إلى مصفوفة فارغة. ولا تعرض أبدًا مصفوفة null. تكون هذه الاستعلامات مفيدة عندما تكون النتائج الكاملة مطلوبة في النتيجة النهائية، مثلما يحدث عند إنشاء مجموعة متداخلة أو مرتبطة.
يمكن لطلبات البحث فلترة البيانات وترتيبها وتجميعها في طلب البحث الفرعي للحدّ أيضًا من كمية البيانات التي يجب جلبها وعرضها، ما يساعد في تقليل تكلفة طلب البحث. يتم احترام ترتيب الاستعلام الفرعي، ما يعني أنّ مرحلة sort(...) في الاستعلام الفرعي تتحكّم في ترتيب النتائج في المصفوفة النهائية.
استخدِم برنامج تضمين حزمة تطوير البرامج (SDK) toArrayExpression() لتحويل طلب بحث إلى مصفوفة.
الاستعلامات الفرعية العددية
يتم غالبًا استخدام الطلبات الفرعية العددية في مرحلة select(...) أو where(...) للسماح بالفلترة أو عرض نتيجة طلب فرعي بدون تنفيذ الطلب الكامل مباشرةً.
سيتم تقييم الاستعلام الفرعي العددي الذي ينتج عنه صفر نتائج إلى null نفسه، بينما سيؤدي الاستعلام الفرعي الذي يتم تقييمه إلى عناصر متعددة إلى حدوث خطأ في وقت التشغيل.
عندما ينتج طلب فرعي عددي حقلاً واحدًا فقط لكل نتيجة، يتم رفع الحقل ليكون النتيجة ذات المستوى الأعلى للطلب الفرعي. يحدث ذلك عادةً عندما ينتهي الاستعلام الفرعي بـ select(field("user_name")) أو aggregate(countAll().as("total"))، حيث يكون مخطط الاستعلام الفرعي عبارة عن حقل واحد فقط. بخلاف ذلك، عندما يمكن أن ينتج طلب فرعي حقولاً متعددة، يتم تضمينها في خريطة.
استخدِم برنامج تضمين حزمة تطوير البرامج (SDK) toScalarExpression() لتحويل طلب بحث إلى تعبير عددي.
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"
}
Anti-Join (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 ميغابايت على البيانات المادية في جميع أجزاء طلب البحث، بما في ذلك جميع المستندات التي تم ربطها.