המסמך הזה עוסק ביסודות של קריאה וכתיבה של נתונים ב-Firebase.
נתוני Firebase נכתבים בהפניה FirebaseDatabase, ואפשר לאחזר אותם על ידי צירוף מאזין אסינכרוני להפניה. המאזין מופעל פעם אחת למצב הראשוני של הנתונים, ופעם נוספת בכל פעם שהנתונים משתנים.
(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות Firebase Local Emulator Suite
לפני שנדבר על האופן שבו האפליקציה קוראת נתונים מ- וכותבת נתונים ל-Realtime Database, נציג קבוצת כלים שבהם אפשר להשתמש כדי ליצור אב טיפוס ולבדוק את הפונקציונליות של Realtime Database: Firebase Local Emulator Suite. אם אתם מנסים מודלים שונים של נתונים, מבצעים אופטימיזציה של כללי האבטחה או מחפשים את הדרך הכי משתלמת לאינטראקציה עם הקצה העורפי, כדאי לעבוד באופן מקומי בלי לפרוס שירותים פעילים.
אמולטור Realtime Database הוא חלק מ-Local Emulator Suite, שמאפשר לאפליקציה שלכם לקיים אינטראקציה עם התוכן וההגדרות של מסד הנתונים המדומה, וגם עם משאבי הפרויקט המדומים (פונקציות, מסדי נתונים אחרים וכללי אבטחה).
כדי להשתמש באמולטור של Realtime Database, צריך לבצע כמה שלבים:
- הוספת שורת קוד להגדרת הבדיקה של האפליקציה כדי להתחבר לאמולטור.
- מפעילים את הפקודה
firebase emulators:startמהשורש של ספריית הפרויקט המקומית. - ביצוע קריאות מקוד אב הטיפוס של האפליקציה באמצעות ערכת SDK של פלטפורמת Realtime Database כרגיל, או באמצעות ה-API בארכיטקטורת REST של Realtime Database.
הדרכה מפורטת שכוללת את Realtime Database ו-Cloud Functions זמינה. כדאי גם לעיין במבוא ל-Local Emulator Suite.
קבלת DatabaseReference
כדי לקרוא נתונים ממסד הנתונים או לכתוב אליו, צריך מופע של DatabaseReference:
Kotlin
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
כתיבת נתונים
פעולות כתיבה בסיסיות
לפעולות כתיבה בסיסיות, אפשר להשתמש ב-setValue() כדי לשמור נתונים בהפניה שצוינה, ולהחליף את הנתונים הקיימים בנתיב הזה. אתם יכולים להשתמש בשיטה הזו כדי:
- סוגי הכרטיסים שתואמים לסוגי ה-JSON הזמינים הם:
StringLongDoubleBooleanMap<String, Object>List<Object>
- אפשר להעביר אובייקט Java בהתאמה אישית, אם למחלקה שמגדירה אותו יש בנאי ברירת מחדל שלא מקבל ארגומנטים, ויש לו פונקציות getter ציבוריות למאפיינים שצריך להקצות.
אם אתם משתמשים באובייקט Java, התוכן של האובייקט ממופה באופן אוטומטי למיקומי צאצאים באופן מקונן. בדרך כלל, שימוש באובייקט Java גם הופך את הקוד לקריא יותר וקל יותר לתחזוקה. לדוגמה, אם יש לכם אפליקציה עם פרופיל משתמש בסיסי, אובייקט User עשוי להיראות כך:
Kotlin
@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
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
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
קריאת נתונים
קריאת נתונים באמצעות מאזינים קבועים
כדי לקרוא נתונים בנתיב ולהאזין לשינויים, משתמשים ב-addValueEventListener()
כדי להוסיף ValueEventListener ל-DatabaseReference.
| מאזין | קריאה חוזרת (callback) של אירוע | שימוש אופייני |
|---|---|---|
ValueEventListener |
onDataChange() |
קריאה וחיפוש של שינויים בכל התוכן של נתיב. |
אפשר להשתמש ב-method onDataChange() כדי לקרוא תמונת מצב סטטית של התוכן בנתיב נתון, כפי שהיה בזמן האירוע. השיטה הזו מופעלת פעם אחת כשמצרפים את המאזין, ושוב בכל פעם שהנתונים משתנים, כולל הצאצאים. הקריאה החוזרת (callback) של האירוע מעבירה תמונת מצב שמכילה את כל הנתונים במיקום הזה, כולל נתוני הצאצאים. אם אין נתונים, תמונת המצב תחזיר את הערך false כשקוראים ל-exists() ואת הערך null כשקוראים ל-getValue().
בדוגמה הבאה מוצגת אפליקציה של בלוג חברתי שמאחזרת את פרטי הפוסט ממסד הנתונים:
Kotlin
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. ל-method הזו מועבר אובייקט DatabaseError שמציין את הסיבה לכשל.
קריאת נתונים פעם אחת
קריאה פעם אחת באמצעות()get
ה-SDK נועד לנהל אינטראקציות עם שרתי מסדי נתונים, גם כשהאפליקציה אונליין וגם כשהיא אופליין.
באופן כללי, כדאי להשתמש בשיטות של ValueEventListener שמתוארות למעלה כדי לקרוא נתונים ולקבל התראות על עדכונים לנתונים מה-backend. השיטות של המאזינים מפחיתות את השימוש והחיוב, והן מותאמות כדי לספק למשתמשים את החוויה הטובה ביותר כשהם עוברים בין מצב אונליין למצב אופליין.
אם אתם צריכים את הנתונים רק פעם אחת, אתם יכולים להשתמש ב-get() כדי לקבל תמונת מצב של הנתונים ממסד הנתונים. אם מסיבה כלשהי get() לא יכול להחזיר את הערך של השרת, הלקוח יבדוק את המטמון של האחסון המקומי ויחזיר שגיאה אם הערך עדיין לא נמצא.
שימוש מיותר ב-get() יכול להגדיל את השימוש ברוחב הפס ולהוביל לאובדן ביצועים. אפשר למנוע את זה באמצעות מאזין בזמן אמת, כמו שמוצג למעלה.
Kotlin
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
@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
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(), כמו בדוגמה הזו שבה הפוסט החדש נוצר בשני המיקומים. עדכונים בו-זמניים שמתבצעים בדרך הזו הם אטומיים: או שכל העדכונים מצליחים או שכולם נכשלים.
הוספת קריאה חוזרת (callback) להשלמה
אם אתם רוצים לדעת מתי הנתונים נשמרו, אתם יכולים להוסיף מאזין להשלמה. גם setValue() וגם updateChildren() מקבלים מאזין השלמה אופציונלי שנקרא כשהכתיבה בוצעה בהצלחה למסד הנתונים. אם הקריאה נכשלה, המאזין מקבל אובייקט שגיאה שמציין את הסיבה לכישלון.
Kotlin
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() גם בכל מאזין צאצא כדי להסיר את הקריאה החוזרת (callback).
שמירת נתונים כעסקאות
כשעובדים עם נתונים שעלולים להיפגם כתוצאה משינויים בו-זמניים, כמו מונים מצטברים, אפשר להשתמש בפעולת טרנזקציה. לפעולה הזו יש שני ארגומנטים: פונקציית עדכון וקריאה חוזרת (callback) אופציונלית להשלמה. פונקציית העדכון מקבלת את המצב הנוכחי של הנתונים כארגומנט, ומחזירה את המצב החדש שרוצים לכתוב. אם לקוח אחר כותב למיקום לפני שהערך החדש נכתב בהצלחה, פונקציית העדכון נקראת שוב עם הערך הנוכחי החדש, והכתיבה מתבצעת שוב.
לדוגמה, באפליקציית הבלוגים החברתיים, אתם יכולים לאפשר למשתמשים לסמן פוסטים בכוכב ולבטל את הסימון, ולעקוב אחרי מספר הכוכבים שפוסט קיבל באופן הבא:
Kotlin
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
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 מסנכרן את הנתונים האלה עם שרתי מסד הנתונים המרוחקים ועם לקוחות אחרים על בסיס "המאמץ המרבי".
כתוצאה מכך, כל פעולות הכתיבה במסד הנתונים מפעילות אירועים מקומיים באופן מיידי, לפני כל אינטראקציה עם השרת. זה אומר שהאפליקציה תמשיך להגיב, בלי קשר לזמן האחזור או לקישוריות של הרשת.
אחרי שהקישוריות נוצרת מחדש, האפליקציה מקבלת את קבוצת האירועים המתאימה כדי שהלקוח יסתנכרן עם מצב השרת הנוכחי, בלי שתצטרכו לכתוב קוד בהתאמה אישית.
נדבר עוד על התנהגות אופליין במידע נוסף על יכולות אונליין ואופליין.