Dexie.transaction()
Syntax
Parameters
mode
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.
tx
Transaction instance
Sample
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.
NOTE: As of v1.4.0+, the scope function will always be executed asynchronously. In previous versions, the scope function could be executed either directly (synchronically) or asynchronously depending on whether the database was ready, or if a parent transaction was locked. See Issue #268.
Transaction Scope
The Transaction Scope is the places in your code where your transaction remains active. I'll sometimes refer to it as the Transaction Zone. 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:
If you call another function, it will also be executing in the current transaction zone:
BUT be aware that zone is lost if using non-indexedDB compatible Promises:
so make sure to only use the global Promise (window.Promise), or Dexie.Promise within a transaction zone.
The Auto-Commit Behavior of IndexedDB Transactions
IndexedDB will commit a transaction as soon as it isn't used within a tick. This means that you MUST NOT call any other async API (at least not wait for it to finish) within a transaction scope. If you do, you will get a TransactionInactiveError thrown at you. To avoid this, you may use Dexie.waitFor(), but use it with caution.
Accessing Transaction Object
As long as you are within the transaction zone, the Transaction object is returned using the Dexie.currentTransaction Promise-static property.
Nested Transactions
Dexie 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. Otherwise it will return a rejected promise and abort the parent transaction.
Limitations with Nested Transactions
Rollback support in nested transactions rely on the rollback support of the parent transaction; If a nested transaction fails, parent transaction will also fail. Normally, this is just fine and exactly what you would want to happen. But just don't expect to prohibit parent from failing by catching the nested transaction. See sample:
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 atomicity 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
Note: The above samples can be done easier using Collection.modify(), but the above sample shows the goodie with nested transactions. If you really wanted to do the above code simpler, you would do
...but that wouldn't visualize the beauty of nested transactions...
Creating Code With Reusable Transaction
Let's assume you have a javascript class Car with the method save()
.
In a transaction-less code block you could then do:
If you call save() from within a transaction block:
... 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.
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 independent on any ongoing transaction, you can override this by adding "!" or "?" to the mode
argument.
!Force Top-level Transaction. This will make your code independent 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 independent 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.
Since the logger component must work independently of whether a transaction is active or not, the logger component 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 wouldn't 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 transaction Promise will resolve and any pending operations on the main transaction can resume. An error occurring in the parent transaction after a "commit" will still abort the entire transaction including the nested transaction.
Parallel 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 parallel operation
Once you have entered a transaction, any database operation done in the transaction will reuse the same transaction. If you want to explicitly 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().
Parallel Operations Within Same Transaction
Database operations are launched in parallel by default unless you wait for the previous operation to finish.
Sample:
The above operations will run in parallel even though they run withing the same transaction. So you will get a mixture of Volvos and Peugeots scrambled around in your console log.
To make sure that stuff happens in a sequence, you would have to write something like the following:
The example above shows how to run your queries in a sequence and wait for each one to finish.
Async and Await
You can use async await without any quirks. It works perfectly well with both native async functions (tested in Edge, Chrome, Safari and Firefox) as well as transpiled async await (Typescript - any version, Babel - any version). For transpiled async await, the end code will survive indexedDB transactions no matter browser (including Internet Explorer). However, when using native async await, the browser will invoke native promises instead of Dexie.Promise. This would break transactions in older browsers. Dexie can maintain its zones (holding current transaction) between native await expressions as well as between transpiled await expressions.
The above code works with Dexie v2.0.0 in Typescript or babel with ES2016 preset. Also works in natively in Chrome, Edge, Safari, Opera and Firefox.