עבודה עם רשימות נתונים ב-Android

במאמר הזה מוסבר איך לעבוד עם רשימות של נתונים ב-Firebase. כדי ללמוד את היסודות של קריאה וכתיבה של נתונים ב-Firebase, אפשר לעיין במאמר קריאה וכתיבה של נתונים ב-Android.

קבלת DatabaseReference

כדי לקרוא ולכתוב נתונים במסד הנתונים, צריך מופע של DatabaseReference:

Kotlin

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

קריאה וכתיבה של רשימות

הוספה לרשימת נתונים

משתמשים בשיטה push() כדי לצרף נתונים לרשימה באפליקציות מרובות משתמשים. השיטה push() יוצרת מפתח ייחודי בכל פעם שמוסיפים צאצא חדש להפניה שצוינה ב-Firebase. באמצעות המפתחות האלה שנוצרים באופן אוטומטי לכל רכיב חדש ברשימה, כמה לקוחות יכולים להוסיף צאצאים לאותו מיקום בו-זמנית בלי שיהיו התנגשויות בכתיבה. המפתח הייחודי שנוצר על ידי push() מבוסס על חותמת זמן, ולכן הפריטים ברשימה מסודרים אוטומטית בסדר כרונולוגי.

אפשר להשתמש בהפניה לנתונים החדשים שמוחזרים על ידי השיטה push() כדי לקבל את הערך של המפתח שנוצר אוטומטית עבור הצאצא או להגדיר נתונים עבור הצאצא. הפעלת הפונקציה getKey() על הפניה push() מחזירה את הערך של המפתח שנוצר אוטומטית.

אפשר להשתמש במפתחות שנוצרו אוטומטית כדי לפשט את תהליך השטחת מבנה הנתונים. מידע נוסף זמין בדוגמה של פיצול נתונים.

האזנה לאירועים משוכפלים

כשעובדים עם רשימות, האפליקציה צריכה להאזין לאירועים של צאצאים ולא לאירועי הערך שמשמשים לאובייקטים בודדים.

אירועים שקשורים לילדים מופעלים בתגובה לפעולות ספציפיות שמתרחשות לילדים של צומת מפעולה כמו הוספה של צומת צאצא חדש באמצעות השיטה push() או עדכון של צומת צאצא באמצעות השיטה updateChildren(). כל אחת מהאפשרויות האלה יכולה להיות שימושית להאזנה לשינויים בצומת ספציפי במסד נתונים.

כדי להאזין לאירועים של ילדים ב-DatabaseReference, צריך לצרף ChildEventListener:

Listener התקשרות חוזרת לאירוע שימוש אופייני
ChildEventListener onChildAdded() אחזור רשימות של פריטים או האזנה להוספות לרשימה של פריטים. הקריאה החוזרת הזו מופעלת פעם אחת לכל צאצא קיים, ואז שוב בכל פעם שמוסיפים צאצא חדש לנתיב שצוין. השדה DataSnapshot שמועבר למאזין מכיל את הנתונים של צאצא חדש.
onChildChanged() חיפוש שינויים בפריטים ברשימה. האירוע הזה מופעל בכל פעם שמשנים צומת צאצא, כולל שינויים בצאצאים של צומת הצאצא. השדה DataSnapshot שמועבר למאזין האירועים מכיל את הנתונים המעודכנים של הילד.
onChildRemoved() האזנה להסרה של פריטים מרשימה. הפרמטר DataSnapshot שמועבר לקריאה החוזרת של האירוע מכיל את הנתונים של צאצא שהוסר.
onChildMoved() האזנה לשינויים בסדר הפריטים ברשימה ממוספרת. האירוע הזה מופעל בכל פעם שהקריאה החוזרת 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.

ניתוק של listeners

כדי להסיר קריאות חוזרות, קוראים לשיטה removeEventListener() בהפניה למסד הנתונים של Firebase.

אם מאזין נוסף כמה פעמים למיקום נתונים, הוא מופעל כמה פעמים לכל אירוע, וצריך לנתק אותו מספר הפעמים שבהן הוא נוסף כדי להסיר אותו לחלוטין.

התקשרות אל removeEventListener() במאזין הורה לא מסירה באופן אוטומטי מאזינים שרשומים בצמתי הצאצא שלו. צריך גם להתקשר אל removeEventListener() בכל מאזין צאצא כדי להסיר את הקריאה החוזרת.

מיון וסינון של נתונים

אפשר להשתמש במחלקה Realtime Database Query כדי לאחזר נתונים ממוינים לפי מפתח, לפי ערך או לפי ערך של צאצא. אפשר גם לסנן את התוצאה הממוינת לפי מספר מסוים של תוצאות או לפי טווח של מפתחות או ערכים.

