Skip to content

Dexie.transaction()

David Fahlander edited this page Apr 21, 2016 · 136 revisions

Syntax

db.transaction(mode, table(s), function() {

    //
    // Transaction Scope
    //

}).then(function(result) {

    //
    // Transaction Committed
    //

}).catch(function(error) {

    //
    // Transaction Failed
    //

});

Parameters

mode
"rw"
READWRITE
"r"
READONLY
"rw!","rw?","r!" or "r?"
Specify how to behave when there already is an ongoing transaction. See Specify Reusage of Parent Transaction
table(s) Table instances or table names to include in transaction. You may either provide multiple arguments after each other, or you may provide an array of tables. Each argument or array item must be either a Table instance or a string.
callback Function to execute with the transaction. Note that since number of arguments may vary, the callback argument will always be the last argument provided to this method.

Sample

    db.transaction('rw', db.friends, db.pets, function() {

        //
        // Transaction Scope
        //

        db.friends.add({name: "New Friend"});
        db.pets.add({name: "New Pet", kind: "snake"});

    }).then(function() {

        //
        // Transaction Complete
        //

        console.log("Transaction committed");

    }).catch(function(err) {

        //
        // Transaction Failed
        //

        console.error(err);

    });

Return Value

Promise that will resolve when the transaction has committed. It will resolve with the return value of the callback (if any). If transaction fails or is aborted, the promise will reject.

Description

Start a database transaction.

When accessing the database within the given scope function, any Table-based operation will execute within the current transaction.

Transaction Scope

The Transaction Scope is the places in your code where your transaction remains active. The obvious scope is of course your callback function to the transaction() method, but the scope will also extend to every callback (such as then(), each(), toArray(), ...) originated from any database operation. Here are some samples that clarifies the scope:

    db.transaction('r', db.friends, db.pets, function() {

        // WITHIN SCOPE!

        db.friends.where('name').equals('David').first(function(friend) {

            // WITHIN SCOPE!

            db.pets.where('daddy').anyOf(friend.pets).each(function(pet) {
                
                // WITHIN SCOPE!

            });
        });

        setTimeout(function() {

            // NOT WITHIN SCOPE! (because setTimeout() is a non-Dexie async function)

        }, 0);

    }).then(function() {

        // Transaction committed. NOT WITHIN SCOPE!

    }).catch(function(err) {

        // Transaction aborted. NOT WITHIN SCOPE!

    });

If you call another function, it will also be executing in the current transaction scope:

    db.transaction('rw', db.friends, function () {
        externalFunction();
    });

    function externalFunction () {
        db.friends.add({name: "my friend"}); // WITHIN SCOPE!
    }

BUT be aware that scope is lost if using non-indexedDB compatible Promises (like standard Promise):

db.transaction('rw', db.cars, function () {
    //
    // Transaction block
    //
    db.cars.put({id: 3}).then (function() {
        // Avoid returning other kinds of promises here:
        return new window.Promise(function(resolve, reject){
            resolve();
        });
    }).then(function() {
        // You'll successfully end-up here, but any further call to db
        // will fail with "Transaction Inactive" error.
        return db.cars.get(3); // WILL FAIL WITH TRANSACTION INACTIVE!
    });

});

so make sure to only use Dexie.Promise within a transaction scope.

Accessing Transaction Object

As long as you are within the transaction scope, the Transaction object is returned using the Dexie.currentTransaction Promise-static property.

Nested Transactions

Dexie 0.9.8 and later supports nested transactions. A nested transaction must be in a compatible mode as its main transaction and all tables included in the nested transaction must also be included in its main transaction.

    db.transaction('rw', db.friends, db.pets, db.cars, function () {

        // MAIN TRANSACTION SCOPE

        db.transaction('rw', db.friends, db.cars, function () {

            // SUB TRANSACTION SCOPE

            db.transaction('r', db.friends, function () {

                // SUB- of SUB-TRANSACTION SCOPE

            });
        });
    });

The Beauty of Nested Transactions

If you write a library function that does some DB operations within a transaction and then need to reuse that function from a higher level library function, combining it with other tasks, you may gain atomicy for the entire operation. Without nested transactions, you would have to split the operations into several transactions, resulting in the risk of losing data integrity.

