نظرة عامة
يتيح إصدار 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"
}
ميزة "عدم الانضمام" (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 ميغابايت على البيانات المادية في طلب البحث بأكمله، بما في ذلك جميع المستندات المدمجة.