קרא וכתוב נתונים באנדרואיד

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

נתוני Firebase נכתבים להפניה FirebaseDatabase ומאוחזרים על ידי הצמדת מאזין אסינכרוני להפניה. המאזין מופעל פעם אחת עבור המצב ההתחלתי של הנתונים ושוב בכל פעם שהנתונים משתנים.

(אופציונלי) אב-טיפוס ובדיקה עם Firebase Local Emulator Suite

לפני שנדבר על האופן שבו האפליקציה שלך קוראת וכותבת למסד נתונים בזמן אמת, הבה נציג קבוצה של כלים שבהם אתה יכול להשתמש כדי לבחון את הפונקציונליות של מסד נתונים בזמן אמת: Firebase Local Emulator Suite. אם אתה מנסה מודלים שונים של נתונים, מייעל את כללי האבטחה שלך, או עובד כדי למצוא את הדרך המשתלמת ביותר לאינטראקציה עם הקצה האחורי, היכולת לעבוד מקומית מבלי לפרוס שירותים חיים יכולה להיות רעיון מצוין.

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

השימוש באמולטור Realtime Database כולל רק כמה שלבים:

  1. הוספת שורת קוד לתצורת הבדיקה של האפליקציה שלך כדי להתחבר לאמולטור.
  2. מהשורש של ספריית הפרויקט המקומית שלך, הפעל firebase emulators:start .
  3. ביצוע שיחות מקוד אב הטיפוס של האפליקציה שלך באמצעות SDK של פלטפורמת Realtime Database כרגיל, או באמצעות Realtime Database REST API.

הדרכה מפורטת הכוללת מסד נתונים ופונקציות ענן בזמן אמת זמין. כדאי גם לעיין במבוא של Local Emulator Suite .

קבל הפניית מסד נתונים

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

Kotlin+KTX

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

Java

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

כתוב נתונים

פעולות כתיבה בסיסיות

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

  • סוגי מעבר התואמים לסוגי ה-JSON הזמינים באופן הבא:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • העבר אובייקט Java מותאם אישית, אם למחלקה שמגדירה אותו יש בנאי ברירת מחדל שלא לוקח ארגומנטים ויש לו מגברים ציבוריים עבור המאפיינים שיוקצו.

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

Kotlin+KTX

@IgnoreExtraProperties
data class User(val username: String? = null, val email: String? = null) {
    // Null default values create a no-argument default constructor, which is needed
    // for deserialization from a DataSnapshot.
}

Java

@IgnoreExtraProperties
public class User {

    public String username;
    public String email;

    public User() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

}

אתה יכול להוסיף משתמש עם setValue() באופן הבא:

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

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

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

קרא נתונים

קרא נתונים עם מאזינים מתמשכים

כדי לקרוא נתונים בנתיב ולהאזין לשינויים, השתמש בשיטת addValueEventListener() כדי להוסיף ValueEventListener ל- DatabaseReference .

מַאֲזִין התקשרות חוזרת לאירוע שימוש אופייני
ValueEventListener onDataChange() קרא והקשב לשינויים בכל התוכן של נתיב.

אתה יכול להשתמש בשיטה onDataChange() כדי לקרוא תמונת מצב סטטית של התוכן בנתיב נתון, כפי שהם היו קיימים בזמן האירוע. שיטה זו מופעלת פעם אחת כאשר המאזין מחובר ושוב בכל פעם שהנתונים, כולל ילדים, משתנים. ההתקשרות חזרה לאירוע מועברת תמונת מצב המכילה את כל הנתונים באותו מיקום, כולל נתוני ילדים. אם אין נתונים, תמונת המצב תחזיר false כאשר אתה קורא exists() ו- null כאשר אתה קורא עליו getValue() .

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

Kotlin+KTX

val postListener = object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        // Get Post object and use the values to update the UI
        val post = dataSnapshot.getValue<Post>()
        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
    }
}
postReference.addValueEventListener(postListener)

Java

