26

When I use transaction() to update a location, data at that location is returning null even though the location having some data.

I tried transaction() after reading data at the same location that time it is giving all data at that location.

How can I use transaction() if the case is like the above?

Ashok
  • 661
  • 6
  • 17

3 Answers3

32

Transactions work in the manner of Amazon's SimpleDB or a sharded cluster of databases. That is to say, they are "eventually consistent" rather than guaranteed consistent.

So when you are using transactions, the processing function may get called more than once with a local value (in some cases null if it's never been retrieved) and then again with the synced value (whatever is on the server).

Example:

pathRef.transaction(function(curValue) {

    // this part is eventually consistent and may be called several times

}, function(error, committed, ss) {

    // this part is guaranteed consistent and will match the final value set

});

This is really the mindset with which you must approach transaction anyways. You should always expect multiple calls, since the first transaction may collide with another change and be rejected. You can't use a transaction's processing method to fetch the server value (although you could read it out of the success callback).

Preventing the locally triggered event

When the transaction happens, a local event is triggered before it reaches the server for latency compensation. If the transaction fails, then the local event will be reverted (a change or remove event is triggered).

You can use the applyLocally property on transactions to override this behavior, which makes the local results slower but ensures that only the server value is triggered locally.

pathRef.transaction(function(curValue) {

    // this is still called multiple times

}, function(error, committed, ss) {

    // this part is guaranteed consistent and will match the final value set

}, 
    // by providing a third argument of `true`, no local event
    // is generated with the locally cached value.
true);
Kato
  • 40,352
  • 6
  • 119
  • 149
  • Thanks, I used transaction almost 3 days after the data inserted. So you say to use transaction first we need to check that location whether any value exists or not? – Ashok May 06 '13 at 06:28
  • "Eventually persistent" has nothing to do with the length it's been on the server, just the length it has been on the client--it's not on the client until you ask for it. You do not necessarily need to check that a value exists, just accept that the function may be called multiple times and will "eventually" be called with the server value. – Kato May 06 '13 at 13:52
  • @Kato I take it then that transaction would be a bad thing for a Node server to use to decrement a user's balance? As if another node server makes a write beforehand, then I want it to write the same thing (so set would be better?) – Dominic May 17 '16 at 22:14
  • @Kato applyLocally has nothing to do with whether the transaction will pass null to the first callback or not. You should always account for null. The transaction will eventually run the update function again with the real data. – Devin Carpenter Oct 09 '17 at 20:52
  • Great point! This old post is like looking into the way-back time machine. Oh the good ole days of Firebase v0 : ) I'll update. – Kato Nov 10 '17 at 16:49
21

You need to follow this pattern:

var pinRef = firebase.database().ref('vm-pin-generator');
pinRef.transaction(function(oldPin) {
    // Check if the result is NOT NULL:
    if (oldPin != null) {
        return localPinIncrementor(oldPin);
    } else {
        // Return a value that is totally different 
        // from what is saved on the server at this address:
        return 0;
    }
}, function(error, committed, snapshot) {
    if (error) {
        console.log("error in transaction");
    } else if (!committed) {
        console.log("transaction not committed");
    } else {
        console.log("Transaction Committed");
    }
}, true);

Firebase usually returns a null value while retrieving a key for the first time but while saving it checks if the new value is similar to older value or not. If not, firebase will run the whole process again, and this time the correct value is returned by the server.

Adding a null check and returning a totally unexpected value (0 in this case) will make firebase run the cycle again.

Antoine
  • 5,504
  • 5
  • 33
  • 54
AS490
  • 283
  • 4
  • 13
  • Instead of returning 0, you can return undefined to abort the transaction. "If undefined is returned (i.e. you return with no arguments) the transaction will be aborted and the data at this location will not be modified." https://firebase.google.com/docs/reference/admin/node/admin.database.Reference#transaction – Jordan Skole Jan 08 '17 at 16:49
  • I don't know if something has changed in Firebase since this answer was written, but if I return undefined or nothing, Firebase hangs for 60 seconds and then times out. I guess the net result is the same, but it does not seem like a good thing to do. – N.J. May 25 '17 at 14:58
  • @N.J. "If undefined is returned (i.e. you return with no arguments) the transaction will be aborted and the data at this location will not be modified." https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction – Devin Carpenter Oct 07 '17 at 01:20
1

Simply showing an example implementation to elaborate on @Kato accepted answer above with a custom upsert function:

  /**
   * Transactional insert or update record
   * @param  {String} type - object type (table or index) to build lookup path
   * @param  {String} id - object ID that will be concat with path for lookup
   * @param  {Object} data - new object (or partial with just edited fields)
   * @return {Object}       new version of object
   */
  const upsert = (type, id, data) => {
    return new Promise((resolve, reject) => {
      if (!type) {
        log.error('Missing db object type')
        reject(new TypeError('Missing db object type'))
      }
      if (!id) {
        log.error('Missing db object id')
        reject(new TypeError('Missing db object id'))
      }
      if (!data) {
        log.error('Missing db data')
        reject(new TypeError('Missing db data'))
      }

      // build path to resource
      const path = `${type}/${id}`
      log.debug(`Upserting record '${path}' to database`)

      try {
        const ref = service.ref(path)
        ref.transaction(record => {
          if (record === null) {
            log.debug(`Creating new record`) // TODO: change to debug
            return data
          } else if (record) {
            log.debug(`Updating existing record`) // TODO: change to debug
            const updatedRecord = Object.assign({}, record, data)
            return updatedRecord
          } else {
            return record
          }
        }, (error, committed, snapshot) => {
          if (error) {
            log.error(`Error upserting record in database`)
            log.error(error.message)
            reject(error)
          } else if (committed) {
            log.debug(`Saved update`)
          } else {
            log.debug(`Record unchanged`)
          }

          if (snapshot) {
            resolve(snapshot.val())
          } else {
            log.info(`No snapshot found in transaction so returning original data object`)
            resolve(data)
          }
        })
      } catch (error) {
        log.error(error)
        reject(error)
      }
    })
  }
Mike S.
  • 4,806
  • 1
  • 33
  • 35