Firebase: How do I update multiple resources atomically?

11,983

UPDATE

It's now possible to update multiple locations atomically. See this blog post for details.

var mergedUpdate = {};
mergedUpdate[ 'users/' + userId + '/widgets/' + widgetId ] = true;
mergedUpdate[ 'widgets/' + widgetId ] = widgetData;

var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");
ref.update(mergedUpdate);

This doesn't enforce transactional data (if value is currently X, make it Y), but this part can be moved to security rules. For example, if we want to update two counters at the same time, we could add rules as follows:

{
  "counter1": {
     ".validate": "newData.val() === (data.val()||0)+1"
  },

  "counter2"1 {
     ".validate": "newData.val() === (data.val()||0)+1"
  }
}

Now we can attempt the same multi-path update as above. If the values have changed since we last read them from the server, the attempt will fail. We can check if( error.code === 'PERMISSION_DENIED' ) { ... } to see if the failure was due to validation, and retry accordingly.

ORIGINAL POST

The only way to do this is to run a transaction on a common ancestor.

For instance, if you want to update /a/b/c and /a/x/y, you could run a transaction at /a and change both of the values.

The downside to this approach is that it can be expensive with network I/O, as all of the data inside the transaction needs to be downloaded and then sent back to the server.

A more complicated but potentially more powerful approach you might want to consider is restructuring your data so that rather than storing the actual values, you store a history of the edits. For example, if you're storing bank balance information, you could store a history of deposits and withdrawals. Then when you wanted to get the balance, you would play back the whole history and calculate the end balance.

The beauty of this approach is it lets you do atomic updates. For example, if you were transferring money from accountA to accountB, you'd just append an element to the end of the log saying "transfer from account A to accountB N dollars". Appending that single element is an atomic operation.

This is the approach we take with Firepad, our collaborative text editor.

Share:
11,983
E_the_Shy
Author by

E_the_Shy

Updated on June 05, 2022

Comments

  • E_the_Shy
    E_the_Shy about 2 years

    Firebase allows for a resource to be updated transactionally. As I understand it, the client does this buy sending requests to the server saying "If the old value is X, make the new value Y". If there is contention the server could reject multiple updates form the client until one is accepted.

    Now, what if I want to update multiple resources atomically?

    What happens if the first update is accepted, and then the client is disconnected prior to the second update being accepted. Is there any way to enclose multiple updates in an atomic transaction? If not, is there an idiomatic solution to this problem?

  • E_the_Shy
    E_the_Shy almost 11 years
    Thanks for your answer. I conceptually like storing new transactions rather than updating in place, but I think this is only workable from a performance perspective if there is a query materialization feature. Replaying all of history would be slow.
  • Andrew Lee
    Andrew Lee almost 11 years
    There isn't one (at least not yet), but we definitely see how this would be useful! For now you can materialize the query on the client and store it back to a "checkpoint" in Firebase (this is how Firepad works), or you could have a node.js or Java-based server somewhere running as a Firebase client that materializes it for you.
  • Zig Mandel
    Zig Mandel over 8 years
    this breaks when two clients simultaneously ask for the semaphore. not a real lock so it doesnt solve the concurrency issue, just reduces chances.
  • Matheus Dal'Pizzol
    Matheus Dal'Pizzol about 8 years
    HI! Thanx, but I just tried that and got an error: "Line 10: Left operand of || must be boolean." Am I missing something? I'm using Firebase 3.0...
  • Matt
    Matt about 8 years
    While this answer is correct for multi-location updates, I would strongly suggest an edit that indicates this counter example is not safe from malicious incrementing. In other words, just because you can write to the two endpoints simultaneously doesn't mean you have to. The Firebase validation you supply doesn't prevent a malicious user from continuing incrementing the counter at 'widgets/' + widgetId without adding any new data to the 'users/' + userId + '/widgets/' + widgetId location unless I am mistaken. Thanks!
  • hairbo
    hairbo about 8 years
    For the record, that blog post referenced at the top of this answer appears to have the wrong examples in it, at least as of this comment (the only comment on that blog post makes mention of the same thing).