(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות כלים לאמולטור של Firebase
לפני שנסביר איך האפליקציה קוראת מ-Realtime Database וכותבת אליו, נציג קבוצה של כלים שבהם אפשר להשתמש כדי ליצור אב טיפוס ולבדוק את הפונקציונליות של Realtime Database: חבילת Firebase Emulator Suite. אם אתם מנסים מודלים שונים של נתונים, מבצעים אופטימיזציה של כללי האבטחה או מנסים למצוא את הדרך הכי חסכונית לאינטראקציה עם ה-Back-end, כדאי לעבוד באופן מקומי בלי לפרוס שירותים פעילים.
אמולטור של מסד נתונים בזמן אמת הוא חלק מכלים לאמולטור, שמאפשרת לאפליקציה שלכם ליצור אינטראקציה עם התוכן וההגדרות של מסד הנתונים המדומה, וגם עם משאבי הפרויקט המדומה (פונקציות, מסדי נתונים אחרים וכללי אבטחה).emulator_suite_short
השימוש באמולטור של מסד נתונים בזמן אמת כולל רק כמה שלבים:
- הוספת שורת קוד להגדרת הבדיקה של האפליקציה כדי להתחבר לאמולטור.
- מהספרייה הראשית של פרויקט מקומי, מריצים את הפקודה
firebase emulators:start. - שליחת קריאות מקוד האב-טיפוס של האפליקציה באמצעות SDK של פלטפורמת מסד נתונים בזמן אמת כרגיל, או באמצעות API בארכיטקטורת REST של מסד נתונים בזמן אמת.
הדרכה מפורטת על השימוש ב-מסד נתונים בזמן אמת וב-Cloud Functions זמינה. מומלץ גם לעיין במבוא ל-Emulator Suite.
קבלת DatabaseReference
כדי לקרוא או לכתוב נתונים במסד הנתונים, צריך מופע של DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
כתיבת נתונים
במאמר הזה מוסבר איך לקרוא נתונים מ-Firebase ולכתוב נתונים ל-Firebase.
נתוני Firebase נכתבים אל DatabaseReference ואפשר לאחזר אותם באמצעות המתנה או האזנה לאירועים שמופקים על ידי ההפניה. האירועים מופקים פעם אחת עבור המצב הראשוני של הנתונים, ושוב בכל פעם שהנתונים משתנים.
פעולות כתיבה בסיסיות
בפעולות כתיבה בסיסיות, אפשר להשתמש ב-set() כדי לשמור נתונים בהפניה שצוינה, ולהחליף את הנתונים הקיימים בנתיב הזה. אפשר להגדיר הפניה לסוגים הבאים: String, boolean, int, double, Map, List.
לדוגמה, אפשר להוסיף משתמש עם set() באופן הבא:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
שימוש ב-set() באופן הזה מחליף את הנתונים במיקום שצוין, כולל כל צמתי הצאצא. אבל עדיין אפשר לעדכן ילד בלי לכתוב מחדש את כל האובייקט. אם רוצים לאפשר למשתמשים לעדכן את הפרופילים שלהם, אפשר לעדכן את שם המשתמש באופן הבא:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
השיטה update() מקבלת נתיב משנה לצמתים, ומאפשרת לעדכן כמה צמתים במסד הנתונים בבת אחת:
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
קריאת נתונים
קריאת נתונים באמצעות הקשבה לאירועים של ערכים
כדי לקרוא נתונים בנתיב ולהאזין לשינויים, משתמשים במאפיין onValue של DatabaseReference כדי להאזין ל-DatabaseEvents.
אפשר להשתמש ב-DatabaseEvent כדי לקרוא את הנתונים בנתיב מסוים, כפי שהם קיימים בזמן האירוע. האירוע הזה מופעל פעם אחת כשהמאזין מצורף, ושוב בכל פעם שהנתונים, כולל צאצאים, משתנים. לאירוע יש מאפיין snapshot שמכיל את כל הנתונים במיקום הזה, כולל נתוני צאצאים. אם אין נתונים, המאפיין exists של התמונה יהיה false והמאפיין value שלה יהיה null.
בדוגמה הבאה מוצגת אפליקציה לבלוגים ברשתות החברתיות שמאחזרת את פרטי הפוסט ממסד הנתונים:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
המאזין מקבל DataSnapshot שמכיל את הנתונים במיקום שצוין במסד הנתונים בזמן האירוע במאפיין value שלו.
קריאת נתונים פעם אחת
קריאה חד-פעמית באמצעות get()
ה-SDK מיועד לניהול אינטראקציות עם שרתי מסדי נתונים, בין אם האפליקציה שלכם אונליין או אופליין.
באופן כללי, כדאי להשתמש בטכניקות של אירועי ערכים שמתוארות למעלה כדי לקרוא נתונים ולקבל עדכונים לגבי הנתונים מהקצה העורפי. הטכניקות האלה מצמצמות את השימוש ואת החיוב, והן מותאמות כדי לספק למשתמשים את החוויה הטובה ביותר כשהם מתחברים לאינטרנט ומתנתקים ממנו.
אם אתם צריכים את הנתונים רק פעם אחת, אתם יכולים להשתמש ב-get() כדי לקבל תמונת מצב של הנתונים ממסד הנתונים. אם מסיבה כלשהי get() לא מצליח להחזיר את ערך השרת, הלקוח יבדוק את מטמון האחסון המקומי ויחזיר שגיאה אם הערך עדיין לא נמצא.
בדוגמה הבאה מוצג אחזור של שם המשתמש שגלוי לכולם של משתמש מסוים ממסד הנתונים, פעם אחת:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
שימוש מיותר ב-get() עלול להגדיל את השימוש ברוחב הפס ולהוביל לירידה בביצועים. כדי למנוע זאת, אפשר להשתמש במאזין בזמן אמת כמו בדוגמה שלמעלה.
קריאת נתונים פעם אחת באמצעות once()
במקרים מסוימים, יכול להיות שתרצו שהערך ממטמון מקומי יוחזר באופן מיידי, במקום לבדוק אם יש ערך מעודכן בשרת. במקרים כאלה אפשר להשתמש ב-once() כדי לקבל את הנתונים ממטמון הדיסק המקומי באופן מיידי.
האפשרות הזו שימושית לנתונים שצריך לטעון רק פעם אחת, ושלא צפויים להשתנות לעיתים קרובות או לדרוש האזנה פעילה. לדוגמה, אפליקציית הבלוגים בדוגמאות הקודמות משתמשת בשיטה הזו כדי לטעון את הפרופיל של המשתמש כשהוא מתחיל לכתוב פוסט חדש:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
עדכון או מחיקה של נתונים
עדכון שדות ספציפיים
כדי לכתוב בו-זמנית לצאצאים ספציפיים של צומת בלי לדרוס צמתי צאצא אחרים, משתמשים בשיטה update().
כשקוראים ל-update(), אפשר לעדכן ערכי צאצא ברמה נמוכה יותר על ידי ציון נתיב למפתח. אם הנתונים מאוחסנים בכמה מיקומים כדי לשפר את יכולת ההתאמה, אפשר לעדכן את כל המופעים של הנתונים האלה באמצעות fan-out של נתונים. לדוגמה, אפליקציה לבלוגים ברשתות חברתיות יכולה ליצור פוסט ולעדכן אותו בו-זמנית בפיד הפעילות האחרונה ובפיד הפעילות של המשתמש שפרסם את הפוסט. לשם כך, אפליקציית הבלוגים משתמשת בקוד כמו זה:
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
בדוגמה הזו נעשה שימוש ב-push() כדי ליצור פוסט בצומת שמכיל פוסטים של כל המשתמשים ב-/posts/$postid, ובמקביל לאחזר את המפתח באמצעות key. אחר כך אפשר להשתמש במפתח כדי ליצור רשומה שנייה בפוסטים של המשתמש בכתובת /user-posts/$userid/$postid.
באמצעות הנתיבים האלה, אפשר לבצע עדכונים בו-זמנית לכמה מיקומים בעץ ה-JSON באמצעות קריאה אחת ל-update(), כמו בדוגמה הזו שבה נוצר הפוסט החדש בשני המיקומים. עדכונים סימולטניים שמתבצעים בדרך הזו הם אטומיים: או שכל העדכונים מצליחים או שכל העדכונים נכשלים.
הוספת קריאה חוזרת (callback) להשלמה
אם רוצים לדעת מתי הנתונים נשמרו, אפשר לרשום פונקציות קריאה חוזרת (callback) להשלמה. הפונקציות set() ו-update() מחזירות Futures, שאליהן אפשר לצרף קריאות חוזרות (callbacks) להצלחה ולשגיאה שמופעלות כשהכתיבה מתבצעת במסד הנתונים וכשהקריאה לא מצליחה.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
מחיקת נתונים
הדרך הפשוטה ביותר למחוק נתונים היא לקרוא ל-remove() בהפניה למיקום של הנתונים האלה.
אפשר גם למחוק על ידי ציון הערך null בפעולת כתיבה אחרת, כמו set() או update(). אפשר להשתמש בשיטה הזו עם update() כדי למחוק כמה ילדים בקריאה אחת ל-API.
שמירת נתונים כעסקאות
כשעובדים עם נתונים שיכולים להיפגם בגלל שינויים בו-זמניים, כמו מוניינים מצטברים, אפשר להשתמש בעסקאות על ידי העברת handler של עסקאות אל runTransaction(). פונקציה לטיפול בטרנזקציות מקבלת את המצב הנוכחי של הנתונים כארגומנט ומחזירה את המצב החדש הרצוי שרוצים לכתוב. אם לקוח אחר כותב למיקום לפני שהערך החדש שלכם נכתב בהצלחה, פונקציית העדכון שלכם נקראת שוב עם הערך הנוכחי החדש, והכתיבה מנסה שוב.
לדוגמה, באפליקציה של בלוג חברתי, אפשר לאפשר למשתמשים לסמן פוסטים בכוכב ולבטל את הסימון שלהם, ולעקוב אחרי מספר הכוכבים שפוסט קיבל באופן הבא:
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
כברירת מחדל, אירועים מופעלים בכל פעם שהפונקציה לעדכון העסקה פועלת.
לכן, אם מפעילים את הפונקציה כמה פעמים, יכול להיות שיוצגו מצבי ביניים.
אפשר להגדיר את applyLocally ל-false כדי להשבית את מצבי הביניים האלה ולחכות עד שהעסקה תושלם לפני שהאירועים יופעלו:
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
התוצאה של עסקה היא TransactionResult, שמכילה מידע כמו אם העסקה בוצעה ותמונת המצב החדשה:
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
ביטול עסקה
כדי לבטל טרנזקציה בצורה בטוחה, מתקשרים אל Transaction.abort() כדי להקפיץ הודעת שגיאה (throw) AbortTransactionException:
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
הגדלות אטומיות בצד השרת
בתרחיש השימוש שלמעלה, אנחנו כותבים שני ערכים למסד הנתונים: המזהה של המשתמש שסימן את הפוסט בכוכב או ביטל את הסימון, ומספר הכוכבים המוגדל. אם אנחנו כבר יודעים שהמשתמש סימן את הפוסט בכוכב, אנחנו יכולים להשתמש בפעולת הגדלה אטומית במקום בעסקה.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
הקוד הזה לא משתמש בפעולת טרנזקציה, ולכן הוא לא מופעל מחדש באופן אוטומטי אם יש עדכון שמתנגש איתו. עם זאת, מכיוון שפעולת ההגדלה מתבצעת ישירות בשרת מסד הנתונים, אין סיכוי לקונפליקט.
אם רוצים לזהות ולדחות התנגשויות ספציפיות לאפליקציה, כמו משתמש שמסמן בכוכב פוסט שכבר סומן בכוכב בעבר, צריך לכתוב כללי אבטחה בהתאמה אישית לתרחיש השימוש הזה.
עבודה עם נתונים במצב אופליין
אם לקוח מאבד את החיבור לרשת, האפליקציה תמשיך לפעול בצורה תקינה.
כל לקוח שמחובר למסד נתונים של Firebase שומר גרסה פנימית משלו של כל הנתונים הפעילים. כשנתונים נכתבים, הם נכתבים קודם לגרסה המקומית הזו. לאחר מכן, לקוח Firebase מסנכרן את הנתונים האלה עם שרתי מסד הנתונים המרוחק ועם לקוחות אחרים על בסיס 'המאמץ הטוב ביותר'.
כתוצאה מכך, כל פעולת כתיבה למסד הנתונים מפעילה מיידית אירועים מקומיים, לפני שנתונים נכתבים בשרת. המשמעות היא שהאפליקציה תמשיך להגיב בלי קשר לזמן האחזור או לקישוריות של הרשת.
אחרי שהקישוריות מתחדשת, האפליקציה מקבלת את קבוצת האירועים המתאימה כדי שהלקוח יסתנכרן עם המצב הנוכחי של השרת, בלי שתצטרכו לכתוב קוד בהתאמה אישית.
מידע נוסף על יכולות אונליין ואופליין