רקע
פעולות בצינור הן ממשק חדש לשליחת שאילתות עבור Cloud Firestore. הממשק הזה מספק פונקציונליות מתקדמת של שאילתות שכוללת ביטויים מורכבים. מהדורת Enterprise של Firestore תומכת בצירופים בסגנון יחסי באמצעות שאילתות משנה קשורות. בניגוד למסדי נתונים רבים ב-NoSQL, שלרוב נדרש בהם ביטול נורמליזציה של נתונים או ביצוע של כמה בקשות בצד הלקוח, שאילתות משנה מאפשרות לכם לשלב ולצבור נתונים מאוספים קשורים או מאוספי משנה קשורים ישירות בשרת.
שאילתות משנה הן ביטויים שמריצים צינור עיבוד נתונים מוטמע לכל מסמך שעובר עיבוד על ידי השאילתה החיצונית. כך אפשר לאחזר דפוסי נתונים מורכבים, כמו אחזור מסמך לצד פריטים קשורים של אוסף משנה, או צירוף נתונים שמקושרים באופן לוגי בין אוספי שורש שונים.
מושגים
בקטע הזה נסביר את המושגים הבסיסיים שמאחורי השימוש בשאילתות משנה לביצוע צירופים בפעולות של צינור עיבוד נתונים.
שאילתות משנה כביטויים
שאילתת משנה היא לא שלב ברמה העליונה, אלא ביטוי שאפשר להשתמש בו בכל שלב שמקבל ביטויים, כמו select(...), add_fields(...), where(...) או sort(...).
Cloud Firestore תומך בשלושה סוגים של שאילתות משנה:
- Array Subqueries: Materialize the entire result set of the subquery as an array of documents.
- Scalar Subqueries: Evaluate to a single value, such as a count, an average, or a specific field from a related document.
subcollection(...)שאילתות משנה: הצטרפות פשוטה לקשר של הורה-צאצא מסוג אחד לרבים.
היקף ומשתנים
כשכותבים שאילתת צירוף, לעיתים קרובות צריך להפנות בשאילתת המשנה המקוננת לשדות מהמסמך 'החיצוני' (המסמך הראשי). כדי לגשר בין ההיקפים האלה, משתמשים בשלב let(...) (שנקרא define(...) בחלק מה-SDK) כדי להגדיר משתנים בהיקף האב שאפשר להפנות אליהם בשאילתת המשנה באמצעות הפונקציה variable(...).
תחביר
בקטעים הבאים מוסבר על התחביר של ביצוע צירופים.
השלב let(...)
השלב let(...) (שנקרא define(...) בחלק מה-SDK) הוא שלב ללא סינון שבו הנתונים מההיקף הראשי מועברים באופן מפורש למשתנה בעל שם לשימוש בהיקפים מקוננים עוקבים.
Web
async function defineStageData() {
await setDoc(doc(collection(db, "Authors"), "author_123"), {
"id": "author_123",
"name": "Jane Austen"
});
}
Swift
func defineStageData() async throws { try await db.collection("authors").document("author_123").setData([ "id": "author_123", "name": "Jane Austen" ]) }
Kotlin
fun defineStageData() { val author = hashMapOf( "id" to "author_123", "name" to "Jane Austen", ) db.collection("Authors").document("author_123").set(author) }
Java
public void defineStageData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_123"); author.put("name", "Jane Austen"); db.collection("Authors").document("author_123").set(author); }
Array Subqueries
שאילתת משנה של מערך היא מקרה מיוחד של שאילתת משנה של ביטוי, שיוצרת את כל קבוצת התוצאות של שאילתת המשנה כמערך. אם שאילתת המשנה מחזירה אפס שורות, היא מוערכת כמערך ריק. היא אף פעם לא מחזירה מערך null. שאילתות כאלה שימושיות כשנדרשות התוצאות המלאות בתוצאה הסופית, למשל כשמממשים אוסף מקונן או מתואם.
אפשר להשתמש בשאילתות כדי לסנן, למיין ולצבור נתונים בשאילתת המשנה, וכך להקטין את כמות הנתונים שצריך לאחזר ולהחזיר, ולצמצם את העלות של השאילתה. הסדר של שאילתת המשנה נשמר, כלומר שלב sort(...) בשאילתת המשנה קובע את סדר התוצאות במערך הסופי.
משתמשים ב-wrapper של toArrayExpression() SDK כדי להמיר שאילתה למערך.
Web
async function toArrayExpressionStageData() {
await setDoc(doc(collection(db, "Projects"), "project_1"), {
"id": "project_1",
"name": "Alpha Build"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "System Architecture"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "Database Schema Design"
});
}
תשובה
{
id: "project_1",
name: "Alpha Build",
taskTitles: [
"System Architecture", "Database Schema Design"
]
}
Swift
async function toArrayExpressionStageData() { await setDoc(doc(collection(db, "Projects"), "project_1"), { "id": "project_1", "name": "Alpha Build" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "System Architecture" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "Database Schema Design" }); }
תשובה
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Kotlin
fun toArrayExpressionData() { val project = hashMapOf( "id" to "project_1", "name" to "Alpha Build", ) db.collection("Projects").document("project_1").set(project) val task1 = hashMapOf( "project_id" to "project_1", "title" to "System Architecture", ) db.collection("Tasks").add(task1) val task2 = hashMapOf( "project_id" to "project_1", "title" to "Database Schema Design", ) db.collection("Tasks").add(task2) }
תשובה
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Java
public void toArrayExpressionData() { Map<String, Object> project = new HashMap<>(); project.put("id", "project_1"); project.put("name", "Alpha Build"); db.collection("Projects").document("project_1").set(project); Map<String, Object> task1 = new HashMap<>(); task1.put("project_id", "project_1"); task1.put("title", "System Architecture"); db.collection("Tasks").add(task1); Map<String, Object> task2 = new HashMap<>(); task2.put("project_id", "project_1"); task2.put("title", "Database Schema Design"); db.collection("Tasks").add(task2); }
תשובה
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
שאילתות משנה סקלריות
לרוב משתמשים בשאילתות משנה סקלריות בשלב select(...) או where(...) כדי לאפשר סינון או כדי להציג את התוצאה של שאילתת משנה בלי להציג ישירות את השאילתה המלאה.
שאילתת משנה סקלרית שמניבה אפס תוצאות תניב את הערך null עצמו, ואילו שאילתת משנה שמניבה כמה רכיבים תגרום לשגיאת זמן ריצה.
כששאילתת משנה סקלרית מפיקה רק שדה אחד לכל תוצאה, השדה מועלה להיות התוצאה ברמה העליונה של שאילתת המשנה. המצב הזה קורה בדרך כלל כשהשאילתה המשנית מסתיימת ב-select(field("user_name")) או ב-aggregate(countAll().as("total")), והסכימה של השאילתה המשנית היא רק שדה יחיד. אחרת, כששאילתת משנה יכולה להפיק כמה שדות, הם נכללים במפה.
משתמשים ב-wrapper של toScalarExpression() SDK כדי להמיר שאילתה לביטוי סקלרי.
Web
async function toScalarExpressionStageData() {
await setDoc(doc(collection(db, "Authors"), "author_202"), {
"id": "author_202",
"name": "Charles Dickens"
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Great Expectations",
"rating": 4.8
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Oliver Twist",
"rating": 4.5
});
}
תשובה
{
"id": "author_202",
"name": "Charles Dickens",
"averageBookRating": 4.65
}
Swift
try await db.collection("authors").document("author_202").setData([ "id": "author_202", "name": "Charles Dickens" ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Great Expectations", "rating": 4.8 ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Oliver Twist", "rating": 4.5 ])
תשובה
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Kotlin
fun toScalarExpressionData() { val author = hashMapOf( "id" to "author_202", "name" to "Charles Dickens", ) db.collection("Authors").document("author_202").set(author) val book1 = hashMapOf( "author_id" to "author_202", "title" to "Great Expectations", "rating" to 4.8, ) db.collection("Books").add(book1) val book2 = hashMapOf( "author_id" to "author_202", "title" to "Oliver Twist", "rating" to 4.5, ) db.collection("Books").add(book2) }
תשובה
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Java
public void toScalarExpressionData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_202"); author.put("name", "Charles Dickens"); db.collection("Authors").document("author_202").set(author); Map<String, Object> book1 = new HashMap<>(); book1.put("author_id", "author_202"); book1.put("title", "Great Expectations"); book1.put("rating", 4.8); db.collection("Books").add(book1); Map<String, Object> book2 = new HashMap<>(); book2.put("author_id", "author_202"); book2.put("title", "Oliver Twist"); book2.put("rating", 4.5); db.collection("Books").add(book2); }
תשובה
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
subcollection(...) שאילתות משנה
שלב הקלט subcollection(...) מאפשר לבצע פעולות צירוף במודל הנתונים ההיררכי של Cloud Firestore. במודל היררכי, לעיתים קרובות השאילתות צריכות לאחזר מסמך לצד נתונים מקולקציות המשנה שלו. אפשר להשיג את זה באמצעות שלב קלט של collection_group(...) ואחריו מסנן בהפניה לאב, אבל subcollection(...) מספק תחביר תמציתי הרבה יותר.
מלבד תנאי הצירוף המרומז, הפעולה הזו דומה לשאילתת משנה של מערך, והיא מחזירה תוצאה ריקה אם לא נמצאו מסמכים תואמים, גם אם האוסף המקונן לא קיים.
היא בעצם קיצור דרך תחבירי: היא משתמשת אוטומטית ב-__name__ של המסמך בהיקף החיצוני כמפתח איחוד (join) כדי לפתור את היחס ההיררכי. לכן, זו הדרך המומלצת לבצע חיפושים בקולקציות שמקושרות בקשר הורה-צאצא.
שיטות מומלצות
- ניהול הזיכרון באמצעות
toArrayExpression(): צריך להיזהר עם שאילתות משנה שלtoArrayExpression(), כי יצירת מספר גדול של מסמכים עלולה לגרום לחריגה ממגבלת הזיכרון של השאילתה (128 MiB). כדי לצמצם את הבעיה, משתמשים ב-select(...)בתוך שאילתת המשנה כדי להחזיר רק את השדות הנדרשים, ומחילים מסנניwhere(...)כדי להגביל את מספר המסמכים שמוחזרים. אם מתאים, כדאי להשתמש ב-limit(...)כדי להגביל את מספר המסמכים שמוחזרים על ידי שאילתת המשנה. - יצירת אינדקס: מוודאים שהשדות שמשמשים בסעיף
where(...)של שאילתת משנה נוספו לאינדקס. הצטרפות יעילה מסתמכת על היכולת לבצע חיפושי אינדקס במקום סריקות מלאות של הטבלה.
לשיטות מומלצות נוספות בנושא שאילתות, אפשר לעיין במדריך שלנו לאופטימיזציה של שאילתות.
מגבלות
- היקף
subcollection(...): שלב הקלטsubcollection(...)נתמך רק בשאילתות משנה, כי הוא דורש את ההקשר של מסמך הורה כדי לפתור את היחס ההיררכי ולבצע את הצירוף. - עומק הקינון: אפשר לקנן שאילתות משנה עד 20 שכבות עומק.
- השימוש בזיכרון: המגבלה של 128MiB על נתונים מגובשים חלה על כל השאילתה, כולל כל המסמכים המצורפים.