Saving Data with Firebase Realtime Database for C++

Get Started

See the Get Started guide first if you have not yet set up your app and access to the database.

Get a DatabaseReference

To write data to the Database, you need an instance of DatabaseReference:

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

Saving Data

There are four methods for writing data to the Firebase Realtime Database:

Method Common uses
SetValue() Write or replace data to a defined path, such as users/<user-id>/<username>.
PushChild() Add to a list of data. Every time you call Push(), Firebase generates a unique key that can also be used as a unique identifier, such as user-scores/<user-id>/<unique-score-id>.
UpdateChildren() Update some of the keys for a defined path without replacing all of the data.
RunTransaction() Update complex data that could be corrupted by concurrent updates.

Write, update, or delete data at a reference

Basic write operations

For basic write operations, you can use SetValue() to save data to a specified reference, replacing any existing data at that path. You can use this method to pass types accepted by JSON through a Variant type which supports:

  • Null (this deletes the data)
  • Integers (64-bit)
  • Double precision floating point numbers
  • Booleans
  • Strings
  • Vectors of Variants
  • Maps of strings to Variants

Using SetValue() in this way overwrites data at the specified location, including any child nodes. However, you can still update a child without rewriting the entire object. If you want to allow users to update their profiles you could update the username as follows:

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

Append to a list of data

Use the PushChild() method to append data to a list in multiuser applications. The PushChild() method generates a unique key every time a new child is added to the specified Firebase reference. By using these auto-generated keys for each new element in the list, several clients can add children to the same location at the same time without write conflicts. The unique key generated by PushChild() is based on a timestamp, so list items are automatically ordered chronologically.

You can use the reference to the new data returned by the PushChild() method to get the value of the child's auto-generated key or set data for the child. Calling GetKey() on a PushChild() reference returns the value of the auto-generated key.

Update specific fields

To simultaneously write to specific children of a node without overwriting other child nodes, use the UpdateChildren() method.

When calling UpdateChildren(), you can update lower-level child values by specifying a path for the key. If data is stored in multiple locations to scale better, you can update all instances of that data using data fan-out. For example, a game might have a LeaderboardEntry class like this:

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

 public:
  LeaderboardEntry() {
  }

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

  std::map<std::string, Object> ToMap() {
    std::map<string, Variant> result = new std::map<string, Variant>();
    result["uid"] = Variant(uid);
    result["score"] = Variant(score);

    return result;
  }
}

To create a LeaderboardEntry and simultaneously update it to the recent score feed and the user's own score list, the game uses the following code:

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<std::string, Variant> entryValues = entry.ToMap();

  std::map<string, Variant> childUpdates = new std::map<string, Variant>();
  childUpdates["/scores/" + key] = entryValues;
  childUpdates["/user-scores/" + userId + "/" + key] = entryValues;

  dbref.UpdateChildren(childUpdates);
}

This example uses PushChild() to create an entry in the node containing entries for all users at /scores/$key and simultaneously retrieve the key with key(). The key can then be used to create a second entry in the user's scores at /user-scores/$userid/$key.

Using these paths, you can perform simultaneous updates to multiple locations in the JSON tree with a single call to UpdateChildren(), such as how this example creates the new entry in both locations. Simultaneous updates made this way are atomic: either all updates succeed or all updates fail.

Delete data

The simplest way to delete data is to call RemoveValue() on a reference to the location of that data.

You can also delete by specifying a null Variant as the value for another write operation such as SetValue() or UpdateChildren(). You can use this technique with UpdateChildren() to delete multiple children in a single API call.

Know when your data is committed.

To know when your data is committed to the Firebase Realtime Database server, check the Future result for success.

Save data as transactions

When working with data that could be corrupted by concurrent modifications, such as incremental counters, you can use a transaction operation. You give this operation a DoTransaction function. This update function takes the current state of the data as an argument and returns the new desired state you would like to write. If another client writes to the location before your new value is successfully written, your update function is called again with the new current value, and the write is retried.

For instance, in a game you could allow users to update a leaderboard with the five highest scores:

void AddScoreToLeaders(std::string email,
                       long score,
                       DatabaseReference leaderBoardRef) {
  leaderBoardRef.RunTransaction([](firebase::database::MutableData* mutableData) {
    if (mutableData.children_count() >= MaxScores) {
      long minScore = LONG_MAX;
      MutableData *minVal = null;
      std::vector<MutableData> children = mutableData.children();
      std::vector<MutableData>::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 < minScore) {
          minScore = childScore;
          minVal = &*it;
        }
      }
      if (minScore > 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<std::string, Variant> newScoreMap =
      new std::map<std::string, Variant>();
    newScoreMap["score"] = score;
    newScoreMap["email"] = email;
    children.Add(newScoreMap);
    mutableData->set_value(children);
    return kTransactionResultSuccess;
  });
}

Using a transaction prevents the leaderboard from being incorrect if multiple users record scores at the same time or the client had stale data. If the transaction is rejected, the server returns the current value to the client, which runs the transaction again with the updated value. This repeats until the transaction is accepted or too many attempts have been made.

Write data offline

If a client loses its network connection, your app will continue functioning correctly.

Every client connected to a Firebase database maintains its own internal version of any active data. When data is written, it's written to this local version first. The Firebase client then synchronizes that data with the remote database servers and with other clients on a "best-effort" basis.

As a result, all writes to the database trigger local events immediately, before any data is written to the server. This means your app remains responsive regardless of network latency or connectivity.

Once connectivity is reestablished, your app receives the appropriate set of events so that the client syncs with the current server state, without having to write any custom code.

Next Steps