(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות 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.
אחזור של 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
, והאחזור שלהם מתבצע על ידי צירוף מאזין לא סינכרוני למשתנה העזר. המאזין מופעל פעם אחת עבור המצב הראשוני של הנתונים ופעם נוספת בכל פעם שהנתונים משתנים.
פעולות כתיבה בסיסיות
לפעולות כתיבה בסיסיות, אפשר להשתמש ב-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
כדי לקרוא את הנתונים בנתיב נתון, כפי שהם קיימים בזמן האירוע. השיטה הזו מופעלת פעם אחת כשהמאזין מצורף, ופעם נוספת בכל פעם שהנתונים, כולל הצאצאים, משתנים. בקריאה החוזרת (callback) של האירוע מועבר 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; // ... }];
המאזין מקבל 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()
עלול להגדיל את השימוש ברוחב הפס ולהוביל לירידה בביצועים. אפשר למנוע זאת באמצעות שימוש במאזין בזמן אמת, כפי שמתואר למעלה.
קריאת נתונים פעם אחת באמצעות משתמש שמתבונן
במקרים מסוימים, כדאי להחזיר את הערך מהמטמון המקומי באופן מיידי, במקום לבדוק אם יש ערך מעודכן בשרת. במקרים כאלה, אפשר להשתמש ב-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
מקבלים בלוק השלמה אופציונלי שנקרא אחרי שהכתיבה הושלמה במסד הנתונים. אפשר להשתמש במאזין הזה כדי לעקוב אחרי הנתונים שנשמרו ואחרי הנתונים שעדיין נמצאים בתהליך סנכרון. אם הקריאה נכשלה, למאזין מועבר אובייקט שגיאה שמציין את הסיבה לכישלון.
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
.
אפשר להשתמש במזהים האלה כדי להסיר את הבלוק של קריאת החזרה (callback).
אם הוספתם כמה מאזינים להפניה למסד נתונים, כל מאזין ייכלל כשאירוע יתרחש. כדי להפסיק את סנכרון הנתונים במיקום הזה, צריך להסיר את כל המשתמשים שמנטרלים את המיקום באמצעות קריאה ל-method removeAllObservers
.
קריאה ל-removeObserverWithHandle
או ל-removeAllObservers
על מאזין לא מסירה באופן אוטומטי מאזינים שרשומים בצמתים הצאצאים שלו. כדי להסיר אותם, צריך גם לעקוב אחרי ההפניות או כינויי המזהה האלה.
שמירת נתונים כעסקאות
כשעובדים עם נתונים שעשויים להיפגם עקב שינויים בו-זמניים, כמו ספירות מצטברות, אפשר להשתמש בפעולת עסקה. מעבירים לפעולה הזו שני ארגומנטים: פונקציית עדכון ופונקציית קריאה חוזרת אופציונלית לסיום. פונקציית העדכון מקבלת את המצב הנוכחי של הנתונים כארגומנטים ומחזירה את המצב הרצוי החדש שרוצים לכתוב.
לדוגמה, באפליקציית הבלוגים החברתית לדוגמה, אפשר לאפשר למשתמשים לסמן פוסט בכוכב ולהסיר את הסימון, ולעקוב אחרי מספר הכוכבים שקיבל הפוסט באופן הבא:
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 מסנכרן את הנתונים האלה עם שרתי מסדי הנתונים המרוחקים ועם לקוחות אחרים על בסיס 'לפי יכולת'.
כתוצאה מכך, כל פעולות הכתיבה במסד הנתונים מפעילות אירועים מקומיים באופן מיידי, לפני שנתונים נכתבים בשרת. המשמעות היא שהאפליקציה תמשיך להגיב במהירות, ללא קשר לזמן האחזור או לקישוריות של הרשת.
אחרי שהקישור מתחדש, האפליקציה מקבלת את קבוצת האירועים המתאימה כדי שהלקוח יתעדכן עם מצב השרת הנוכחי, בלי שתצטרכו לכתוב קוד מותאם אישית.
מידע נוסף על התנהגות אופליין זמין במאמר מידע נוסף על יכולות אונליין ואופליין.