(אופציונלי) אב-טיפוס ובדיקה באמצעות Firebase Local Emulator Suite
לפני שמדברים על האופן שבו האפליקציה קוראת וכותבת אל Realtime Database, בואו להציג קבוצת כלים שבהם תוכלו להשתמש כדי ליצור אב טיפוס ולבדוק את Realtime Database פונקציונליות: Firebase Local Emulator Suite. אם אתם רוצים לנסות מודלים שונים של נתונים, לבצע אופטימיזציה של כללי האבטחה או למצוא את הדרך היעילה ביותר מבחינת עלות לאינטראקציה עם הקצה העורפי, יכול להיות שעבודה מקומית ללא פריסה של שירותים פעילים תהיה רעיון מצוין.
אמולטור Realtime Database הוא חלק מ-Local Emulator Suite, מאפשרת לאפליקציה לבצע פעולות באמולציה של התוכן של מסד הנתונים ושל ההגדרות שלו, כמו וגם באופן אופציונלי את משאבי הפרויקט האמולציה (פונקציות, מסדי נתונים אחרים, וכללי אבטחה).
כדי להשתמש במהדמ של Realtime Database, צריך לבצע כמה שלבים פשוטים:
- הוספה של שורת קוד להגדרות הבדיקה של האפליקציה כדי להתחבר לאמולטור.
- מהרמה הבסיסית (root) של ספריית הפרויקט המקומית, מריצים את
firebase emulators:start
. - ביצוע שיחות מקוד אב הטיפוס של האפליקציה באמצעות פלטפורמת Realtime Database ב-SDK כרגיל, או באמצעות ה-API ל-REST של Realtime Database.
יש הדרכה מפורטת בנושא Realtime Database ו-Cloud Functions. כדאי גם לקרוא את המבוא בנושא Local Emulator Suite.
אחזור של FIRDatabaseReference
כדי לקרוא או לכתוב נתונים מהמסד נתונים, צריך מופע של FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
כתיבת נתונים
במסמך הזה נסביר על העקרונות הבסיסיים של קריאה וכתיבה של נתונים ב-Firebase.
נתוני Firebase נכתבים בהפניה של Database
ומאוחזרים על ידי
צירוף אוזן אסינכרוני לקובץ העזר. ה-listener מופעל
פעם אחת למצב הראשוני של הנתונים ופעם אחת בכל פעם שהנתונים משתנים.
פעולות כתיבה בסיסיות
כדי לבצע פעולות כתיבה בסיסיות, אפשר להשתמש ב-setValue
כדי לשמור נתונים
ולהחליף את כל הנתונים הקיימים בנתיב הזה. אפשר להשתמש בשיטה הזו כדי:
- סוגי הכרטיסים שתואמים לסוגי ה-JSON הזמינים באופן הבא:
NSString
NSNumber
NSDictionary
NSArray
לדוגמה, אפשר להוסיף משתמש עם setValue
באופן הבא:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
שימוש ב-setValue
באופן הזה גורם להחליף את הנתונים במיקום שצוין, כולל צמתים צאצאים. עם זאת, עדיין אפשר לעדכן ילד או ילדה בלי
שכתוב של כל האובייקט. אם רוצים לאפשר למשתמשים לעדכן את הפרופילים שלהם
אפשר לעדכן את שם המשתמש באופן הבא:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
קריאת הנתונים
קריאת נתונים על ידי האזנה לאירועים משמעותיים
כדי לקרוא נתונים בנתיבים ולעקוב אחרי שינויים, משתמשים ב-observeEventType:withBlock
של FIRDatabaseReference
כדי לצפות באירועי FIRDataEventTypeValue
.
סוג האירוע | שימוש אופייני |
---|---|
FIRDataEventTypeValue |
קריאה והאזנה לשינויים בכל התוכן של הנתיב. |
אפשר להשתמש באירוע FIRDataEventTypeValue
כדי לקרוא את הנתונים בנתיב נתון,
כפי שהוא קיים במועד האירוע. השיטה הזו מופעלת פעם אחת כאשר
מצורף, ושוב בכל פעם שהנתונים, כולל ילדים,
שינויים. הקריאה החוזרת של האירוע מועברת באמצעות snapshot
שמכיל את כל הנתונים
המיקום, כולל נתוני ילדים. אם אין נתונים, קובץ snapshot יחזיר את הערך false
כשקוראים לפונקציה exists()
ואת הערך nil
כשקוראים לנכס value
שלו.
הדוגמה הבאה ממחישה אפליקציית בלוגים חברתית שמאחזרת את פרטי הפוסט מהמסד הנתונים:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
ה-listener מקבל FIRDataSnapshot
שמכיל את הנתונים ברמה שצוינה
במסד הנתונים בזמן האירוע בנכס value
. אפשר להקצות את הערכים לסוג המקורי המתאים, כמו NSDictionary
.
אם לא קיימים נתונים במיקום, הערך של value
הוא nil
.
קריאת הנתונים פעם אחת
קריאה אחת באמצעות getData()
ערכת ה-SDK מיועדת לנהל אינטראקציות עם שרתי מסדי נתונים, האפליקציה במצב אונליין או אופליין.
באופן כללי, צריך להשתמש בטכניקות של אירועים בעלי ערך שתוארו למעלה כדי לקרוא כדי לקבל התראות על עדכונים בנתונים מהקצה העורפי. השיטות האלה מפחיתות את מקרי השימוש והחיוב, והם מותאמים במיוחד כדי לספק למשתמשים במהלך הגלישה באינטרנט ובמצב אופליין.
אם דרושים לך הנתונים רק פעם אחת, אפשר להשתמש ב-getData()
כדי לקבל תמונת מצב
ממסד הנתונים. אם מסיבה כלשהי getData()
לא יכול להחזיר את
ערך השרת, הלקוח יבדוק את המטמון של האחסון המקומי ויחזיר שגיאה
אם הערך עדיין לא נמצא.
הדוגמה הבאה ממחישה אחזור של שם משתמש של משתמש שגלוי לכולם פעם אחת ממסד הנתונים:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
שימוש מיותר ב-getData()
עלול להגביר את השימוש ברוחב הפס ולהוביל לאובדן הנתונים
של הביצועים, שניתן למנוע באמצעות שימוש ב-listener בזמן אמת, כפי שמוצג
למעלה.
קריאת הנתונים פעם אחת עם צופה
במקרים מסוימים, כדאי להחזיר את הערך מהמטמון המקומי באופן מיידי, במקום לבדוק אם יש ערך מעודכן בשרת. באלה
אפשר להשתמש בפונקציה observeSingleEventOfType
כדי לקבל את הנתונים
המטמון המקומי של הדיסק באופן מיידי.
האפשרות הזו שימושית לנתונים שצריך לטעון רק פעם אחת, ושלא סביר שהם ייטענו משתנים לעיתים קרובות או מחייבים האזנה פעילה. למשל, אפליקציית הבלוג בדוגמאות הקודמות, משתמשת בשיטה הזו כדי לטעון פרופיל של משתמש כשהוא מתחילים לכתוב פוסט חדש:
Swift
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
Objective-C
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
עדכון או מחיקה של נתונים
עדכון שדות ספציפיים
לכתוב בו-זמנית לצאצאים ספציפיים של צומת בלי להחליף צומת אחר
צמתים צאצאים, צריך להשתמש בשיטה updateChildValues
.
בשיחה עם updateChildValues
, ניתן לעדכן ערכים של צאצאים ברמה נמוכה יותר עד
שמציין נתיב למפתח. אם הנתונים מאוחסנים בכמה מיקומים כדי להתאים לעומס
אפשר לעדכן את כל המופעים של הנתונים האלה באמצעות
מאוורר מאוורר. לדוגמה,
אפליקציית בלוגים חברתית עשויה ליצור פוסט במקביל ולעדכן אותו
פיד הפעילות האחרונה ופיד הפעילות של המשתמש שמפרסם את התוכן. כדי לעשות את זה,
יישום הבלוג משתמש בקוד כמו:
Swift
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
Objective-C
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
בדוגמה הזו נעשה שימוש ב-childByAutoId
כדי ליצור פוסט בצומת שמכיל פוסטים של כל המשתמשים ב-/posts/$postid
, ובמקביל לאחזר את המפתח באמצעות getKey()
. לאחר מכן אפשר להשתמש במפתח כדי ליצור רשומה שנייה בפוסט של המשתמש בכתובת /user-posts/$userid/$postid
.
בעזרת הנתיבים האלה אפשר לבצע עדכונים בו-זמנית במספר מיקומים בעץ ה-JSON באמצעות קריאה אחת ל-updateChildValues
. לדוגמה, כך יוצרים את הפוסט החדש בשני המיקומים. עדכונים בו-זמניים שמתבצעים באופן הזה הם אטומיים: כל העדכונים מתבצעים בהצלחה או שהם כולם נכשלים.
הוספת בלוק השלמה
אם רוצים לדעת מתי הנתונים הוגדרו, אפשר להוסיף בלוק השלמה. גם setValue
וגם updateChildValues
הן אופציונליות
בלוק ההשלמה שנשלח כשהכתיבה בוצעה
מסד נתונים. ה-listener הזה יכול להיות שימושי למעקב אחרי הנתונים שנאספו
שנשמרו ואילו נתונים עדיין מסונכרנים. אם השיחה נכשלה,
אובייקט השגיאה מועבר ל-listener, שמציין למה הכשל התרחש.
Swift
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Objective-C
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
מחיקת נתונים
הדרך הפשוטה ביותר למחוק נתונים היא לקרוא לפונקציה removeValue
את המיקום של הנתונים האלה.
אפשר גם למחוק על ידי ציון nil
כערך של פעולת כתיבה אחרת
פעולה כמו setValue
או updateChildValues
. אפשר להשתמש בשיטה הזו.
עם updateChildValues
כדי למחוק כמה צאצאים בקריאה אחת ל-API.
ניתוק מאזינים
הצופים לא מפסיקים באופן אוטומטי לסנכרן נתונים כשיוצאים
ViewController
אם צופה לא מוסר כראוי, הוא ממשיך להסתנכרן
לזיכרון המקומי. כשאין יותר צורך במבצע מעקב, מסירים אותו על ידי העברת FIRDatabaseHandle
המשויך ל-method removeObserverWithHandle
.
כשמוסיפים בלוק של קריאה חוזרת לקובץ עזר, מוחזר FIRDatabaseHandle
.
אפשר להשתמש בכינויים האלה כדי להסיר את חסימת הקריאה החוזרת.
אם נוספו מספר מאזינים להפניה למסד נתונים, כל מאזינים
שנקראה כשמתרחש אירוע. כדי להפסיק את סנכרון הנתונים במיקום הזה,
עליך להסיר את כל התצפיתנים במיקום מסוים באמצעות קריאה לremoveAllObservers
.
אם מתקשרים אל removeObserverWithHandle
או removeAllObservers
דרך מאזינים
לא להסיר באופן אוטומטי מאזינים שרשומים בצמתים הצאצאים שלו, צריך גם
לעקוב אחר ההתייחסויות או הכינויים האלה כדי להסיר אותם.
שמירת נתונים כעסקאות
כשעובדים עם נתונים שעשויים להיפגם עקב שינויים בו-זמניים, כמו ספירות מצטברות, אפשר להשתמש בפעולת עסקה. אפשר להגדיר לפעולה הזו שני ארגומנטים: פונקציית עדכון ופונקציית השלמה של הקריאה החוזרת (callback). פונקציית העדכון לוקחת את המצב הנוכחי של הנתונים בתור ארגומנט ומחזירה את המצב הרצוי החדש שרוצים לכתוב.
למשל, באפליקציית הבלוג החברתית לדוגמה, אפשר לאפשר למשתמשים סימון פוסטים בכוכב וביטול הסימון בכוכב של פוסטים ומעקב אחרי מספר הכוכבים שקיבלו פוסט ככה:
Swift
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
Objective-C
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
שימוש בעסקה מונע טעות בספירת הכוכבים אם
משתמשים מסמנים אותו בכוכב באותו זמן או שללקוח היו נתונים לא פעילים. הערך שמכיל הכיתה FIRMutableData
הוא בהתחלה הערך האחרון שהלקוח מכיר לנתיב, או nil
אם אין ערך כזה. השרת משווה את
בערך הראשוני מול הערך הנוכחי שלו, ומקבל את העסקה אם
תואמים, או דוחה אותה. אם העסקה נדחית, השרת מחזיר
את הערך הנוכחי ללקוח, שיריץ את העסקה שוב עם
בערך המעודכן. האירוע הזה חוזר על עצמו עד שהעסקה מתקבלת או יותר מדי פעמים
בוצעו כמה ניסיונות.
מעברים אטומיים בצד השרת
בתרחיש לדוגמה שלמעלה אנחנו כותבים שני ערכים למסד הנתונים: המזהה של המשתמש שמסמן את הפוסט בכוכב או מבטל את הסימון שלו בכוכב, ואת ספירת הכוכבים המצטברת. אם כבר ידוע שהמשתמש מגדיר את הפוסט בכוכב, נוכל להשתמש במספר אטומי פעולה במקום עסקה.
Swift
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates)
Objective-C
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
הקוד הזה לא משתמש בפעולה של עסקה, ולכן הוא לא מופעל מחדש באופן אוטומטי אם יש עדכון מתנגש. עם זאת, מכיוון שפעולת ההגדלה קורה ישירות בשרת מסד הנתונים, אין סיכוי להתנגשות.
אם רוצים לזהות ולדחות התנגשויות ספציפיות לאפליקציה, כמו משתמש סימון פוסט שהוא כבר סימן בכוכב בעבר, עליך לכתוב פוסט מותאם אישית כללי אבטחה לתרחיש לדוגמה הזה.
עבודה עם נתונים במצב אופליין
אם הלקוח ינתק את החיבור לרשת, האפליקציה תמשיך לפעול בצורה נכונה.
לכל לקוח שמחובר למסד נתונים של Firebase יש גרסה פנימית משלו. של נתונים פעילים כלשהם. כשכותבים נתונים, הם נכתבים קודם בגרסה המקומית הזו. לאחר מכן לקוח Firebase מסנכרן את הנתונים האלה עם מסד הנתונים המרוחק. ועם לקוחות אחרים על ידי "הפעולה הטובה ביותר" במצב בסיסי.
כתוצאה מכך, כל הכתיבה במסד הנתונים מפעילה אירועים מקומיים באופן מיידי, לפני כל הנתונים נכתבים בשרת. פירוש הדבר הוא שהאפליקציה נשארת הוא רספונסיבי, ללא קשר לזמן האחזור ברשת או לקישוריות.
כשהקישוריות תתחדש, האפליקציה תקבל את הקבוצה המתאימה של האירועים, כך שהלקוח יסונכרן עם מצב השרת הנוכחי, בלי שהוא יצטרך כותבים כל קוד מותאם אישית.
נדבר יותר על התנהגות אופליין ב: למידע נוסף על יכולות אונליין ואופליין.