במסמך הזה מוסבר איך לעבוד עם רשימות של נתונים ב-Firebase. כדי ללמוד את העקרונות הבסיסיים של קריאה וכתיבה של נתוני Firebase, קראו את המאמר קריאה וכתיבה של נתונים ב-Android.
קבלת DatabaseReference
כדי לקרוא נתונים ממסד הנתונים ולכתוב אליו, צריך מופע של DatabaseReference:
Kotlin
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
קריאה וכתיבה של רשימות
הוספה לרשימת נתונים
משתמשים ב-method push() כדי להוסיף נתונים לרשימה באפליקציות עם כמה משתמשים.
השיטה push() יוצרת מפתח ייחודי בכל פעם שנוסף צאצא חדש להפניה שצוינה ב-Firebase. כשמשתמשים במפתחות שנוצרים אוטומטית לכל רכיב חדש ברשימה, כמה לקוחות יכולים להוסיף צאצאים לאותו מיקום בו-זמנית בלי התנגשויות כתיבה. המפתח הייחודי שנוצר על ידי push() מבוסס על חותמת זמן, כך שהפריטים ברשימה מסודרים באופן אוטומטי בסדר כרונולוגי.
אתם יכולים להשתמש בהפניה לנתונים החדשים שהוחזרו על ידי השיטה push() כדי לקבל את הערך של המפתח שנוצר באופן אוטומטי של הצאצא, או להגדיר נתונים לצאצא. קריאה ל-getKey() בהפניה ל-push() מחזירה את הערך של המפתח שנוצר אוטומטית.
אתם יכולים להשתמש במפתחות שנוצרו באופן אוטומטי כדי לפשט את מבנה הנתונים. למידע נוסף, ראו דוגמה להסתעפות נתונים.
האזנה לאירועים משניים
כשעובדים עם רשימות, האפליקציה צריכה להאזין לאירועים משניים ולא לאירועי הערך שמשמשים לאובייקטים בודדים.
אירועים משניים מופעלים בתגובה לפעולות ספציפיות שמתרחשות בצאצאים של צומת, כמו הוספת צאצא חדש באמצעות השיטה push() או עדכון של צאצא באמצעות השיטה updateChildren().
כל אחד מהם יכול להיות שימושי להאזנה לשינויים בצומת ספציפי במסד נתונים.
כדי להאזין לאירועים משניים ב-DatabaseReference, צריך לצרף
ChildEventListener:
| מאזין | קריאה חוזרת (callback) של אירוע | שימוש אופייני |
|---|---|---|
ChildEventListener
| onChildAdded() |
אחזור רשימות של פריטים או האזנה לתוספות לרשימת פריטים.
הקריאה החוזרת הזו מופעלת פעם אחת לכל צאצא קיים, ואז שוב
בכל פעם שצאצא חדש מתווסף לנתיב שצוין. ה-DataSnapshot שמועבר למאזין מכיל את הנתונים של הצאצא החדש.
|
onChildChanged() |
האזנה לשינויים בפריטים ברשימה. האירוע הזה הופעל בכל פעם שצומת צאצא השתנה, כולל שינויים בצאצאים של צומת הצאצא. ה-DataSnapshot שמועבר לפונקציית ה-event listener מכיל את הנתונים המעודכנים של הצאצא.
|
|
onChildRemoved() |
האזנה לפריטים שמוסרים מרשימה. ה-DataSnapshot שמועבר לקריאה החוזרת (callback) של האירוע מכיל את הנתונים של הצאצא שהוסר.
|
|
onChildMoved() |
האזנה לשינויים בסדר הפריטים ברשימה ממוינת.
האירוע הזה מופעל בכל פעם שהקריאה החוזרת (callback) של onChildChanged() מופעלת על ידי עדכון שגורם לשינוי הסדר של הצאצא.
היא משמשת עם נתונים שמסודרים באמצעות orderByChild או orderByValue.
|
לדוגמה, אפליקציה של בלוג חברתי יכולה להשתמש בשיטות האלה ביחד כדי לעקוב אחרי הפעילות בתגובות של פוסט, כמו שמוצג בהמשך:
Kotlin
val childEventListener = object : ChildEventListener { override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) { Log.d(TAG, "onChildAdded:" + dataSnapshot.key!!) // A new comment has been added, add it to the displayed list val comment = dataSnapshot.getValue<Comment>() // ... } override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) { Log.d(TAG, "onChildChanged: ${dataSnapshot.key}") // A comment has changed, use the key to determine if we are displaying this // comment and if so displayed the changed comment. val newComment = dataSnapshot.getValue<Comment>() val commentKey = dataSnapshot.key // ... } override fun onChildRemoved(dataSnapshot: DataSnapshot) { Log.d(TAG, "onChildRemoved:" + dataSnapshot.key!!) // A comment has changed, use the key to determine if we are displaying this // comment and if so remove it. val commentKey = dataSnapshot.key // ... } override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) { Log.d(TAG, "onChildMoved:" + dataSnapshot.key!!) // A comment has changed position, use the key to determine if we are // displaying this comment and if so move it. val movedComment = dataSnapshot.getValue<Comment>() val commentKey = dataSnapshot.key // ... } override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "postComments:onCancelled", databaseError.toException()) Toast.makeText( context, "Failed to load comments.", Toast.LENGTH_SHORT, ).show() } } databaseReference.addChildEventListener(childEventListener)
Java
ChildEventListener childEventListener = new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey()); // A new comment has been added, add it to the displayed list Comment comment = dataSnapshot.getValue(Comment.class); // ... } @Override public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey()); // A comment has changed, use the key to determine if we are displaying this // comment and if so displayed the changed comment. Comment newComment = dataSnapshot.getValue(Comment.class); String commentKey = dataSnapshot.getKey(); // ... } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey()); // A comment has changed, use the key to determine if we are displaying this // comment and if so remove it. String commentKey = dataSnapshot.getKey(); // ... } @Override public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey()); // A comment has changed position, use the key to determine if we are // displaying this comment and if so move it. Comment movedComment = dataSnapshot.getValue(Comment.class); String commentKey = dataSnapshot.getKey(); // ... } @Override public void onCancelled(DatabaseError databaseError) { Log.w(TAG, "postComments:onCancelled", databaseError.toException()); Toast.makeText(mContext, "Failed to load comments.", Toast.LENGTH_SHORT).show(); } }; databaseReference.addChildEventListener(childEventListener);
האזנה לאירועי ערך
השימוש ב-ChildEventListener הוא הדרך המומלצת לקרוא רשימות של נתונים, אבל יש מצבים שבהם כדאי לצרף ValueEventListener להפניה לרשימה.
אם מצרפים ValueEventListener לרשימת נתונים, כל הרשימה מוחזרת בתור DataSnapshot יחיד, ואז אפשר לעבור עליה בלולאה כדי לגשת לצאצאים ספציפיים.
גם אם יש רק התאמה אחת לשאילתה, תמונת המצב עדיין תהיה רשימה, אבל היא תכיל רק פריט אחד. כדי לגשת לפריט, צריך לעבור על התוצאה בלולאה:
Kotlin
// My top posts by number of stars myTopPostsQuery.addValueEventListener(object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { for (postSnapshot in dataSnapshot.children) { // TODO: handle the post } } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) // ... } })
Java
// My top posts by number of stars myTopPostsQuery.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot dataSnapshot) { for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) { // TODO: handle the post } } @Override public void onCancelled(@NonNull DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); // ... } });
התבנית הזו יכולה להיות שימושית כשרוצים לאחזר את כל הצאצאים של רשימה בפעולה אחת, במקום להאזין לאירועים נוספים של onChildAdded.
ניתוק מאזינים
כדי להסיר קריאות חוזרות, צריך להפעיל את השיטה removeEventListener() בהפניה למסד הנתונים של Firebase.
אם מאזין נוסף כמה פעמים לתוסף לאיתור נתונים, הוא נקרא כמה פעמים לכל אירוע, וצריך לנתק אותו אותו מספר פעמים כדי להסיר אותו לגמרי.
קריאה ל-removeEventListener() במאזין הורה לא מסירה באופן אוטומטי את המאזינים שרשומים בצומתי הצאצא שלו. צריך לקרוא ל-removeEventListener() גם בכל מאזין צאצא כדי להסיר את הקריאה החוזרת (callback).
מיון וסינון נתונים
אפשר להשתמש במחלקה Realtime Database Query כדי לאחזר נתונים שממוינים לפי מפתח, לפי ערך או לפי ערך של צאצא. אפשר גם לסנן את התוצאה הממוינת למספר ספציפי של תוצאות או לטווח של מפתחות או ערכים.
מיון נתונים
כדי לאחזר נתונים ממוינים, קודם צריך לציין את אחת משיטות הסידור כדי לקבוע איך התוצאות יסודרו:
| שיטה | שימוש |
|---|---|
orderByChild() |
מסדרים את התוצאות לפי הערך של מפתח צאצא מסוים או נתיב צאצא מוטמע. |
orderByKey()
| סידור התוצאות לפי מפתחות צאצא. |
orderByValue() |
סידור התוצאות לפי ערכי הצאצאים. |
אפשר להשתמש רק בשיטת סידור אחת בכל פעם. אם קוראים לשיטת הסידור כמה פעמים באותה שאילתה, מתקבלת שגיאה.
בדוגמה הבאה אפשר לראות איך מאחזרים רשימה של הפוסטים המובילים של משתמש, ממוינים לפי מספר הכוכבים שלהם:
Kotlin
// My top posts by number of stars val myUserId = uid val myTopPostsQuery = databaseReference.child("user-posts").child(myUserId) .orderByChild("starCount") myTopPostsQuery.addChildEventListener(object : ChildEventListener { // TODO: implement the ChildEventListener methods as documented above // ... })
Java
// My top posts by number of stars String myUserId = getUid(); Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId) .orderByChild("starCount"); myTopPostsQuery.addChildEventListener(new ChildEventListener() { // TODO: implement the ChildEventListener methods as documented above // ... });
כך מגדירים שאילתה שכאשר משלבים אותה עם מאזין צאצא, היא מסנכרנת את הלקוח עם הפוסטים של המשתמש מהנתיב במסד הנתונים על סמך מזהה המשתמש, לפי מספר הכוכבים שכל פוסט קיבל. השיטה הזו, של שימוש במזהים כמפתחות אינדקס, נקראת "הפצת נתונים". תוכלו לקרוא עליה בהרחבה במאמר מבנה מסד הנתונים.
הקריאה ל-method orderByChild() מציינת את מפתח הצאצא שלפיו יסודרו התוצאות. במקרה הזה, הפוסטים ממוינים לפי הערך של הצאצא "starCount" שלהם. אפשר גם לסדר שאילתות לפי צאצאים בתצוגת עץ, אם יש לכם נתונים שנראים כך:
"posts": {
"ts-functions": {
"metrics": {
"views" : 1200000,
"likes" : 251000,
"shares": 1200,
},
"title" : "Why you should use TypeScript for writing Cloud Functions",
"author": "Doug",
},
"android-arch-3": {
"metrics": {
"views" : 900000,
"likes" : 117000,
"shares": 144,
},
"title" : "Using Android Architecture Components with Firebase Realtime Database (Part 3)",
"author": "Doug",
}
},בדוגמה הזו, אנחנו יכולים לסדר את רכיבי הרשימה לפי ערכים שמוטמעים מתחת למפתח metrics, על ידי ציון הנתיב היחסי לצאצא המוטמע בקריאה ל-orderByChild().
Kotlin
// Most viewed posts val myMostViewedPostsQuery = databaseReference.child("posts") .orderByChild("metrics/views") myMostViewedPostsQuery.addChildEventListener(object : ChildEventListener { // TODO: implement the ChildEventListener methods as documented above // ... })
Java
// Most viewed posts Query myMostViewedPostsQuery = databaseReference.child("posts") .orderByChild("metrics/views"); myMostViewedPostsQuery.addChildEventListener(new ChildEventListener() { // TODO: implement the ChildEventListener methods as documented above // ... });
במאמר איך מסודרים נתוני השאילתה מוסבר איך מסודרים סוגי נתונים אחרים.
סינון נתונים
כדי לסנן נתונים, אפשר לשלב כל אחת משיטות ההגבלה או הטווח עם שיטת הסידור כשיוצרים שאילתה.
| שיטה | שימוש |
|---|---|
limitToFirst() |
מגדיר את המספר המקסימלי של פריטים שיוחזרו מתחילת רשימת התוצאות המסודרת. |
limitToLast() |
מגדירה את המספר המקסימלי של פריטים שיוחזרו מסוף רשימת התוצאות המסודרת. |
startAt() |
החזרת פריטים שגדולים מהמפתח או מהערך שצוינו או שווים להם, בהתאם לשיטת הסידור שנבחרה. |
startAfter() |
החזרת פריטים שגדולים מהמפתח או מהערך שצוינו, בהתאם לשיטת הסידור שנבחרה. |
endAt() |
החזרת פריטים שקטנים מהמפתח או מהערך שצוינו או שווים להם, בהתאם לשיטת הסידור שנבחרה. |
endBefore() |
החזרת פריטים שהם קטנים מהמפתח או מהערך שצוינו, בהתאם לשיטת הסידור שנבחרה. |
equalTo() |
הפונקציה מחזירה פריטים ששווים למפתח או לערך שצוינו, בהתאם לשיטת הסידור שנבחרה. |
בניגוד לשיטות הסידור, אפשר לשלב כמה פונקציות של הגבלה או טווח.
לדוגמה, אפשר לשלב את השיטות startAt() ו-endAt() כדי להגביל את התוצאות לטווח ערכים מסוים.
גם אם יש רק התאמה אחת לשאילתה, תמונת המצב עדיין תהיה רשימה, אבל היא תכיל רק פריט אחד. כדי לגשת לפריט, צריך לעבור על התוצאה בלולאה:
Kotlin
// My top posts by number of stars myTopPostsQuery.addValueEventListener(object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { for (postSnapshot in dataSnapshot.children) { // TODO: handle the post } } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) // ... } })
Java
// My top posts by number of stars myTopPostsQuery.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot dataSnapshot) { for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) { // TODO: handle the post } } @Override public void onCancelled(@NonNull DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); // ... } });
להגביל את מספר התוצאות
אפשר להשתמש בשיטות limitToFirst() ו-limitToLast() כדי להגדיר את המספר המקסימלי של צאצאים שיסונכרנו בקריאה חוזרת (callback) נתונה. לדוגמה, אם אתם משתמשים ב-limitToFirst() כדי להגדיר מגבלה של 100, בהתחלה תקבלו עד 100 קריאות חוזרות (callbacks) של onChildAdded(). אם יש פחות מ-100 פריטים במסד הנתונים של Firebase, מופעלת קריאה חוזרת (callback) מסוג onChildAdded() לכל פריט.
כשהפריטים משתנים, אתם מקבלים קריאות חוזרות (callbacks) מסוג onChildAdded() לפריטים שנכנסים לשאילתה וקריאות חוזרות מסוג onChildRemoved() לפריטים שיוצאים ממנה, כך שהמספר הכולל נשאר 100.
בדוגמה הבאה מוצג איך אפליקציית בלוגים לדוגמה מגדירה שאילתה כדי לאחזר רשימה של 100 הפוסטים האחרונים של כל המשתמשים:
Kotlin
// Last 100 posts, these are automatically the 100 most recent // due to sorting by push() keys. databaseReference.child("posts").limitToFirst(100)
Java
// Last 100 posts, these are automatically the 100 most recent // due to sorting by push() keys Query recentPostsQuery = databaseReference.child("posts") .limitToFirst(100);
בדוגמה הזו מוגדרת רק שאילתה. כדי לסנכרן את הנתונים בפועל, צריך לצרף לה מאזין.
סינון לפי מפתח או ערך
אפשר להשתמש ב-startAt(), startAfter(), endAt(), endBefore() ו-equalTo() כדי לבחור נקודות התחלה, סיום ושוויון שרירותיות לשאילתות. האפשרות הזו יכולה להיות שימושית להוספת מספרי עמודים לנתונים או למציאת פריטים עם צאצאים שיש להם ערך ספציפי.
איך נתוני השאילתות מסודרים
בקטע הזה מוסבר איך הנתונים ממוינים לפי כל אחת משיטות הסידור במחלקה Query.
orderByChild
כשמשתמשים ב-orderByChild(), הנתונים שמכילים את מפתח הצאצא שצוין מסודרים באופן הבא:
- ילדים עם ערך
nullלמפתח הצאצא שצוין מופיעים ראשונים. - אחריהם מופיעים צאצאים עם הערך
falseבמפתח הצאצא שצוין. אם לכמה צאצאים יש ערך שלfalse, הם ממוינים לקסיקוגרפית לפי מפתח. - אחריהם מופיעים צאצאים עם הערך
trueבמפתח הצאצא שצוין. אם לכמה צאצאים יש ערך שלtrue, הם ממוינים לפי סדר לקסיקוגרפי לפי מפתח. - אחריהם מופיעים צאצאים עם ערך מספרי, בסדר עולה. אם לכמה צאצאים יש אותו ערך מספרי בצומת הצאצא שצוין, הם ממוינים לפי מפתח.
- מחרוזות מופיעות אחרי מספרים וממוינות בסדר לקסיקוגרפי עולה. אם לכמה צאצאים יש את אותו ערך בצומת הצאצא שצוין, הם מסודרים לפי סדר לקסיקוגרפי לפי מפתח.
- האובייקטים מופיעים בסוף וממוינים בסדר לקסיקוגרפי לפי מפתח בסדר עולה.
orderByKey
כשמשתמשים ב-orderByKey() כדי למיין את הנתונים, הנתונים מוחזרים בסדר עולה לפי מפתח.
- הצאצאים עם מפתח שאפשר לנתח כמספר שלם של 32 ביט מופיעים קודם, בסדר עולה.
- אחריהם מופיעים צאצאים עם ערך מחרוזת כמפתח, ממוינים בסדר לקסיקוגרפי עולה.
orderByValue
כשמשתמשים ב-orderByValue(), הצאצאים מסודרים לפי הערך שלהם. הקריטריונים לסידור זהים לאלה של orderByChild(), אבל נעשה שימוש בערך של הצומת במקום בערך של מפתח צאצא שצוין.