(אופציונלי) אב-טיפוס ובדיקה באמצעות חבילת האמולטור של Firebase
לפני שמדברים על האופן שבו האפליקציה קוראת וכותבת ממסד נתונים בזמן אמת, בואו להציג קבוצת כלים שבהם תוכלו להשתמש כדי ליצור אב-טיפוס ולבדוק את 'מסד נתונים בזמן אמת' פונקציונליות: חבילת אמולטור של Firebase. אם מנסים נתונים שונים לבצע אופטימיזציה של כללי האבטחה או לעבוד כדי למצוא דרך חסכונית לאינטראקציה עם הקצה העורפי, יכולת לעבוד באופן מקומי בלי לפרוס שירותים בזמן אמת יכול להיות רעיון מעולה.
אמולטור של מסדי נתונים בזמן אמת הוא חלק מחבילת האמולטור, מאפשרת לאפליקציה לבצע פעולות באמולציה של התוכן של מסד הנתונים ושל ההגדרות שלו, כמו וגם באופן אופציונלי את משאבי הפרויקט האמולציה (פונקציות, מסדי נתונים אחרים, וכללי אבטחה).emulator_Suite_short
כדי להשתמש באמולטור של Realtime Database, צריך לבצע כמה שלבים פשוטים:
- הוספת שורת קוד להגדרות הבדיקה של האפליקציה כדי להתחבר למהדר.
- מריצים את
firebase emulators:start
ברמה הבסיסית של ספריית הפרויקט המקומית. - ביצוע קריאות מקוד האב טיפוס של האפליקציה באמצעות ה-SDK של פלטפורמת Realtime Database כרגיל, או באמצעות ה-API ל-REST של Realtime Database.
יש הדרכה מפורטת בנושא מסדי נתונים בזמן אמת ו-Cloud Functions. מומלץ גם לעיין במבוא לחבילת אמולטור.
קבלת DatabaseReference
כדי לקרוא או לכתוב נתונים מהמסד נתונים, צריך מופע של DatabaseReference
:
DatabaseReference ref = FirebaseDatabase.instance.ref();
כתיבת נתונים
במסמך הזה מוסברים העקרונות הבסיסיים של הקריאה והכתיבה של נתונים ב-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
להאזנה
DatabaseEvent
שנ'.
אפשר להשתמש ב-DatabaseEvent
כדי לקרוא את הנתונים בנתיב נתון, כפי שהם קיימים בזמן האירוע. האירוע הזה מופעל פעם אחת כש
מתבצע צירוף של ה-listener שוב ושוב בכל פעם שהנתונים, כולל ילדים,
שינויים. לאירוע יש מאפיין snapshot
שמכיל את כל הנתונים במיקום הזה, כולל נתוני הצאצאים. אם אין נתונים, הערך של המאפיין exists
של קובץ snapshot יהיה 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()
כדי לקבל את הנתונים מהמטמון המקומי של הדיסק
באופן מיידי.
האפשרות הזו שימושית לנתונים שצריך לטעון רק פעם אחת, ולא צפויים להשתנות לעיתים קרובות או לדרוש הקשבה פעילה. למשל, אפליקציית הבלוג בדוגמאות הקודמות, משתמשת בשיטה הזו כדי לטעון פרופיל של משתמש כשהוא מתחילים לכתוב פוסט חדש:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
עדכון או מחיקה של נתונים
עדכון שדות ספציפיים
כדי לכתוב בו-זמנית לצאצאים ספציפיים של צומת בלי לשכתב צמתים צאצאים אחרים, משתמשים בשיטה update()
.
כשקוראים ל-update()
, אפשר לעדכן ערכים של צאצאים ברמה נמוכה יותר על ידי ציון נתיב למפתח. אם הנתונים מאוחסנים במספר מיקומים כדי לשפר את יכולת ההתאמה לעומס, אפשר לעדכן את כל המופעים של הנתונים באמצעות הרחבת נתונים. לדוגמה, אפליקציה של בלוג חברתי עשויה ליצור פוסט ולעדכן אותו בו-זמנית בפיד הפעילות האחרונה ובפיד הפעילות של המשתמש שפרסמ את הפוסט. כדי לעשות זאת, באפליקציית הבלוגים נעשה שימוש בקוד כזה:
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) כהשלמה
אם אתם רוצים לדעת מתי הנתונים הושלמו, תוכלו לרשום פונקציות קריאה חוזרת (callbacks) להשלמה. גם set()
וגם update()
מחזירים ערכים מסוג Future
, שאפשר לצרף אליהם פונקציות קריאה חוזרת (callbacks) של הצלחה ושגיאה, שיופעלו כשהכתיבה בוצעה במסד הנתונים וכשהקריאה נכשלה.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
מחיקת נתונים
הדרך הפשוטה ביותר למחוק נתונים היא להפעיל את remove()
על הפניה למיקום של הנתונים האלה.
אפשר גם למחוק על ידי ציון null בתור הערך של פעולת כתיבה אחרת, כמו set()
או update()
. אפשר להשתמש בשיטה הזו עם update()
כדי למחוק כמה צאצאים בקריאה אחת ל-API.
שמירת נתונים כעסקאות
כשעובדים עם נתונים שעלולים להיפגם כתוצאה משינויים בו-זמנית,
כמו מוניים מצטברים, תוכלו להשתמש בעסקה על ידי העברת
הגורם המטפל בעסקה אל 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()
למספר
נותנים 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 מסנכרן את הנתונים האלה עם שרתי מסדי הנתונים המרוחקים ועם לקוחות אחרים על בסיס 'לפי יכולת'.
כתוצאה מכך, כל הכתיבה במסד הנתונים מפעילה אירועים מקומיים באופן מיידי, לפני שנתונים נכתבים בשרת. המשמעות היא שהאפליקציה תמשיך להגיב במהירות, ללא קשר לזמן האחזור או לקישוריות של הרשת.
כשהקישוריות תתחדש, האפליקציה תקבל את הקבוצה המתאימה של האירועים, כך שהלקוח יסונכרן עם מצב השרת הנוכחי, בלי שהוא יצטרך כותבים כל קוד מותאם אישית.
נדבר יותר על התנהגות אופליין ב: למידע נוסף על יכולות אונליין ואופליין.