ValueEventListener postListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // Get Post object and use the values to update the UI
        Post post = dataSnapshot.getValue(Post.class);
        // ..
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
    }
};
mPostReference.addValueEventListener(postListener);

המאזין מקבל DataSnapshot שמכיל את הנתונים במיקום שצוין במסד הנתונים בזמן האירוע. קריאה ל- getValue() בתמונת מצב מחזירה את ייצוג אובייקט Java של הנתונים. אם לא קיימים נתונים במיקום, קריאת getValue() מחזירה null .

בדוגמה זו, ValueEventListener מגדיר גם את המתודה onCancelled() הנקראת אם הקריאה מבוטלת. לדוגמה, ניתן לבטל קריאה אם ​​ללקוח אין הרשאה לקרוא ממיקום מסד נתונים של Firebase. שיטה זו מועברת אובייקט DatabaseError המציין מדוע התרחש הכשל.

קרא נתונים פעם אחת

קרא פעם אחת באמצעות get()

ה-SDK נועד לנהל אינטראקציות עם שרתי מסד נתונים בין אם האפליקציה שלך מקוונת או לא מקוונת.

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

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

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

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Java

mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DataSnapshot> task) {
        if (!task.isSuccessful()) {
            Log.e("firebase", "Error getting data", task.getException());
        }
        else {
            Log.d("firebase", String.valueOf(task.getResult().getValue()));
        }
    }
});

קרא פעם אחת באמצעות מאזין

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

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

עדכון או מחיקת נתונים

עדכן שדות ספציפיים

כדי לכתוב בו-זמנית לילדים ספציפיים של צומת מבלי לדרוס צמתים צאצאים אחרים, השתמש בשיטת updateChildren() .

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

Kotlin+KTX

@IgnoreExtraProperties
data class Post(
    var uid: String? = "",
    var author: String? = "",
    var title: String? = "",
    var body: String? = "",
    var starCount: Int = 0,
    var stars: MutableMap<String, Boolean> = HashMap(),
) {

    @Exclude
    fun toMap(): Map<String, Any?> {
        return mapOf(
            "uid" to uid,
            "author" to author,
            "title" to title,
            "body" to body,
            "starCount" to starCount,
            "stars" to stars,
        )
    }
}

Java

@IgnoreExtraProperties
public class Post {

    public String uid;
    public String author;
    public String title;
    public String body;
    public int starCount = 0;
    public Map<String, Boolean> stars = new HashMap<>();

    public Post() {
        // Default constructor required for calls to DataSnapshot.getValue(Post.class)
    }

    public Post(String uid, String author, String title, String body) {
        this.uid = uid;
        this.author = author;
        this.title = title;
        this.body = body;
    }

    @Exclude
    public Map<String, Object> toMap() {
        HashMap<String, Object> result = new HashMap<>();
        result.put("uid", uid);
        result.put("author", author);
        result.put("title", title);
        result.put("body", body);
        result.put("starCount", starCount);
        result.put("stars", stars);

        return result;
    }
}

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

Kotlin+KTX

private fun writeNewPost(userId: String, username: String, title: String, body: String) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    val key = database.child("posts").push().key
    if (key == null) {
        Log.w(TAG, "Couldn't get push key for posts")
        return
    }

    val post = Post(userId, username, title, body)
    val postValues = post.toMap()

    val childUpdates = hashMapOf<String, Any>(
        "/posts/$key" to postValues,
        "/user-posts/$userId/$key" to postValues,
    )

    database.updateChildren(childUpdates)
}

Java

private void writeNewPost(String userId, String username, String title, String body) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    String key = mDatabase.child("posts").push().getKey();
    Post post = new Post(userId, username, title, body);
    Map<String, Object> postValues = post.toMap();

    Map<String, Object> childUpdates = new HashMap<>();
    childUpdates.put("/posts/" + key, postValues);
    childUpdates.put("/user-posts/" + userId + "/" + key, postValues);

    mDatabase.updateChildren(childUpdates);
}

