نظرة عامة
يتيح إصدار Firestore Enterprise عمليات الربط ذات النمط العلائقي من خلال الطلبات الفرعية المرتبطة. على عكس العديد من قواعد بيانات NoSQL التي تتطلّب غالبًا إلغاء تسوية البيانات أو تنفيذ طلبات متعددة من جهة العميل، تتيح لك طلبات البحث الفرعية دمج البيانات وتجميعها من المجموعات أو المجموعات الفرعية ذات الصلة مباشرةً على الخادم.
الاستعلامات الفرعية هي تعبيرات تنفّذ مسارًا متداخلاً لكل مستند تتم معالجته بواسطة الاستعلام الخارجي. يتيح ذلك أنماطًا معقّدة لاسترداد البيانات، مثل جلب مستند مع عناصر المجموعة الفرعية ذات الصلة أو دمج البيانات المرتبطة منطقيًا في مجموعات جذرية مختلفة.
المفاهيم
يتناول هذا القسم المفاهيم الأساسية وراء استخدام طلبات البحث الفرعية لإجراء عمليات الربط في عمليات Pipeline.
الاستعلامات الفرعية كتعبيرات
لا يُعدّ الطلب الفرعي مرحلة ذات مستوى أعلى، بل هو تعبير يمكن استخدامه في أي مرحلة تقبل التعبيرات، مثل select(...) أو add_fields(...) أو where(...) أو sort(...).
يتيح Cloud Firestore ثلاثة أنواع من طلبات البحث الفرعية:
- طلبات البحث الفرعية عن المصفوفات: يمكنك تجسيد مجموعة النتائج الكاملة لطلب البحث الفرعي كمصفوفة من المستندات.
- طلبات البحث الفرعية العددية: يتم تقييمها إلى قيمة واحدة، مثل عدد أو متوسط أو حقل معيّن من مستند ذي صلة.
subcollection(...)طلبات البحث الفرعية: عمليات دمج مبسّطة لعلاقة واحد لمتعدد بين العنصر الرئيسي والعنصر الثانوي
النطاق والمتغيّرات
عند كتابة عملية دمج، غالبًا ما يحتاج الطلب الفرعي المتداخل إلى الإشارة إلى حقول
من المستند "الخارجي" (المستند الرئيسي). وللربط بين هذين النطاقَين، يمكنك استخدام المرحلة let(...) (يُشار إليها باسم define(...) في بعض حزم تطوير البرامج) لتحديد المتغيّرات في النطاق الرئيسي التي يمكن بعد ذلك الإشارة إليها في طلب البحث الفرعي باستخدام الدالة variable(...).
البنية
تقدّم الأقسام التالية نظرة عامة على البنية اللغوية لإجراء عمليات الدمج.
المرحلة let(...)
مرحلة let(...) (يُشار إليها باسم define(...) في بعض حزم تطوير البرامج) هي مرحلة لا تتم فيها الفلترة، وتجلب البيانات بشكل صريح من النطاق الرئيسي إلى متغيّر مُسمّى لاستخدامه في النطاقات المُدمجة اللاحقة.
الاستعلامات الفرعية للمصفوفات
"طلب البحث الفرعي عن مصفوفة" هو حالة خاصة من "طلب البحث الفرعي عن تعبير"، ويتم فيه تجميع مجموعة النتائج الكاملة لطلب البحث الفرعي في مصفوفة. إذا عرض الاستعلام الفرعي صفوفًا صفرية،
فسيتم تقييمه إلى مصفوفة فارغة. ولا تعرض مصفوفة 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 = db.collection("cities");
await cities.doc("SF").set({
name: "San Francisco",
state: "CA",
country: "USA",
});
await cities.doc("LA").set({
name: "Los Angeles",
state: "CA",
country: "USA"
});
await cities.doc("DC").set({
name: "Washington, D.C.",
state: null,
country: "USA"
});
await cities.doc("TOK").set({
name: "Tokyo",
state: null,
country: "Japan"
});
// Load restaurants in various cities.
const sfRestaurants = db.collection("cities/SF/restaurants");
const laRestaurants = db.collection("cities/LA/restaurants");
const dcRestaurants = db.collection("cities/DC/restaurants");
const rest1 = await sfRestaurants.add({
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
});
const rest2 = await sfRestaurants.add({
name: "Bay Area Burger",
type: "burger",
owner_id: "Sarah Jenkins"
});
const rest3 = await sfRestaurants.add({
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
});
const rest4 = await laRestaurants.add({
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
});
const rest5 = await laRestaurants.add({
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano"
});
const rest6 = await dcRestaurants.add({
name: "Capitol Tacos",
type: "mexican",
owner_id: "Maria Garcia"
});
const rest7 = await dcRestaurants.add({
name: "Georgetown Coffee",
type: "cafe",
owner_id: "David Kim"
});
// Load collection of reviews.
const reviews = db.collection("reviews");
await reviews.add({ restaurant: rest1, rating: 5, reviewer_id: "Alice" });
await reviews.add({ restaurant: rest1, rating: 4, reviewer_id: "Bob" });
await reviews.add({ restaurant: rest2, rating: 4, reviewer_id: "Charlie" });
await reviews.add({ restaurant: rest3, rating: 5, reviewer_id: "Diana" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Edward" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Fiona" });
// rest4 has 0 reviews
await reviews.add({ restaurant: rest5, rating: 3, reviewer_id: "George" });
await reviews.add({ restaurant: rest6, rating: 5, reviewer_id: "Hannah" });
await reviews.add({ restaurant: rest6, rating: 4, reviewer_id: "Ian" });
await reviews.add({ restaurant: rest7, rating: 5, reviewer_id: "Julia" });
البحث عن مستند في مجموعة أخرى
يُجري طلب البحث التالي في مجموعة reviews عملية بحث في مجموعة restaurant باستخدام مرجع مفتاح أساسي.
Node.js
let results = await 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"))
.execute();
الردّ
{
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 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"))
.execute();
الردّ
{
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 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"))
.execute();
الردّ
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
أفضل N لكل مجموعة (استعلام فرعي مع حد)
يجلب طلب البحث التالي جميع المستندات من مجموعة المجموعات restaurants، ويستخدم طلب بحث فرعيًا مرتبطًا لجلب أعلى مراجعتَين لكل مطعم.
يضمن ذلك عدم زيادة حجم مصفوفة المراجعات بشكل كبير وتجاوزها الحدّ الأقصى لذاكرة طلب البحث.
Node.js
let results = await 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"))
.execute();
الردّ
{
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 db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count"))
.execute();
الردّ
{
__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 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))
.execute();
الردّ
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Anti-Join (NOT EXISTS)
يفحص طلب البحث التالي مجموعة المجموعات restaurants ويعثر على جميع المطاعم التي ليس لديها أي مراجعات حتى الآن.
Node.js
let results = await 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))
.execute();
الردّ
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
الاستعلام الفرعي كعملية دمج
يعمل طلب البحث التالي على تسوية العلاقة بين كل مطعم بيتزا ومراجعاته. من خلال وضع الاستعلام الفرعي داخل مرحلة unnest(...)، يكرّر الخادم مستند المطعم الخارجي لكل مراجعة مطابقة، ما يؤدي إلى إنشاء مستندات مسطّحة ومدمجة (على غرار INNER JOIN في SQL).
Node.js
let results = await 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"))
.execute();
الردّ
{
__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 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")
.execute();
الردّ
{
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 ميغابايت على البيانات المادية في طلب البحث بأكمله، بما في ذلك جميع المستندات المضمّنة.