קריאה וכתיבה של נתונים בפלטפורמות של 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 הזה לא זמין ביעד של קטע מקדים לאפליקציה.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד של קטע מקדים לאפליקציה.
@property (strong, nonatomic) FIRDatabaseReference *ref;

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

כתיבת נתונים

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

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

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

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

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

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

Swift

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

Objective-C

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

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

Swift

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

Objective-C

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

קריאת נתונים

קריאת נתונים על ידי האזנה לאירועי ערך

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

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

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

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

Swift

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

Objective-C

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

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

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

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

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

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

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

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

Swift

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

Objective-C

הערה: מוצר Firebase הזה לא זמין ביעד של קטע מקדים לאפליקציה.
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

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

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

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

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

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

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

Swift

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

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

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

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

השלבים הבאים