דוגמה זו משתמשת ב- push() כדי ליצור פוסט בצומת המכיל פוסטים עבור כל המשתמשים ב- /posts/$postid ובו-זמנית לאחזר את המפתח עם getKey() . לאחר מכן ניתן להשתמש במפתח ליצירת ערך שני בפוסטים של המשתמש ב- /user-posts/$userid/$postid .

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

הוסף התקשרות חוזרת של השלמה

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

Kotlin+KTX

database.child("users").child(userId).setValue(user)
    .addOnSuccessListener {
        // Write was successful!
        // ...
    }
    .addOnFailureListener {
        // Write failed
        // ...
    }

Java

mDatabase.child("users").child(userId).setValue(user)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Write was successful!
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Write failed
                // ...
            }
        });

מחק נתונים

הדרך הפשוטה ביותר למחוק נתונים היא לקרוא ל- removeValue() בהפניה למיקום הנתונים הללו.

אתה יכול גם למחוק על ידי ציון null כערך עבור פעולת כתיבה אחרת כגון setValue() או updateChildren() . אתה יכול להשתמש בטכניקה זו עם updateChildren() כדי למחוק ילדים מרובים בקריאה אחת ל-API.

לנתק מאזינים

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

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

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

שמור נתונים כעסקאות

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

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

Kotlin+KTX

private fun onStarClicked(postRef: DatabaseReference) {
    // ...
    postRef.runTransaction(object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result {
            val p = mutableData.getValue(Post::class.java)
                ?: return Transaction.success(mutableData)

            if (p.stars.containsKey(uid)) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1
                p.stars.remove(uid)
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1
                p.stars[uid] = true
            }

            // Set value and report transaction success
            mutableData.value = p
            return Transaction.success(mutableData)
        }

        override fun onComplete(
            databaseError: DatabaseError?,
            committed: Boolean,
            currentData: DataSnapshot?,
        ) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
        }
    })
}

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @NonNull
        @Override
        public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
            Post p = mutableData.getValue(Post.class);
            if (p == null) {
                return Transaction.success(mutableData);
            }

            if (p.stars.containsKey(getUid())) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1;
                p.stars.remove(getUid());
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1;
                p.stars.put(getUid(), true);
            }

            // Set value and report transaction success
            mutableData.setValue(p);
            return Transaction.success(mutableData);
        }

        @Override
        public void onComplete(DatabaseError databaseError, boolean committed,
                               DataSnapshot currentData) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

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

מרווחים בצד השרת האטומי

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

Kotlin+KTX

private fun onStarClicked(uid: String, key: String) {
    val updates: MutableMap<String, Any> = hashMapOf(
        "posts/$key/stars/$uid" to true,
        "posts/$key/starCount" to ServerValue.increment(1),
        "user-posts/$uid/$key/stars/$uid" to true,
        "user-posts/$uid/$key/starCount" to ServerValue.increment(1),
    )
    database.updateChildren(updates)
}

Java

private void onStarClicked(String uid, String key) {
    Map<String, Object> updates = new HashMap<>();
    updates.put("posts/"+key+"/stars/"+uid, true);
    updates.put("posts/"+key+"/starCount", ServerValue.increment(1));
    updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true);
    updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1));
    mDatabase.updateChildren(updates);
}

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

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

עבודה עם נתונים במצב לא מקוון

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

כל לקוח המחובר למסד נתונים של Firebase שומר על גרסה פנימית משלו של כל נתונים שבהם נעשה שימוש במאזינים או שמסומנים ככאלה שיישמרו מסונכרנים עם השרת. כאשר הנתונים נקראים או כתובים, נעשה שימוש תחילה בגרסה המקומית הזו של הנתונים. לאחר מכן, לקוח Firebase מסנכרן את הנתונים האלה עם שרתי מסד הנתונים המרוחקים ועם לקוחות אחרים על בסיס "המאמץ הטוב ביותר".

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

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

נדבר יותר על התנהגות לא מקוונת במידע נוסף על יכולות מקוונות ולא מקוונות .

הצעדים הבאים