במסמך הזה מוסבר איך לעבוד עם רשימות של נתונים ב-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 // ... });
הקוד הזה מגדיר שאילתה שכאשר משלבים אותה עם מאזין צאצא, היא מסנכרנת את הלקוח עם הפוסטים של המשתמש מהנתיב במסד הנתונים על סמך מזהה המשתמש, לפי מספר הכוכבים שכל פוסט קיבל. השיטה הזו, של שימוש במזהים כמפתחות אינדקס, נקראת data fan out. תוכלו לקרוא עליה בהרחבה במאמר Structure Your Database.
הקריאה ל-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(), אבל במקום הערך של מפתח צאצא שצוין, נעשה שימוש בערך של הצומת.