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

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

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

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

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

  1. הוספה של שורת קוד להגדרות הבדיקה של האפליקציה כדי להתחבר לאמולטור.
  2. מהרמה הבסיסית (root) של ספריית הפרויקט המקומית, מריצים את firebase emulators:start.
  3. ביצוע שיחות מקוד אב הטיפוס של האפליקציה באמצעות פלטפורמת Realtime Database ב-SDK כרגיל, או באמצעות ה-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 כדי לקרוא את הנתונים בנתיב נתון, כפי שהוא קיים במועד האירוע. השיטה הזו מופעלת פעם אחת כאשר מצורף, ושוב בכל פעם שהנתונים, כולל ילדים, שינויים. הקריאה החוזרת של האירוע מועברת באמצעות 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 הן אופציונליות בלוק ההשלמה שנשלח כשהכתיבה בוצעה מסד נתונים. ה-listener הזה יכול להיות שימושי למעקב אחרי הנתונים שנאספו שנשמרו ואילו נתונים עדיין מסונכרנים. אם השיחה נכשלה, אובייקט השגיאה מועבר ל-listener, שמציין למה הכשל התרחש.

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

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

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

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

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

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

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

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

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

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

השלבים הבאים