Sample
// Lower-level function
function birthday(friendName) {
    db.transaction('rw', db.friends, function () {
        db.friends.where('name').equals(friendName).first(function (friend) {
            ++friend.age;
            db.friends.put(friend);
        });
    });
}

// Higher-level function
function birthdays(today) {
    db.transaction('rw', db.friends, function() {
        db.friends.where('birthdate').equals(today).each(function (friend) {
            birthday(friend.name);
        });
    });
}

Note: The above samples are quite stupid, but samplifies the goodie with nested transactions. If you really wanted to do the above code simpler, you would do

function birthday (friendName) {
    db.friends
      .where('name')
      .equals(friendName)
      .modify(function(friend) {
        ++friend.age;
    });
}

...but that wouldnt visualize the beauty of nested transactions...

Creating Code With Reusable Transaction

Let's assume you have a javascript class Car with the method save().

    function Car (brand, carModel) {
        this.brand = brand;
        this.model = carModel;
    }

    Car.prototype.save = function () {
        return db.cars.put(this);
    }

In a transaction-less code block you could then do:

    var car = new Car ("Pegeut", "Van Range");
    car.save();

If you call save() from within a transaction block:

    db.transaction('rw', db.friends, db.pets, db.cars, function () {
        var car = new Car ("Pegeut", "Van Range");
        car.save();
    });

... then the save method will run the put() operation within your transaction scope. It is quite convenient not having to pass transaction instances around your code - that would easily bloat up the code and make it less reusable. It also makes it easier to switch from non-transactional to transactional code.

When you write your transaction scope, you must make sure to include all tables that will be needed by the functions you are calling. If you forget to include a table required by a function, the operation will fail and so will the transaction.

    db.transaction('rw', db.friends, function() {
        var car = new Car ("Pegeut", "Van Range");
        car.save(); 
    }).catch (function (err) {
        // Will fail with Error ("Table cars not included in parent transaction")
    });

Specify Reusage of Parent Transaction

When entering a nested transaction block, Dexie will first check that it is compatible with the currently ongoing transaction ("parent transaction"). All store names from nested must be present in parent and if nested is "rw" (READWRITE), the parent must also be that.

The default behavior is to fail with rejection of the transaction promises (both main and nested) if the two are incompatible.

If your code must be independant on any ongoing transaction, you can override this by adding "!" or "?" to the mode argument.

    db.transaction("rw!", db.Table, function () {
        // Require top-level transaction (ignore ongoing transaction)
    });

    db.transaction("rw?", db.Table, function () {
        // Use nested transaction only if compatible with ongoing, otherwise
        // launch a parallell top-level transaction for this scope
    });
!
Force Top-level Transaction. This will make your code independant on any ongoing transaction and instead always spawn a new transaction at top-level scope.
?
Reuse parent transaction only if they are compatible, otherwise launch a top-level transaction.

The "!" postfix should typically be used on a high-level API where the callers are totally unaware of how you are storing your data.

The "?" postfix can be used when your API could be used both internally or externally and you want to take advantage of transaction reusage whenever it is possible.

Sample Using The "!" Postfix

Assume you have an external "Logger" component that is independant on anything else except db and the "logentries" table. Users of the Logger component should not have to worry about how it is implemented or whether a specific table or mode must be used in an ongoing transaction. In those kind of scenarios it is recommended to use a transaction block with the "!" postfix as the following sample shows.

	//
	// "logger.js":
	//
	function Logger() {
		this.log = function (message) {
            // Use the "!" postfix to ensure we work with our own transaction and never
            // reuse any ongoing transaction.
			return db.transaction('rw!', db.logentries, function () {
				db.logentries.add({message: message, date: new Date()});
			});
		}
	}

    //
    // Application code:
    //
	var logger = new Logger();

	db.transaction('rw', db.friends, function () {
        logger.log("Now adding hillary...");
		return db.friends.add({name: "Hillary"}).then(function() {
			logger.log("Hillary was added");
		});
	}).then(function() {
        logger.log("Transaction successfully committed");
    });

Since the logger component must work independantly of whether a transaction is active or not, the logger compontent must be using the "!" postfix. Otherwise it would fail whenever called from within a transaction scope that did not include the "logentries" table.

