Transaction invocation
ArangoDB transactions are different from transactions in SQL.
In SQL, transactions are started with explicit BEGIN or START TRANSACTION_command. Following any series of data retrieval or modification operations, anSQL transaction is finished with a _COMMIT command, or rolled back with aROLLBACK command. There may be client/server communication between the startand the commit/rollback of an SQL transaction.
In ArangoDB, a transaction is always a server-side operation, and is executedon the server in one go, without any client interaction. All operations to beexecuted inside a transaction need to be known by the server when the transactionis started.
There are no individual BEGIN, COMMIT or ROLLBACK transaction commandsin ArangoDB. Instead, a transaction in ArangoDB is started by providing adescription of the transaction to the db._executeTransaction JavaScriptfunction:
db._executeTransaction(description);
This function will then automatically start a transaction, execute all requireddata retrieval and/or modification operations, and at the end automaticallycommit the transaction. If an error occurs during transaction execution, thetransaction is automatically aborted, and all changes are rolled back.
Execute transaction
executes a transactiondb._executeTransaction(object)
Executes a server-side transaction, as specified by object.
object must have the following attributes:
- collections: a sub-object that defines which collections will beused in the transaction. collections can have these attributes:
- read: a single collection or a list of collections that will beused in the transaction in read-only mode
- write: a single collection or a list of collections that will beused in the transaction in write or read mode.
action: a Javascript function or a string with Javascript codecontaining all the instructions to be executed inside the transaction.If the code runs through successfully, the transaction will be committedat the end. If the code throws an exception, the transaction will berolled back and all database operations will be rolled back.Additionally, object can have the following optional attributes:
waitForSync: boolean flag indicating whether the transactionis forced to be synchronous.
- lockTimeout: a numeric value that can be used to set a timeout forwaiting on collection locks. If not specified, a default value will beused. Setting lockTimeout to 0 will make ArangoDB not timeout waiting for a lock.
- params: optional arguments passed to the function specified inaction.
Declaration of collections
All collections which are to participate in a transaction need to be declaredbeforehand. This is a necessity to ensure proper locking and isolation.
Collections can be used in a transaction in write mode or in read-only mode.
If any data modification operations are to be executed, the collection must bedeclared for use in write mode. The write mode allows modifying and reading datafrom the collection during the transaction (i.e. the write mode includes theread mode).
Contrary, using a collection in read-only mode will only allow performingread operations on a collection. Any attempt to write into a collection usedin read-only mode will make the transaction fail.
Collections for a transaction are declared by providing them in the collections_attribute of the object passed to the __executeTransaction function. Thecollections attribute has the sub-attributes read and write:
db._executeTransaction({
collections: {
write: [ "users", "logins" ],
read: [ "recommendations" ]
}
});
read and write are optional attributes, and only need to be specified ifthe operations inside the transactions demand for it.
The contents of read or write can each be lists arrays collection names or asingle collection name (as a string):
db._executeTransaction({
collections: {
write: "users",
read: "recommendations"
}
});
Note: It is currently optional to specify collections for read-only access.Even without specifying them, it is still possible to read from such collectionsfrom within a transaction, but with relaxed isolation. Please refer toTransactions Locking for more details.
In order to make a transaction fail when a non-declared collection is used insidefor reading, the optional allowImplicit sub-attribute of collections can beset to false:
db._executeTransaction({
collections: {
read: "recommendations",
allowImplicit: false /* this disallows read access to other collections
than specified */
},
action: function () {
var db = require("@arangodb").db;
return db.foobar.toArray(); /* will fail because db.foobar must not be accessed
for reading inside this transaction */
}
});
The default value for allowImplicit is true. Write-accessing collections thathave not been declared in the collections array is never possible, regardless ofthe value of allowImplicit.
Declaration of data modification and retrieval operations
All data modification and retrieval operations that are to be executed insidethe transaction need to be specified in a Javascript function, using the _action_attribute:
db._executeTransaction({
collections: {
write: "users"
},
action: function () {
// all operations go here
}
});
Any valid Javascript code is allowed inside action but the code may onlyaccess the collections declared in collections.action may be a Javascript function as shown above, or a string representationof a Javascript function:
db._executeTransaction({
collections: {
write: "users"
},
action: "function () { doSomething(); }"
});
Please note that any operations specified in action will be executed on theserver, in a separate scope. Variables will be bound late. Accessing any JavaScriptvariables defined on the client-side or in some other server context from insidea transaction may not work.Instead, any variables used inside action should be defined inside action itself:
db._executeTransaction({
collections: {
write: "users"
},
action: function () {
var db = require(...).db;
db.users.save({ ... });
}
});
When the code inside the action attribute is executed, the transaction isalready started and all required locks have been acquired. When the code insidethe action attribute finishes, the transaction will automatically commit.There is no explicit commit command.
To make a transaction abort and roll back all changes, an exception needs tobe thrown and not caught inside the transaction:
db._executeTransaction({
collections: {
write: "users"
},
action: function () {
var db = require("@arangodb").db;
db.users.save({ _key: "hello" });
// will abort and roll back the transaction
throw "doh!";
}
});
There is no explicit abort or roll back command.
As mentioned earlier, a transaction will commit automatically when the end ofthe action function is reached and no exception has been thrown. In thiscase, the user can return any legal JavaScript value from the function:
db._executeTransaction({
collections: {
write: "users"
},
action: function () {
var db = require("@arangodb").db;
db.users.save({ _key: "hello" });
// will commit the transaction and return the value "hello"
return "hello";
}
});
Custom exceptions
One may wish to define custom exceptions inside of a transaction. To have theexception propagate upwards properly, please throw an an instance of baseJavaScript Error
class or a derivative. To specify an error number, include itas the errorNumber
field. As an example:
db._executeTransaction({
collections: {},
action: function () {
var err = new Error('My error context');
err.errorNumber = 1234;
throw err;
}
});
Note: In previous versions, custom exceptions which did not have anError
-like form were simply converted to strings and exposed in theexception
field of the returned error. This is no longer the case, as it hadthe potential to leak unwanted information if improperly used.
Examples
The first example will write 3 documents into a collection named c1.The c1 collection needs to be declared in the write attribute of thecollections attribute passed to the executeTransaction function.
The action attribute contains the actual transaction code to be executed.This code contains all data modification operations (3 in this example).
// setup
db._create("c1");
db._executeTransaction({
collections: {
write: [ "c1" ]
},
action: function () {
var db = require("@arangodb").db;
db.c1.save({ _key: "key1" });
db.c1.save({ _key: "key2" });
db.c1.save({ _key: "key3" });
}
});
db.c1.count(); // 3
Aborting the transaction by throwing an exception in the action functionwill revert all changes, so as if the transaction never happened:
// setup
db._create("c1");
db._executeTransaction({
collections: {
write: [ "c1" ]
},
action: function () {
var db = require("@arangodb").db;
db.c1.save({ _key: "key1" });
db.c1.count(); // 1
db.c1.save({ _key: "key2" });
db.c1.count(); // 2
throw "doh!";
}
});
db.c1.count(); // 0
The automatic rollback is also executed when an internal exception is thrownat some point during transaction execution:
// setup
db._create("c1");
db._executeTransaction({
collections: {
write: [ "c1" ]
},
action: function () {
var db = require("@arangodb").db;
db.c1.save({ _key: "key1" });
// will throw duplicate a key error, not explicitly requested by the user
db.c1.save({ _key: "key1" });
// we'll never get here...
}
});
db.c1.count(); // 0
As required by the consistency principle, aborting or rolling back atransaction will also restore secondary indexes to the state at transactionstart.
Cross-collection transactions
There’s also the possibility to run a transaction across multiple collections.In this case, multiple collections need to be declared in the _collections_attribute, e.g.:
// setup
db._create("c1");
db._create("c2");
db._executeTransaction({
collections: {
write: [ "c1", "c2" ]
},
action: function () {
var db = require("@arangodb").db;
db.c1.save({ _key: "key1" });
db.c2.save({ _key: "key2" });
}
});
db.c1.count(); // 1
db.c2.count(); // 1
Again, throwing an exception from inside the action function will make thetransaction abort and roll back all changes in all collections:
// setup
db._create("c1");
db._create("c2");
db._executeTransaction({
collections: {
write: [ "c1", "c2" ]
},
action: function () {
var db = require("@arangodb").db;
for (var i = 0; i < 100; ++i) {
db.c1.save({ _key: "key" + i });
db.c2.save({ _key: "key" + i });
}
db.c1.count(); // 100
db.c2.count(); // 100
// abort
throw "doh!"
}
});
db.c1.count(); // 0
db.c2.count(); // 0