Transaction

Transaction is used to achieve atomic, consistent, isolated database operation.

Transaction is performed by obtaining transaction database instance using run method.  

This example demonstrates one use of transactions: updating an entity with a new property value relative to its current value.

amount = 10;
db.run(function health_10up(tdb) { // tdb is transaction database instance  
   console.log('entering transaction');
   tdb.get('player', 1).done(function(p1_obj) {     
        print_player(p1_obj);
        p1_obj.health += amount;
        tdb.put('player', p1_obj).done(function(p1id) {       
           tdb.get('player', 1).done(function(p1_obj_2) {                
                print_player(p1_obj_2);
                console.log(tdb + '  transaction done.')
        })
        });
   });  
}, ['player'], 'readwrite');

db.get('player', 1).done(function(p1_obj_2) { 
  console.log('after transaction')
  print_player(p1_obj_2);
});
console.log('transaction requested')
This requires a transaction because the value may be updated by another user after this code fetches the object, but before it saves the modified object. Without a transaction, the user's request uses the value of health prior to the other user's update, and the save overwrites the new value. With a transaction, the application is told about the other user's update. If the entity is updated during the transaction, then the transaction is retried until all steps are completed without interruption.

If operation fail, the transaction is abort and relevant error callback of the key will be invoked. As best practice, keep transaction operation short and quick and resolve error in early possible step.

The following example illustrate modifying two records. To archive consistency, transaction is used as follow:

amount = 10;
db.run(function tx_heal(idb) {   
  var results = idb.get('player', [1, 2]);
  results.done(function(objs) {   
    print_player(objs);
    objs[0].health += amount;
    objs[1].health -= amount;
    idb.put('player', objs);
  });
}, ['player'], 'readwrite');

db.get('player', [1, 2]).done(function(objs) {
  console.log('after transaction')
  print_player(objs);
});
console.log('transaction requested.')

To use multiple stores in a transaction, names of object stores used in the transaction are declare in the transaction function input. This example also illustrate using mixing transaction and non-transaction. Notice the use of key array when we want to get records from multiple stores though single a get call.

var log_them = function(pid, sno, tdb) {
  tdb = tdb || db;
  db.getItem('current_weapon').done(function(wp) {console.log(sno + ': current_weapon: ' + wp)});
  tdb.run(function tx_log(idb) {
    idb.get('player', pid).done(function(player) {
      console.log(sno + ': player weapon: ' + player.weapon);
      idb.get('weapon', player.weapon).done(function(weapon) {
        console.log(sno + ': weapon ' + weapon.name + ' count: ' + weapon.count);
      });
    })
  }, ['player', 'weapon'], 'readonly');
  console.log(sno + ': out.');
};
var change_weapon = function (pid, new_weapon_name) {
  db.run(function tx_change(idb) {
    console.log('tx_change transaction started.');
    var get_player_data = idb.get('player', pid);
    get_player_data.done(function get_pre_data(player) {
      var old_weapon_name = player.weapon;
      db.setItem('current_weapon', old_weapon_name).done(
        function(x) {console.log('current_weapon updated to ' + old_weapon_name)});
      console.log('old_weapon_name: ' + old_weapon_name);
      log_them(pid, 3, idb);
      log_them(pid, 4);
      idb.run(function tx_readwrite(idb) {
        console.log('tx_readwrite transaction started.');
        var get_all_data = idb.get([idb.key('player', pid), idb.key('weapon', new_weapon_name), idb.key('weapon', old_weapon_name)]);
        get_all_data.done(function (data) {
          var player = data[0];
          var new_weapon = data[1];
          idb.get('weapon', player.weapon).done(function (old_weapon) {
            db.setItem('current_weapon', old_weapon.name + '->' + new_weapon.name).done(
              function(x) {console.log('current_weapon updating to ' + new_weapon.name)});
            console.log('Changing from ' + old_weapon.name + ' to ' + new_weapon.name);
            new_weapon.count--;
            old_weapon.count++;
            player.weapon = new_weapon.name;
            log_them(pid, 5, idb);
            log_them(pid, 6);
            idb.put('weapon', [new_weapon, old_weapon]);
            idb.put('player', player).done(function () {
              db.setItem('current_weapon', new_weapon.name).done(
                function(x) {console.log('current_weapon updated to ' + new_weapon.name)});
              console.log('transaction done.');
              log_them(pid, 7);
            });
          })
        });
      }, ['player', 'weapon'], 'readwrite');
    });
  }, ['player', 'weapon'], 'readonly');
  console.log('change_weapon out.')
};