Note: This is just a theoretical sample to explain the "!" postfix. In a real world scenario, I would rather recommend to have a dedicated Dexie instance (dedicated db) for logging purpose rather than having it as a table within your app code. In that case you wouldnt have to use the "!" postfix because only you logging component would know about the db and there would never be ongoing transactions for that db.

Implementation Details of Nested Transactions

Nested transactions has no out-of-the-box support in IndexedDB. Dexie emulates it by reusing the parent IDBTransaction within a new Dexie Transaction object with reference count of ongoing requests. The nested transaction will also block any operations made on parent transaction until nested transaction "commits". The nested transaction will "commit" when there are no more ongoing requests on it (exactly as IDB works for main transactions). The "commit" of a nested transaction only means that the tranction Promise will resolve and any pending operations on the main transaction can resume. An error occuring in the parent transaction after a "commit" will still abort the entire transaction including the nested transaction.

Parallell Transactions

At a glance, it could seem like you could only be able to run one transaction at a time. However, that is not the case. Dexie.currentTransaction is a Promise-Local static property (similar to how Thread-local storage works in threaded environments) that makes sure to always return the Transaction instance that is bound to the transaction scope that initiated the operation.

Spawning a parallell operation

Once you have entered a transaction, any database operation done in the transaction will reuse the same transaction. If you want to explicitely spawn another top-level transaction from within your current scope, you could either add the "!" postfix to the mode, or use encapsulate the database operation with Dexie.ignoreTransaction().

db.transaction('rw', db.friends, function () {
    // Use Dexie.ignoreTransaction() to launch a parallell
    // transaction from within your current transaction.
    db.transaction('r!', db.pets, function() {
        // This transaction will run in parallell because using the "!" postfix.
    }).catch(...);

    // Spawn a transaction-less operation outside our transaction:
    Dexie.ignoreTransaction(function () {
        db.pets.toArray(function (){}).catch(...); // Will launch in parallell due to Dexie.ignoreTransaction()
    });
});

Parallell Operations Within Same Transaction

Database operations are launched in parallell by default unless you wait for the previous operation to finish.

Sample:

function logCarModels() {
    return db.transaction('r', db.cars, function() {
        db.cars
          .where('brand')
          .equals('Volvo')
          .each(function (car) {
            console.log("Volvo " + car.model);
        });
    
        db.cars
          .where('brand')
          .equals('Peugeut')
          .each(function (car) {
            console.log("Peugeut " + car.model);
        });
    });
}

The above operations will run in parallell even though they run withing the same transaction. So you will get a mixture of Volvos and Peugeuts scrambled around in your console log.

To make sure that stuff happends in a sequence, you would have to write something like the following:

    // 1. Log Volvos
    db.cars
      .where('brand')
      .equals('Volvo')
      .each(function (car) {
          console.log("Volvo " + car.model);
      }).then (function() {
          // 2. Log Pegeuts
          return db.cars
            .where('brand')
            .equals('Peugeut')
            .each(function (car) {
                console.log("Peugeut " + car.model);
            });
      }).then (function (){
          // 3. Do something more...
      }).catch (function (e) {
          ...
      });

The example above shows how to run your queries in a sequence and wait for each one to finish.

Async and Await

Ecmascript 2016 (ES7) and Typescript both support async / await. When using with db.transaction(), make sure to declare let Promise = Dexie.Promise on the same level as you call db.transaction() or at any upper scope. Putting it on the top-level of your Dexie-facing module is good practice since you wont forget to declare it everywhere.

ES7 and Typescript

let Promise = Dexie.Promise; // Keep! Otherwise transaction scopes will break.
db.transaction('rw', db.friends, async function() {
    let friendId = await db.friends.add({name: "New Friend"});
    let petId = await db.pets.add({name: "New Pet", kind: "snake"});
    //...
});

Today's modern browsers

db.transaction('rw', db.friends, function*() {
    var friendId = yield db.friends.add({name: "New Friend"});
    var petId = yield db.pets.add({name: "New Pet", kind: "snake"});
    //...    
});

Read more about this in https://github.com/dfahlander/Dexie.js/wiki/Simplify-with-yield

Clone this wiki locally