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

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

קבלת DatabaseReference

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

Kotlin+KTX

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:

האזנה קריאה חוזרת (callback) של האירוע שימוש אופייני
ChildEventListener onChildAdded() מאחזרים רשימות של פריטים או מאזינים לתוספות לרשימת פריטים. הקריאה החוזרת (callback) הזו מופעלת פעם אחת לכל צאצא קיים, ולאחר מכן שוב בכל פעם שצאצא חדש נוסף לנתיב שצוין. השדה DataSnapshot שהועבר אל ה-listen מכיל את הפקודה את הנתונים של הילד או הילדה החדשים.
onChildChanged() מאזינים לשינויים בפריטים ברשימה. האירוע הזה הופעל בכל פעם צומת הצאצא עובר שינויים, כולל שינויים בצאצאים של את צומת הצאצא. המסמך של DataSnapshot הועבר לאירוע ה-listener מכיל את הנתונים המעודכנים לגבי הילד או הילדה.
onChildRemoved() האזנה לפריטים שהוסרו מהרשימה. השדה DataSnapshot שהועבר לקריאה החוזרת של האירוע מכיל את הערך נתונים של הילד או הילדה שהוסרו.
onChildMoved() מאזינים לשינויים בסדר הפריטים ברשימה ממוינת. האירוע הזה מופעל בכל פעם שonChildChanged() הקריאה החוזרת (callback) מופעלת על ידי עדכון שמוביל לסידור מחדש של חשבון הילד או הילדה. הוא משמש יחד עם נתונים שמסודרים לפי orderByChild או orderByValue.

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

Kotlin+KTX

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 יחיד, שאפשר להשתמש בו בלולאה גישה לילדים ספציפיים.

גם כאשר יש רק התאמה אחת לשאילתה, קובץ ה-snapshot עדיין list; הוא מכיל רק פריט אחד. כדי לגשת לפריט, צריך להפעיל אותו בלופ על התוצאה:

Kotlin+KTX

// 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.

אם ה-listener נוסף מספר פעמים למיקום נתונים, נקרא כמה פעמים לכל אירוע, וצריך לנתק אותו מספר כדי להסיר אותו לגמרי.

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

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

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

מיון נתונים

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

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

אפשר להשתמש רק בשיטה אחת בכל פעם. קריאה לשיטה 'הזמנה לפי' כמה פעמים באותה שאילתה, גורמת לשגיאה.

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

Kotlin+KTX

// 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+KTX

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

גם כאשר יש רק התאמה אחת לשאילתה, קובץ ה-snapshot עדיין רשימה, הוא מכיל רק פריט אחד. כדי לגשת לפריט, צריך כדי להציג את התוצאה בלולאה:

Kotlin+KTX

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

הגבלת מספר התוצאות

אפשר להשתמש ב-methods limitToFirst() ו-limitToLast() כדי להגדיר המספר המקסימלי של צאצאים שיסונכרנו להתקשרות חזרה נתונה. לדוגמה, אם משתמשים ב-limitToFirst() כדי להגדיר מגבלה של 100 פעמים, בהתחלה מקבלים ל-100 שיחות חוזרות של onChildAdded(). אם יש לכם פחות מ-100 פריטים שמאוחסנים מסד הנתונים של Firebase, מופעל קריאה חוזרת (callback) מסוג onChildAdded() לכל פריט.

כשפריטים משתנים, יישלחו אליך onChildAdded() פניות חוזרות לגבי פריטים ו-onChildRemoved() קריאות חוזרות (callbacks) של פריטים שנעלמים ממנה, והמספר הכולל של השהייה הוא 100.

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

Kotlin+KTX

// 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(), נתונים שמכילים את מפתח הצאצא שצוין הם מסודרות כך:

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

orderByKey

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

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

orderByValue

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

השלבים הבאים