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