מיון נתונים

כדי לאחזר נתונים ממוינים, צריך קודם לציין אחת משיטות המיון כדי לקבוע את סדר התוצאות:

שיטה שימוש
orderByChild() מיון התוצאות לפי הערך של מפתח צאצא ספציפי או נתיב צאצא מוטמע.
orderByKey() מיון התוצאות לפי מפתחות צאצא.
orderByValue() מיון התוצאות לפי ערכי הצאצא.

אפשר להשתמש רק בשיטה אחת של מיון בכל פעם. הפעלת שיטה של מיון לפי סדר (order-by) מספר פעמים באותה שאילתה גורמת לשגיאה.

בדוגמה הבאה מוצג איך אפשר לאחזר רשימה של הפוסטים המובילים של משתמש, ממוינים לפי מספר הכוכבים:

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
    // ...
});

השאילתה הזו מגדירה שאילתה, שכאשר משלבים אותה עם listener צאצא, מסנכרנת את הלקוח עם הפוסטים של המשתמש מהנתיב במסד הנתונים על סמך מזהה המשתמש, ומסדרת אותם לפי מספר הכוכבים שכל פוסט קיבל. הטכניקה הזו של שימוש במזהים כמפתחות אינדקס נקראת פיצול נתונים. אפשר לקרוא עליה עוד במאמר 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() הפונקציה מחזירה פריטים ששווים למפתח או לערך שצוינו, בהתאם לשיטת המיון שנבחרה.

בניגוד לשיטות order-by, אפשר לשלב כמה פונקציות limit או range. לדוגמה, אפשר לשלב בין השיטות 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 קריאות חוזרות של onChildAdded(). אם יש לכם פחות מ-100 פריטים שמאוחסנים במסד הנתונים של Firebase, מתבצעת קריאה חוזרת (callback) של onChildAdded() לכל פריט.

כשהפריטים משתנים, אתם מקבלים קריאות חוזרות (callback) מסוג 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() כדי לבחור נקודות שרירותיות להתחלה, לסיום ולשוויון של שאילתות. המאפיין הזה יכול להיות שימושי כשמבצעים חלוקה לעמודים של נתונים או כשמחפשים פריטים עם פריטי צאצא עם ערך ספציפי.

איך נתוני השאילתות מסודרים

בקטע הזה מוסבר איך הנתונים ממוינים לפי כל אחת מהשיטות של order-by במחלקה Query.

orderByChild

כשמשתמשים ב-orderByChild(), נתונים שמכילים את מפתח הצאצא שצוין מסודרים באופן הבא:

  1. ילדים עם ערך null למפתח הילד שצוין מופיעים ראשונים.
  2. אחריהם מופיעים רכיבי צאצא עם הערך false עבור מפתח הצאצא שצוין. אם לכמה צאצאים יש ערך של false, הם ממוינים לקסיקוגרפית לפי מפתח.
  3. אחריהם מופיעים רכיבי צאצא עם הערך true עבור מפתח הצאצא שצוין. אם לכמה צאצאים יש ערך של true, הם ממוינים לפי מפתח בסדר לקסיקוגרפי.
  4. אחריהם מופיעים ילדים עם ערך מספרי, ממוינים בסדר עולה. אם לכמה צאצאים יש את אותו ערך מספרי בצומת הצאצא שצוין, הם ממוינים לפי מפתח.
  5. מחרוזות מופיעות אחרי מספרים וממוינות בסדר עולה לפי סדר מילוני. אם לכמה צאצאים יש את אותו ערך בצומת הצאצא שצוין, הם מסודרים לפי מפתח בסדר לקסיקוגרפי.
  6. האובייקטים מופיעים בסוף וממוינים בסדר עולה לפי מפתח, בסדר לקסיקוגרפי.

orderByKey

כשמשתמשים ב-orderByKey() כדי למיין את הנתונים, הנתונים מוחזרים בסדר עולה לפי מפתח.

  1. הילדים עם מפתח שאפשר לנתח כמספר שלם בן 32 ביט מופיעים ראשונים, בסדר עולה.
  2. אחריהם מופיעים ילדים עם ערך מחרוזת כמפתח, ממוינים בסדר עולה לפי סדר מילוני.

orderByValue

כשמשתמשים ב-orderByValue(), הילדים מסודרים לפי הערך שלהם. קריטריוני המיון זהים לאלה שמופיעים ב-orderByChild(), אלא שהערך של הצומת משמש במקום הערך של מפתח צאצא שצוין.

השלבים הבאים