Google Cloud Datastore

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

  1. Using transactions
  2. What can be done in a transaction
  3. Isolation and consistency
  4. 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:

  1. Creates a new Person named Tom (a root entity) outside of a transaction.
  2. Sets Tom's age in a transaction.
  3. In a new transaction, creates a Photo for Tom (child entity).
  4. 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 request to be made serializable which means that another transaction cannot concurrently modify the data that is read or modified by this transaction. For more information, see Transaction Isolation.

In a transaction, all reads reflect the current, consistent state of the Datastore at the time the transaction started. Queries and lookups inside a transaction are guaranteed to see a single, consistent snapshot of the Datastore as of the beginning of the transaction. Entities and index rows in the transaction's entity group are fully updated so that queries return the complete, correct set of result entities, without the false positives or false negatives described in Transaction Isolation that can occur in queries outside of transactions.

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.

Another common use for transactions is to fetch an entity with a named key, or create it if it doesn't yet exist:

Node.js (JSON)

var async = require('async');

var messageBoardPath = { kind: 'MessageBoard', name: 'Foo' };
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: [messageBoardPath] }]
    }).execute(callback);
  },
  // create the entity if missing.
  function(result, response, callback) {
    if (result.found) {
      callback(null, result.found[0].entity);
      return;
    }
    var entity =  {
      key: { path: [messageBoardPath] },
      properties: { count: { integerValue: 0 } }
    };
    datastore.commit({
      datasetId: datasetId,
      transaction: tx,
      mutation: {
        upsert: [entity]
      }
    }).execute(function(err, result) {
      callback(err, entity);
    });
  }
], callback);

Python (Protocol Buffers)

begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction
committing = False

try:
  commit = datastore.CommitRequest()
  commit.transaction = txn

  lookup = datastore.LookupRequest()
  lookup.read_options.transaction = txn
  key = lookup.key.add()
  path_element = key.path_element.add()
  path_element.kind = 'MessageBoard'
  path_element.name = 'Foo'

  results = self.datastore.lookup(lookup)
  if len(results.missing) is 1:
    sales_account = commit.mutation.insert.add()
    sales_account.key.CopyFrom(key)
    count_property = sales_account.property.add()
    count_property.name = 'count'
    count_property.value.integer_value = 0
  else:
    sales_account = results.found[0].entity

  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)

Key boardKey = makeKey("MessageBoard", "Foo").build();
Entity messageBoard;
BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
CommitRequest.Builder commit = CommitRequest.newBuilder().setTransaction(txn.getTransaction());
LookupRequest.Builder lookupRequest = LookupRequest.newBuilder().addKey(boardKey);
lookupRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
LookupResponse response = datastore.lookup(lookupRequest.build());
if (response.getMissingCount() == 1) {
  Entity.Builder messageBoardBuilder = Entity.newBuilder().setKey(boardKey);
  messageBoardBuilder.addProperty(makeProperty("count", makeValue(0)));
  messageBoard = messageBoardBuilder.build();
  commit.setMutation(Mutation.newBuilder().addInsert(messageBoardBuilder));
} else {
  messageBoard = response.getFoundList().get(0).getEntity();
}
datastore.commit(commit.build());

As before, a transaction is necessary to handle the case where another user is attempting to create or update an entity with the same string ID. Without a transaction, if the entity does not exist and two users attempt to create it, the second overwrites the first without knowing that it happened.

When a transaction fails, you can have your app retry the transaction until it succeeds, or you can let your users deal with the error by propagating it to your app's user interface level. You do not have to create a retry loop around every transaction.

Finally, you can use a transaction to read a consistent snapshot of the Datastore. This can be useful when multiple reads are needed to render a page or export data that must be consistent. These kinds of transactions are often called read-only transactions, since they perform no writes. Read-only single-group transactions never fail due to concurrent modifications, so you don't have to implement retries upon failure. However, multi-entity-group transactions can fail due to concurrent modifications, so these should have retries. Committing and rolling back a read-only transaction are both no-ops.

Node.js (JSON)

var async = require('async');

// Fetch information about a message board and its first 10 messages.
var messageBoardPath = { kind: 'MessageBoard', name: 'fooBoard' };
var tx = null;
async.waterfall([
  // start new transaction.
  function(callback) {
    datastore.beginTransaction({
      datasetId: datasetId
    }).execute(callback);
  },
  function(result, response, callback) {
    tx = result.transaction;
    async.parallel([
      // lookup root entity.
      function(callback) {
        datastore.lookup({
          datasetId: datasetId,
          readOptions: { transaction: tx },
          keys: [{ path: [messageBoardPath] }]
        }).execute(callback);
      },
      // query child entities.
      function(callback) {
        datastore.runQuery({
          datasetId: datasetId,
          readOptions: { transaction: tx },
          query: {
            kinds: [{ name: 'Message' }],
            filter: {
              propertyFilter: {
                property: { name: '__key__' },
                operator: 'HAS_ANCESTOR',
                value: {
                  // ancestor key.
                  keyValue: { path: [messageBoardPath] }
                }
              }
            },
            limit: 10
          }
        }).execute(callback);
      }], callback);
  }], callback);

Python (Protocol Buffers)

# Fetch information about a message board and its first 10 messages.
begin = datastore.BeginTransactionRequest()
txn = self.datastore.begin_transaction(begin).transaction

lookup = datastore.LookupRequest()
lookup.read_options.transaction = txn

board_key = lookup.key.add()
path_element = board_key.path_element.add()
path_element.kind = 'MessageBoard'
path_element.name = 'my message board'

message_board = self.datastore.lookup(lookup).found[0].entity
count = message_board.property[0].value.integer_value

run_query = datastore.RunQueryRequest()
run_query.read_options.transaction = txn
query = run_query.query

# This is an ancestor query.
query.kind.add().name = 'Message'
ancestor_filter = query.filter.property_filter
ancestor_filter.property.name = '__key__'
ancestor_filter.operator = datastore.PropertyFilter.HAS_ANCESTOR
ancestor_filter.value.key_value.CopyFrom(board_key)

query.limit = 10

resp = self.datastore.run_query(run_query)
messages = resp.batch.entity_result

commit = datastore.CommitRequest()
commit.transaction = txn
self.datastore.commit(commit)

Java (Protocol Buffers)

// Display information about a message board and its first 10 messages.
Key boardKey = makeKey("MessageBoard", "my message board").build();

BeginTransactionRequest.Builder beginTxn = BeginTransactionRequest.newBuilder();
BeginTransactionResponse txn = datastore.beginTransaction(beginTxn.build());
LookupRequest.Builder lookupRequest = LookupRequest.newBuilder().addKey(boardKey);
lookupRequest.setReadOptions(ReadOptions.newBuilder().setTransaction(txn.getTransaction()));
Entity messageBoard = datastore.lookup(lookupRequest.build()).getFound(0).getEntity();
Map<String, Value> properties = getPropertyMap(messageBoard);
long count = properties.get("count").getIntegerValue();

Query.Builder query = Query.newBuilder();
query.addKindBuilder().setName("Message");

// This is an ancestor query
query.setFilter(makeFilter(
    makeFilter("__key__", PropertyFilter.Operator.HAS_ANCESTOR, makeValue(boardKey)).build()));
query.setLimit(10);
RunQueryRequest.Builder queryRequest = RunQueryRequest.newBuilder();
queryRequest.setQuery(query);
List<EntityResult> messages =
    datastore.runQuery(queryRequest.build()).getBatch().getEntityResultList();

CommitRequest.Builder commit = CommitRequest.newBuilder().setTransaction(txn.getTransaction());
datastore.commit(commit.build());

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.