קריאה וכתיבה של נתונים בפלטפורמות של Apple

(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות Firebase Local Emulator Suite

לפני שנדבר על האופן שבו האפליקציה קוראת נתונים מ- וכותבת נתונים ל-Realtime Database, נציג קבוצת כלים שבהם אפשר להשתמש כדי ליצור אב טיפוס ולבדוק את הפונקציונליות של Realtime Database: Firebase Local Emulator Suite. אם אתם מנסים מודלים שונים של נתונים, מבצעים אופטימיזציה של כללי האבטחה או מחפשים את הדרך הכי משתלמת לאינטראקציה עם הקצה העורפי, כדאי לעבוד באופן מקומי בלי לפרוס שירותים פעילים.

אמולטור Realtime Database הוא חלק מ-Local Emulator Suite, שמאפשר לאפליקציה שלכם לקיים אינטראקציה עם התוכן וההגדרות של מסד הנתונים המדומה, וגם עם משאבי הפרויקט המדומים (פונקציות, מסדי נתונים אחרים וכללי אבטחה).

כדי להשתמש באמולטור של Realtime Database, צריך לבצע כמה שלבים:

  1. הוספת שורת קוד להגדרת הבדיקה של האפליקציה כדי להתחבר לאמולטור.
  2. מפעילים את הפקודה firebase emulators:start מהשורש של ספריית הפרויקט המקומית.
  3. ביצוע קריאות מקוד אב הטיפוס של האפליקציה באמצעות ערכת SDK של פלטפורמת Realtime Database כרגיל, או באמצעות ה-API בארכיטקטורת REST של Realtime Database.

הדרכה מפורטת שכוללת את Realtime Database ו-Cloud Functions זמינה. כדאי גם לעיין במבוא ל-Local Emulator Suite.

קבלת FIRDatabaseReference

כדי לקרוא נתונים ממסד הנתונים או לכתוב אליו, צריך מופע של FIRDatabaseReference:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

כתיבת נתונים

המסמך הזה עוסק ביסודות של קריאה וכתיבה של נתונים ב-Firebase.

נתוני Firebase נכתבים בהפניה Database, ואפשר לאחזר אותם על ידי צירוף מאזין אסינכרוני להפניה. המאזין מופעל פעם אחת למצב הראשוני של הנתונים, ופעם נוספת בכל פעם שהנתונים משתנים.

פעולות כתיבה בסיסיות

לפעולות כתיבה בסיסיות, אפשר להשתמש ב-setValue כדי לשמור נתונים בהפניה שצוינה, ולהחליף את הנתונים הקיימים בנתיב הזה. אתם יכולים להשתמש בשיטה הזו כדי:

  • סוגי הכרטיסים שתואמים לסוגי ה-JSON הזמינים הם:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

לדוגמה, אפשר להוסיף משתמש עם setValue באופן הבא:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

שימוש ב-setValue באופן הזה מחליף את הנתונים במיקום שצוין, כולל צומתי צאצא. עם זאת, עדיין אפשר לעדכן צאצא בלי לשכתב את כל האובייקט. אם רוצים לאפשר למשתמשים לעדכן את הפרופילים שלהם, אפשר לעדכן את שם המשתמש באופן הבא:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

קריאת נתונים

קריאת נתונים באמצעות האזנה לאירועי ערך

כדי לקרוא נתונים בנתיב ולהאזין לשינויים, משתמשים ב-observeEventType:withBlock של FIRDatabaseReference כדי לצפות באירועי FIRDataEventTypeValue.

סוג האירוע שימוש אופייני
FIRDataEventTypeValue קריאה וחיפוש של שינויים בכל התוכן של נתיב.

אפשר להשתמש באירוע FIRDataEventTypeValue כדי לקרוא את הנתונים בנתיב נתון, כפי שהם קיימים בזמן האירוע. השיטה הזו מופעלת פעם אחת כשמצרפים את המאזין, ופעם נוספת בכל פעם שהנתונים משתנים, כולל צאצאים. הקריאה החוזרת (callback) של האירוע מקבלת snapshot שמכיל את כל הנתונים במיקום הזה, כולל נתונים של צאצאים. אם אין נתונים, תמונת המצב תחזיר את הערך false כשקוראים ל-exists() ואת הערך nil כשקוראים את המאפיין value שלה.

בדוגמה הבאה מוצגת אפליקציה של בלוג חברתי שמאחזרת את פרטי הפוסט ממסד הנתונים:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

המאזין מקבל FIRDataSnapshot שמכיל את הנתונים במיקום שצוין במסד הנתונים בזמן האירוע במאפיין value שלו. אפשר להקצות את הערכים לסוג המקורי המתאים, כמו NSDictionary. אם אין נתונים במיקום, הערך של value הוא nil.

קריאת נתונים פעם אחת

קריאה פעם אחת באמצעות getData()‎

ה-SDK נועד לנהל אינטראקציות עם שרתי מסדי נתונים, גם כשהאפליקציה אונליין וגם כשהיא אופליין.

באופן כללי, כדאי להשתמש בשיטות של אירועי ערכים שמתוארות למעלה כדי לקרוא נתונים ולקבל התראות על עדכונים לנתונים מה-backend. השיטות האלה מפחיתות את השימוש והחיוב, והן מותאמות כדי לספק למשתמשים את החוויה הטובה ביותר כשהם עוברים בין מצב אונליין למצב אופליין.

אם אתם צריכים את הנתונים רק פעם אחת, אתם יכולים להשתמש ב-getData() כדי לקבל תמונת מצב של הנתונים ממסד הנתונים. אם מסיבה כלשהי getData() לא יכול להחזיר את ערך השרת, הלקוח יבדוק את המטמון של האחסון המקומי ויחזיר שגיאה אם הערך עדיין לא נמצא.

בדוגמה הבאה מוצג אחזור של שם המשתמש שגלוי לכולם פעם אחת ממסד הנתונים:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
[[[_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.

כשמוסיפים בלוק קריאה חוזרת (callback) להפניה, מוחזר FIRDatabaseHandle. אפשר להשתמש בכינויים האלה כדי להסיר את בלוק הקריאה החוזרת (callback).

אם נוספו כמה מאזינים להפניה למסד נתונים, כל מאזין נקרא כשמופעל אירוע. כדי להפסיק את סנכרון הנתונים במיקום הזה, צריך להסיר את כל הצופים במיקום באמצעות קריאה ל-method‏ removeAllObservers.

קריאה ל-removeObserverWithHandle או ל-removeAllObservers במאזין לא מסירה באופן אוטומטי מאזינים שרשומים בצומתי הצאצא שלו. צריך גם לעקוב אחרי ההפניות או נקודות האחיזה האלה כדי להסיר אותן.

שמירת נתונים כעסקאות

כשעובדים עם נתונים שעלולים להיפגם כתוצאה משינויים בו-זמניים, כמו מונים מצטברים, אפשר להשתמש בפעולת טרנזקציה. לפעולה הזו יש שני ארגומנטים: פונקציית עדכון וקריאה חוזרת (callback) אופציונלית להשלמה. פונקציית העדכון מקבלת את המצב הנוכחי של הנתונים כארגומנט, ומחזירה את המצב החדש שרוצים לכתוב.

לדוגמה, באפליקציית הבלוגים החברתיים, אתם יכולים לאפשר למשתמשים לסמן פוסטים בכוכב ולבטל את הסימון, ולעקוב אחרי מספר הכוכבים שפוסט קיבל באופן הבא:

Swift

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
[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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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

הערה: מוצר Firebase הזה לא זמין ביעד App Clip.
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 מסנכרן את הנתונים האלה עם שרתי מסד הנתונים המרוחקים ועם לקוחות אחרים על בסיס "המאמץ המרבי".

כתוצאה מכך, כל הכתיבות למסד הנתונים מפעילות אירועים מקומיים באופן מיידי, לפני שנתונים כלשהם נכתבים בשרת. זה אומר שהאפליקציה תמשיך להגיב, בלי קשר לזמן האחזור או לקישוריות של הרשת.

אחרי שהקישוריות נוצרת מחדש, האפליקציה מקבלת את קבוצת האירועים המתאימה כדי שהלקוח יסתנכרן עם מצב השרת הנוכחי, בלי שתצטרכו לכתוב קוד בהתאמה אישית.

נדבר עוד על התנהגות אופליין במידע נוסף על יכולות אונליין ואופליין.

השלבים הבאים