קרא וכתוב נתונים ב- iOS

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

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

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

השימוש באמולטור מסד הנתונים בזמן אמת כולל רק כמה צעדים:

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

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

קבל FIRDatabaseReference

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

מָהִיר

var ref: DatabaseReference!

ref = Database.database().reference()

מטרה-ג

@property (strong, nonatomic) FIRDatabaseReference *ref;

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

כתוב נתונים

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

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

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

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

  • העברת סוגי התואמים לסוגי JSON הזמינים כדלקמן:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

למשל, אתה יכול להוסיף משתמש עם setValue כדלקמן:

מָהִיר

self.ref.child("users").child(user.uid).setValue(["username": username])

מטרה-ג

[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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

מָהִיר

self.ref.child("users/\(user.uid)/username").setValue(username)

מטרה-ג

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

קרא נתונים

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

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

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

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

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

מָהִיר

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

מטרה-ג

_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

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

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

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

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

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

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

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

self.ref.child("users/\(user.uid)/username").getData { (error, snapshot) in
    if let error = error {
        print("Error getting data \(error)")
    }
    else if snapshot.exists() {
        print("Got data \(snapshot.value!)")
    }
    else {
        print("No data available")
    }
}

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

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

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

מָהִיר

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)
}

מטרה-ג

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

מָהִיר

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)

מטרה-ג

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

מָהִיר

ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

מטרה-ג

[[[_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 אל removeObserverWithHandle השיטה.

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

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

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

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

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

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

מָהִיר

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)
  }
}

מטרה-ג

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

תוספות בצד השרת האטומיות

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

מָהִיר

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);

מטרה-ג

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

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

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

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

הצעדים הבאים