Transactions
The Datastore supports transactions . A transaction is an operation or set of operations that is atomic—either all of the operations in the transaction occur, or none of them occur. An application can perform multiple operations and calculations in a single transaction.
Contents
- Using transactions
- What can be done in a transaction
- Isolation and consistency
- Uses for transactions
Using transactions
A transaction is a set of Datastore operations on one or more entities. Each transaction is guaranteed to be atomic, which means that transactions are never partially applied. Either all of the operations in the transaction are applied, or none of them are applied. Transactions have a maximum duration of 60 seconds with a 10 second idle expiration time after 30 seconds.
An operation may fail when:
- Too many concurrent modifications are attempted on the same entity group.
- The transaction exceeds a resource limit.
- The Datastore encounters an internal error.
In all these cases, the Datastore API returns an error.
Transactions are an optional feature of the Datastore; you're not required to use transactions to perform Datastore operations.
An application can execute a set of statements and Datastore operations in a single transaction, such that if any statement or operation raises an exception, none of the Datastore operations in the set are applied. The application defines the actions to perform in the transaction.
The following snippet shows how to perform a transaction using the Datastore API. It adds 10 days of vacation to an existing Employee named Joe.
Node.js (JSON)
var async = require('async');
var joePath = { kind: 'Employee', name: 'Joe' };
var tx = null;
async.waterfall([
// start new transaction
function(callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// lookup root entity.
function(result, response, callback) {
tx = result.transaction;
datastore.lookup({
datasetId: datasetId,
readOptions: { transaction: tx },
keys: [{ path: [joePath] }]
}).execute(callback);
},
// commit upsert mutation
function(result, response, callback) {
datastore.commit({
datasetId: datasetId,
// set transaction.
transaction: tx,
// set updated entity.
mutation: {
upsert: [{
key: { path: [joePath] },
properties: { vacationDays: { integerValue: 10 }}
}]
}
}).execute(callback);
}
], function(err, result) {
// rollback transaction if commit failed.
if (err && tx) {
datastore.rollback({
datasetId: datasetId,
transaction: tx
}).execute(function(err, result) {
err = err || 'transaction rolled back';
callback(err, result);
});
} else {
callback(err, result);
}
});
Python (Protocol Buffers)
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
committing = False
try:
lookup = datastore.LookupRequest()
key = lookup.key.add()
path_element = key.path_element.add()
path_element.kind = 'Employee'
path_element.name = 'Joe'
employee = self.datastore.lookup(lookup).found[0].entity
vacation_days_property = employee.property.add()
vacation_days_property.name = 'vacation_days'
vacation_days_property.value.integer_value = 10
commit = datastore.CommitRequest()
commit.transaction = txn
commit.mutation.update.extend([employee])
# Once we attempt to commit there's no way to rollback. Set a flag so that
# we don't even try it.
committing = True
self.datastore.commit(commit)
finally:
if not committing:
rollback = datastore.RollbackRequest()
rollback.transaction = txn
try:
self.datastore.rollback(rollback)
except datastore.RPCError:
pass # we did our best
Java (Protocol Buffers)
BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
boolean comitting = false;
try {
Key.Builder employeeKey = makeKey("Employee", "Joe");
ReadOptions.Builder readOpts = ReadOptions.newBuilder().setTransaction(txn.getTransaction());
LookupRequest.Builder lookup =
LookupRequest.newBuilder().addKey(employeeKey).setReadOptions(readOpts);
LookupResponse response = datastore.lookup(lookup.build());
Entity employee = response.getFound(0).getEntity();
Entity.Builder updatedEmployee = Entity.newBuilder(employee);
updatedEmployee.addProperty(makeProperty("vacationDays", makeValue(10)));
CommitRequest.Builder commit =
CommitRequest.newBuilder().setTransaction(txn.getTransaction());
commit.setMutation(Mutation.newBuilder().addUpdate(updatedEmployee));
// Once we attempt to commit there's no way to rollback. Set a flag so that we don't even try
// it.
comitting = true;
datastore.commit(commit.build());
} finally {
if (!comitting) {
RollbackRequest.Builder rollback =
RollbackRequest.newBuilder().setTransaction(txn.getTransaction());
try {
datastore.rollback(rollback.build());
} catch (DatastoreException e) {
// we did our best
}
}
}
Note that in order to keep our examples more succinct we sometimes omit the
rollback
if the transaction fails. In production code it is important to ensure that every transaction is either explicitly committed or rolled back.
Entity groups
Transactions must operate on entities that belong to a limited number (5) of entity groups . The entity group for an entity is identified by the key of the entity group's root entity (its "oldest" ancestor), so it never changes once the entity is created. Taken together, these restrictions mean that you must think carefully about how to organize your entity's keys and parent/child relationships, so that you can perform the transactions your application requires.
The following snippet shows how to create a parent and child in the same entity group and in the same transaction, in this case a message board and a message:
Node.js (JSON)
var async = require('async');
var tx = null;
async.waterfall([
// start new transaction.
function(callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// create child entity.
function(result, response, callback) {
tx = result.transaction;
var messageBoardPath = { kind: 'MessageBoard', name: 'fooBoard' };
datastore.commit({
datasetId: datasetId,
transaction: tx,
mutation: {
insertAutoId: [{
key: { path: [messageBoardPath, { kind: 'Message' }] }, // parent: [messageBoardPath]
properties: {
message_title: { stringValue: 'Welcome' },
message_body: { stringValue: 'Hello World!' },
post_date: { dateTimeValue: new Date() }
}
}]
}
}).execute(callback);
}
], callback);
Python (Protocol Buffers)
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
commit = datastore.CommitRequest()
commit.transaction = txn
message = commit.mutation.insert_auto_id.add()
path_element = message.key.path_element.add()
path_element.kind = 'MessageBoard'
path_element.name = 'board 1'
path_element = message.key.path_element.add()
path_element.kind = 'Message'
title_property = message.property.add()
title_property.name = 'message_title'
title_property.value.string_value = 'greetings!'
text_property = message.property.add()
text_property.name = 'message_text'
text_property.value.string_value = 'this is the text of the message'
date_property = message.property.add()
date_property.name = 'post_date'
date_property.value.timestamp_microseconds_value = 42
self.datastore.commit(commit)
Java (Protocol Buffers)
Key messageKey = makeKey("MessageBoard", "board 1", "Message").build();
BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
Entity.Builder message = Entity.newBuilder()
.setKey(messageKey)
.addProperty(makeProperty("message_title", makeValue("greetings!")))
.addProperty(makeProperty("message_text", makeValue("this is the text of the message")))
.addProperty(makeProperty("post_date", makeValue(new Date(42))));
CommitRequest commitRequest = CommitRequest.newBuilder()
.setTransaction(txn.getTransaction())
.setMutation(Mutation.newBuilder().addInsertAutoId(message))
.build();
messageKey = datastore.commit(commitRequest).getMutationResult().getInsertAutoIdKey(0);
A more complex transaction example
The following snippet shows how to do a few more interesting things than the previous sample. It does the following:
- Creates a new Person named Tom (a root entity) outside of a transaction.
- Sets Tom's age in a transaction.
- In a new transaction, creates a Photo for Tom (child entity).
-
Finally, in yet another transaction, it does a
lookup
on Tom and creates a new Photo, but this time one that doesn't belong to anyone (not a child entity).
Node.js (JSON)
var async = require('async');
var tomPath = { kind: 'Person', name: 'Tom' };
var tx = null;
async.waterfall([
// create root entity.
function(callback) {
datastore.commit({
datasetId: datasetId,
mutation: {
upsert: [{
key: { path: [tomPath] }
}]
},
mode: 'NON_TRANSACTIONAL'
}).execute(callback);
},
// start new transaction on root entity.
function(result, response, callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// lookup root entity.
function(result, response, callback) {
tx = result.transaction;
datastore.lookup({
datasetId: datasetId,
readOptions: { transaction: tx },
keys: [{ path: [tomPath] }]
}).execute(callback);
},
// update root entity.
function(result, response, callback) {
datastore.commit({
datasetId: datasetId,
transaction: tx,
mutation: {
update: [{
key: { path: [tomPath] },
properties: {
age: { integerValue: 40 }
}
}]
}
}).execute(callback);
},
// start new transaction on child entities.
function(result, response, callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// create child entity.
function(result, response, callback) {
tx = result.transaction;
datastore.commit({
datasetId: datasetId,
transaction: tx,
mutation: {
insertAutoId: [{
key: { path: [tomPath, { kind: 'Photo' }] }, // parent: [tomPath]
properties: {
photoUrl: { stringValue: 'http://domain.com/path/to/photo.jpg' }
}
}]
}
}).execute(callback);
},
// start a new transaction on multiple entity groups.
function(result, response, callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// lookup root entity.
function(result, response, callback) {
tx = result.transaction;
datastore.lookup({
datasetId: datasetId,
readOptions: { transaction: tx },
keys: [{ path: [tomPath] }]
}).execute(callback);
},
// create another root entity.
function(result, response, callback) {
datastore.commit({
datasetId: datasetId,
transaction: tx,
mutation: {
insertAutoId: [{
key: { path: [{ kind: 'Photo' }] }, // no parent
properties: {
photoUrl: { stringValue: 'http://domain.com/path/to/photo.jpg' }
}
}]
}
}).execute(callback);
}
], callback);
Python (Protocol Buffers)
commit = datastore.CommitRequest()
commit.mode = datastore.CommitRequest.NON_TRANSACTIONAL
person = commit.mutation.insert.add()
path_element = person.key.path_element.add()
path_element.kind = 'Person'
path_element.name = 'tom'
self.datastore.commit(commit)
# Transactions on root entities
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
lookup = datastore.LookupRequest()
lookup.key.extend([person.key])
tom = self.datastore.lookup(lookup).found[0].entity
age_property = tom.property.add()
age_property.name = 'age'
age_property.value.integer_value = 40
commit = datastore.CommitRequest()
commit.transaction = txn
commit.mutation.update.extend([tom])
self.datastore.commit(commit)
# Transactions on child entities
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
lookup = datastore.LookupRequest()
lookup.key.extend([person.key])
tom = self.datastore.lookup(lookup).found[0].entity
# Create a Photo that is a child of the Person entity named "tom"
photo = datastore.Entity()
photo.key.path_element.extend(person.key.path_element)
path_element = photo.key.path_element.add()
path_element.kind = 'Photo'
photo_url_property = photo.property.add()
photo_url_property.name = 'photo_url'
photo_url_property.value.string_value = ('http://domain.com'
'/path/to/photo.jpg')
commit = datastore.CommitRequest()
commit.transaction = txn
commit.mutation.insert_auto_id.extend([photo])
self.datastore.commit(commit)
# Transactions on entities in different entity groups
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
lookup = datastore.LookupRequest()
lookup.key.extend([person.key])
tom = self.datastore.lookup(lookup).found[0].entity
photo_not_a_child = datastore.Entity()
path_element = photo_not_a_child.key.path_element.add()
path_element.kind = 'Photo'
photo_url_property = photo_not_a_child.property.add()
photo_url_property.name = 'photo_url'
photo_url_property.value.string_value = ('http://domain.com'
'/path/to/photo.jpg')
commit = datastore.CommitRequest()
commit.transaction = txn
commit.mutation.insert_auto_id.extend([photo])
# Transaction succeeds but spans the entity group of tom and the entity
# group of the photo
self.datastore.commit(commit)
Java (Protocol Buffers)
Entity person = Entity.newBuilder().setKey(makeKey("Person", "tom")).build();
CommitRequest commitRequest = CommitRequest.newBuilder()
.setMode(CommitRequest.Mode.NON_TRANSACTIONAL)
.setMutation(Mutation.newBuilder().addInsert(person))
.build();
datastore.commit(commitRequest);
// Transactions on root entities
BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
LookupRequest.Builder lookupTomRequest = LookupRequest.newBuilder().addKey(person.getKey());
lookupTomRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
Entity tom = datastore.lookup(lookupTomRequest.build()).getFound(0).getEntity();
Entity.Builder updatedTom = Entity.newBuilder(tom);
// Add an age property to the entity
updatedTom.addProperty(makeProperty("age", makeValue(40)));
CommitRequest.Builder commit = CommitRequest.newBuilder().setTransaction(txn.getTransaction());
commit.setMutation(Mutation.newBuilder().addUpdate(updatedTom));
datastore.commit(commit.build());
// Transactions on child entities
beginTxn = BeginTransactionRequest.newBuilder();
txn = datastore.beginTransaction(beginTxn.build());
lookupTomRequest = LookupRequest.newBuilder().addKey(person.getKey());
lookupTomRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
tom = datastore.lookup(lookupTomRequest.build()).getFound(0).getEntity();
// Create a Photo that is a child of the Person entity named "tom"
Entity.Builder photo1 = Entity.newBuilder().setKey(makeKey("Person", "tom", "Photo"));
photo1.addProperty(makeProperty("photoUrl", makeValue("http://domain.com/path/to/photo.jpg")));
commit = CommitRequest.newBuilder().setTransaction(txn.getTransaction());
commit.setMutation(Mutation.newBuilder().addInsertAutoId(photo1));
Key photo1Key = datastore.commit(commit.build()).getMutationResult().getInsertAutoIdKey(0);
// Transactions on entities in different entity groups
beginTxn = BeginTransactionRequest.newBuilder();
txn = datastore.beginTransaction(beginTxn.build());
lookupTomRequest = LookupRequest.newBuilder().addKey(person.getKey());
lookupTomRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
tom = datastore.lookup(lookupTomRequest.build()).getFound(0).getEntity();
// Create a Photo that is not a child of the Person entity named "tom"
Entity.Builder photo2 = Entity.newBuilder().setKey(makeKey("Photo"));
photo2.addProperty(makeProperty("photoUrl", makeValue("http://domain.com/path/to/photo.jpg")));
commit = CommitRequest.newBuilder().setTransaction(txn.getTransaction());
commit.setMutation(Mutation.newBuilder().addInsertAutoId(photo2));
// Transaction succeeds but spans the entity group of tom and the entity group of the photo
Key photo2Key = datastore.commit(commit.build()).getMutationResult().getInsertAutoIdKey(0);
What can be done in a transaction
All Datastore operations in a transaction can operate on a maximum of five entity groups. This includes querying for entities by ancestor, retrieving entities by key, updating entities, and deleting entities.
When two or more transactions simultaneously attempt to modify entities in one or more common entity groups, only the first transaction to commit its changes can succeed; all the others will fail on commit. Because of this design, using entity groups limits the number of concurrent writes you can do on any entity in the groups. When a transaction starts, the Datastore uses optimistic concurrency control by checking the last update time for the entity groups used in the transaction. Upon commiting a transaction for the entity groups, the Datastore again checks the last update time for the entity groups used in the transaction. If it has changed since our initial check, an error is returned. For an explanation of entity groups, see the Datastore Overview page.
Isolation and consistency
Inside transactions the default isolation level is snapshot isolation , which means that another transaction may not concurrently modify the data modified by this transaction. Optionally, a transaction can be set to serializable isolation which means that another transaction cannot concurrently modify the data that is read or modified by this transaction. For more information, see Transaction Isolation .
Outside of transactions, the Datastore's isolation level is closest to read committed.
Uses for transactions
This example demonstrates one use of transactions: updating an entity with a new property value relative to its current value. The Google Cloud Datastore API does not automatically retry transactions, but you can add your own logic to retry them, for instance to handle conflicts when another request updates the same MessageBoard or any of its Messages at the same time.
Node.js (JSON)
var async = require('async');
var messageBoardPath = { kind: 'MessageBoard', name: 'fooBoard42' };
(function retry(err, count) {
var tx = null;
if (count < 0) {
callback(err, null);
return;
}
async.waterfall([
// start new transaction.
function(callback) {
datastore.beginTransaction({
datasetId: datasetId
}).execute(callback);
},
// lookup root entity.
function(result, response, callback) {
tx = result.transaction;
datastore.lookup({
datasetId: datasetId,
readOptions: { transaction: tx },
keys: [{ path: [messageBoardPath] }]
}).execute(callback);
},
// increment root entity count property value.
function(result, response, callback) {
var entity = result.found && result.found[0].entity || {
key: { path: [messageBoardPath] },
properties: { count: { integerValue: 0 } }
};
entity.properties.count.integerValue++;
datastore.commit({
datasetId: datasetId,
transaction: tx,
mutation: {
upsert: [entity]
}
}).execute(callback);
}
], function(err, result) {
// rollback transaction if commit failed.
if (err && tx) {
datastore.rollback({
datasetId: datasetId,
transaction: tx
}).execute(function(err, result) {
err = err || 'transaction rollbacked';
retry(err, count - 1);
});
} else {
callback(err, result);
}
});
})(null, 3); // Retry 3 times.
Python (Protocol Buffers)
retries = 3
while True:
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
committing = False
try:
lookup = datastore.LookupRequest()
key = lookup.key.add()
path_element = key.path_element.add()
path_element.kind = 'MessageBoard'
path_element.name = 'my message board'
message_board = self.datastore.lookup(lookup).found[0].entity
message_board.property[0].value.integer_value += 1
commit = datastore.CommitRequest()
commit.transaction = txn
commit.mutation.update.extend([message_board])
committing = True
self.datastore.commit(commit)
break
except datastore.RPCError as e:
# CONFLICT indicates contention on the entity group.
if e.response.status != httplib.CONFLICT or retries is 0:
raise e
# Allow retry to occur
retries -= 1
finally:
if not committing:
rollback = datastore.RollbackRequest()
rollback.transaction = txn
try:
self.datastore.rollback(rollback)
except datastore.RPCError:
pass # we did our best
Java (Protocol Buffers)
int retries = 3;
boolean success = false;
while (true) {
BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
try {
Key.Builder boardKey = makeKey("MessageBoard", "my message board");
LookupRequest.Builder lookupRequest = LookupRequest.newBuilder().addKey(boardKey);
lookupRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
LookupResponse response = datastore.lookup(lookupRequest.build());
Entity messageBoard = response.getFound(0).getEntity();
Entity.Builder updatedMessageBoard = Entity.newBuilder(messageBoard);
updatedMessageBoard.clearProperty();
for (Property prop : messageBoard.getPropertyList()) {
if (prop.getName().equals("count")) {
updatedMessageBoard.addProperty(
makeProperty("count", makeValue(prop.getValue().getIntegerValue() + 1)));
} else {
updatedMessageBoard.addProperty(prop);
}
}
CommitRequest.Builder commit =
CommitRequest.newBuilder().setTransaction(txn.getTransaction());
commit.setMutation(Mutation.newBuilder().addUpdate(updatedMessageBoard));
datastore.commit(commit.build());
success = true;
break;
} catch (DatastoreException e) {
// SC_CONFLICT indicates contention on the entity group.
if (e.getCode() != HttpServletResponse.SC_CONFLICT || retries == 0) {
throw e;
}
// Allow retry to occur
--retries;
} finally {
if (!success) {
RollbackRequest.Builder rollback =
RollbackRequest.newBuilder().setTransaction(txn.getTransaction());
try {
datastore.rollback(rollback.build());
} catch (DatastoreException e) {
// we did our best
}
}
}
}
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
count
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.