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

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

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

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

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

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

כאן אפשר למצוא הסבר מפורט על 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.

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

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

אם אתם צריכים את הנתונים רק פעם אחת, אתם יכולים להשתמש ב-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 אחת.

ניתוק של listeners

הצופים לא מפסיקים אוטומטית את סנכרון הנתונים כשעוזבים את ViewController. אם לא מסירים את האפשרות 'צפייה' בצורה תקינה, הנתונים ממשיכים להסתנכרן עם הזיכרון המקומי. כשאין יותר צורך באובייקט observer, אפשר להסיר אותו על ידי העברת FIRDatabaseHandle המשויך לשיטה removeObserverWithHandle.

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

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

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

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

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

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

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

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

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

מידע נוסף על יכולות אונליין ואופליין

השלבים הבאים