שמירת נתונים באמצעות מסד נתונים בזמן אמת ב-Firebase עבור C++

שנתחיל?

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

קבלת DatabaseReference

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

    // Get the root reference location of the database.
    firebase::database::DatabaseReference dbref = database->GetReference();

שמירת נתונים

יש ארבע שיטות לכתיבת נתונים אל Firebase Realtime Database:

שיטה שימושים נפוצים
SetValue() כתיבה או החלפה של נתונים בנתיב מוגדר, כמו users/<user-id>/<username>.
PushChild() מוסיפים לרשימת נתונים. בכל פעם שמפעילים את הפונקציה Push(), ‏ Firebase יוצר מפתח ייחודי שאפשר להשתמש בו גם כמזהה ייחודי, כמו user-scores/<user-id>/<unique-score-id>.
UpdateChildren() לעדכן חלק מהמפתחות של נתיב מוגדר בלי להחליף את כל הנתונים.
RunTransaction() עדכון נתונים מורכבים שעלולים להיפגם בגלל עדכונים בו-זמניים.

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

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

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

  • ‫Null (האפשרות הזו מוחקת את הנתונים)
  • מספרים שלמים (64 ביט)
  • מספרים בשיטת נקודה צפה (floating-point) עם דיוק כפול
  • ערכים בוליאניים
  • מיתרים
  • וקטורים של וריאציות
  • מיפוי של מחרוזות לווריאציות

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

dbref.Child("users").Child(userId).Child("username").SetValue(name);

הוספה לרשימת נתונים

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

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

עדכון שדות ספציפיים

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

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

class LeaderboardEntry {
  std::string uid;
  int score = 0;

 public:
  LeaderboardEntry() {
  }

  LeaderboardEntry(std::string uid, int score) {
    this->uid = uid;
    this->score = score;
  }

  std::map&ltstd::string, Object&gt ToMap() {
    std::map&ltstring, Variant&gt result = new std::map&ltstring, Variant&gt();
    result["uid"] = Variant(uid);
    result["score"] = Variant(score);

    return result;
  }
}

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

void WriteNewScore(std::string userId, int score) {
  // Create new entry at /user-scores/$userid/$scoreid and at
  // /leaderboard/$scoreid simultaneously
  std::string key = dbref.Child("scores").PushChild().GetKey();
  LeaderBoardEntry entry = new LeaderBoardEntry(userId, score);
  std::map&ltstd::string, Variant&gt entryValues = entry.ToMap();

  std::map&ltstring, Variant&gt childUpdates = new std::map&ltstring, Variant&gt();
  childUpdates["/scores/" + key] = entryValues;
  childUpdates["/user-scores/" + userId + "/" + key] = entryValues;

  dbref.UpdateChildren(childUpdates);
}

בדוגמה הזו נעשה שימוש ב-PushChild() כדי ליצור רשומה בצומת שמכילה רשומות של כל המשתמשים ב-/scores/$key, ובמקביל לאחזר את המפתח באמצעות key(). אחר כך אפשר להשתמש במפתח כדי ליצור רשומה שנייה בניקוד של המשתמש בכתובת /user-scores/$userid/$key.

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

מחיקת נתונים

הדרך הפשוטה ביותר למחוק נתונים היא להתקשר אל RemoveValue() בהפניה למיקום של הנתונים האלה.

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

כדאי לדעת מתי הנתונים נשמרים.

כדי לדעת מתי הנתונים שלכם נשמרו בשרת Firebase Realtime Database, בודקים את התוצאה Future כדי לוודא שהפעולה בוצעה בהצלחה.

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

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

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

void AddScoreToLeaders(std::string email,
                       long score,
                       DatabaseReference leaderBoardRef) {
  leaderBoardRef.RunTransaction([](firebase::database::MutableData* mutableData) {
    if (mutableData.children_count() &gt= MaxScores) {
      long minScore = LONG_MAX;
      MutableData *minVal = null;
      std::vector&ltMutableData&gt children = mutableData.children();
      std::vector&ltMutableData&gt::iterator it;
      for (it = children.begin(); it != children.end(); ++it) {
        if (!it->value().is_map())
          continue;
        long childScore = (long)it->Child("score").value().int64_value();
        if (childScore &lt minScore) {
          minScore = childScore;
          minVal = &amp*it;
        }
      }
      if (minScore &gt score) {
        // The new score is lower than the existing 5 scores, abort.
        return kTransactionResultAbort;
      }

      // Remove the lowest score.
      children.Remove(minVal);
    }

    // Add the new high score.
    std::map&ltstd::string, Variant&gt newScoreMap =
      new std::map&ltstd::string, Variant&gt();
    newScoreMap["score"] = score;
    newScoreMap["email"] = email;
    children.Add(newScoreMap);
    mutableData->set_value(children);
    return kTransactionResultSuccess;
  });
}

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

כתיבת נתונים במצב אופליין

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

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

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

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

השלבים הבאים