In this example, to see how database perform transactions and request, progress are print out multiple checkpoints.

pid = 1;
log_them(pid, 1);
change_weapon(pid, 'sword');
log_them(pid, 2);

Many interesting fact can be learn how browser database perform transactions and handle requests from looking at the output of the script. Let us see typical output

Chrome (IndexedDB)Safari (WebSQL)
1: out.
tx_change transaction started.
change_weapon out.
2: out.
undefined
1: current_weapon: undefined
1: player weapon: gun
1: weapon gun count: 5
old_weapon_name: gun
3: out.
4: out.
2: player weapon: gun
2: weapon gun count: 5
2: current_weapon: undefined
4: player weapon: gun
4: weapon gun count: 5
3: player weapon: gun
3: weapon gun count: 5
tx_readwrite transaction started.
current_weapon updated to gun
Changing from gun to sword
5: out.
6: out.
transaction done.
7: out.
3: current_weapon: gun
6: player weapon: sword
6: weapon sword count: 9
7: player weapon: sword
7: weapon sword count: 9
5: player weapon: sword
5: weapon sword count: 9
4: current_weapon: gun
current_weapon updating to sword
5: current_weapon: gun->sword
6: current_weapon: gun->sword
current_weapon updated to sword
7: current_weapon: sword
1: out.
change_weapon out.
2: out.
undefined
1: current_weapon: undefined
1: player weapon: gun
1: weapon gun count: 5
tx_change transaction started.
old_weapon_name: gun
3: out.
4: out.
2: current_weapon: undefined
2: player weapon: gun
2: weapon gun count: 5
current_weapon updated to gun
3: current_weapon: gun
4: current_weapon: gun
4: player weapon: gun
4: weapon gun count: 5
3: player weapon: gun
3: weapon gun count: 5
tx_readwrite transaction started.
Changing from gun to sword
5: out.
6: out.
transaction done.
7: out.
current_weapon updating to sword
5: current_weapon: gun->sword
6: current_weapon: gun->sword
6: player weapon: sword
6: weapon sword count: 9
current_weapon updated to sword
7: current_weapon: sword
7: player weapon: sword
7: weapon sword count: 9
5: player weapon: sword
5: weapon sword count: 9

Remember that WebSQL do not have explicit a a mean for locking TABLE individually. Hence, object stores list on transaction input arguments is ignored by this API. Both IndexedDB and WebSQL has separate API call for readonly and readwrite transaction. Both of them use confusing but very appropriate concept called automatic committing. Although we can explicitly request a transaction, committing is not. Browser will attempt to commit automatically if not aborted and transaction is not used actively.

In this database, there are three object stores: player, weapon and default key-value store, which is used by setItem and getItem methods. Also notice the differentiation of transaction instance use by separate variables db and idb. Since default key-value store is not in the transactions, it is run under separate default transaction queue. Remember that all transaction queue are non-overlapping, i.e., next request will be executed only after the previous transaction is completed. A transaction is obtained by run function from base db instance get new transaction queue and run immediately. Transaction obtained from idb will be non-overlapping transaction from the existing queue. All database operations (get, put, etc) under a transaction use active transaction. We will discuss these transaction queue and transaction instance more detail in advance section. 

This arrangement provides very natural and robust (meaning that never you will encounter InvalidStateError) transaction workflow on most web application requirement.