Transacciones

Datastore admite transacciones. Una transacción es una operación o un conjunto de operaciones que es atómico: o se realizan todas las operaciones de la transacción o no se realiza ninguna. Una aplicación puede realizar varias operaciones y cálculos en una sola transacción.

Usar transacciones

Una transacción es un conjunto de operaciones de Datastore en una o varias entidades. Se garantiza que cada transacción es atómica, lo que significa que las transacciones nunca se aplican parcialmente. Se aplican todas las operaciones de la transacción o no se aplica ninguna. Las transacciones tienen una duración máxima de 60 segundos y un tiempo de caducidad por inactividad de 10 segundos después de 30 segundos.

Una operación puede fallar en los siguientes casos:

  • Se han intentado realizar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Datastore detecta un error interno.

En todos estos casos, la API Datastore genera una excepción.

Las transacciones son una función opcional de Datastore. No es obligatorio usarlas para realizar operaciones de Datastore.

A continuación se muestra un ejemplo de cómo actualizar el campo vacationDays de una entidad de tipo Employee llamada Joe:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Ten en cuenta que, para que nuestros ejemplos sean más concisos, a veces omitimos el bloque finally que realiza una reversión si la transacción sigue activa. En el código de producción, es importante asegurarse de que cada transacción se confirma o se revierte de forma explícita.

Grupos de entidades

Cada entidad pertenece a un grupo de entidades, que es un conjunto de una o varias entidades que se pueden manipular en una sola transacción. Las relaciones de grupo de entidades indican a App Engine que almacene varias entidades en la misma parte de la red distribuida. Una transacción configura operaciones de Datastore para un grupo de entidades. Todas las operaciones se aplican como un grupo o no se aplican si la transacción falla.

Cuando la aplicación crea una entidad, puede asignar otra entidad como elemento superior de la nueva entidad. Si asignas un elemento superior a una entidad nueva, esta se incluirá en el mismo grupo de entidades que la entidad superior.

Una entidad sin elemento superior es una entidad raíz. Una entidad que es superior de otra entidad también puede tener un elemento superior. Una cadena de entidades parentales de una entidad hasta la raíz es la ruta de la entidad, y los miembros de la ruta son los ancestros de la entidad. El elemento superior de una entidad se define cuando se crea la entidad y no se puede cambiar más adelante.

Todas las entidades que tienen una entidad raíz determinada como ancestro están en el mismo grupo de entidades. Todas las entidades de un grupo se almacenan en el mismo nodo de Datastore. Una sola transacción puede modificar varias entidades de un mismo grupo o añadir nuevas entidades al grupo asignando como elemento superior de la nueva entidad una entidad que ya forme parte del grupo. En el siguiente fragmento de código se muestran transacciones de varios tipos de entidades:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

Crear una entidad en un grupo de entidades específico

Cuando tu aplicación crea una entidad, puedes asignarla a un grupo de entidades proporcionando la clave de otra entidad. En el ejemplo siguiente se crea la clave de una entidad MessageBoard y, a continuación, se usa esa clave para crear y conservar una entidad Message que reside en el mismo grupo de entidades que MessageBoard:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

Usar transacciones entre grupos

Las transacciones entre grupos (también llamadas transacciones XG) operan en varios grupos de entidades y se comportan como las transacciones de un solo grupo descritas anteriormente, excepto que no fallan si el código intenta actualizar entidades de más de un grupo de entidades.

Usar una transacción entre grupos es similar a usar una transacción de un solo grupo, excepto que debes especificar que quieres que la transacción sea entre grupos cuando la inicies, mediante TransactionOptions:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

Qué se puede hacer en una transacción

Datastore impone restricciones sobre lo que se puede hacer dentro de una sola transacción.

Todas las operaciones de Datastore de una transacción deben operar en entidades del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades de un máximo de 25 grupos de entidades si la transacción es entre grupos. Esto incluye consultar entidades por ancestro, obtener entidades por clave, actualizar entidades y eliminar entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades independiente, por lo que una sola transacción no puede crear ni operar en más de una entidad raíz, a menos que sea una transacción entre grupos.

Cuando dos o más transacciones intentan modificar simultáneamente entidades en uno o más grupos de entidades comunes, solo la primera transacción que confirme sus cambios puede completarse correctamente. El resto fallarán al confirmar. Debido a este diseño, el uso de grupos de entidades limita el número de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de concurrencia optimista comprobando la hora de la última actualización de los grupos de entidades usados en la transacción. Cuando se confirma una transacción de los grupos de entidades, Datastore vuelve a comprobar la hora de la última actualización de los grupos de entidades utilizados en la transacción. Si ha cambiado desde la comprobación inicial, se produce una excepción.

Una aplicación puede realizar una consulta durante una transacción, pero solo si incluye un filtro de ancestros. Una aplicación también puede obtener entidades de Datastore por clave durante una transacción. Puedes preparar claves antes de la transacción o bien crear claves dentro de la transacción con nombres de clave o ID.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más parecido al de lectura confirmada. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que otra transacción no puede modificar simultáneamente los datos que lee o modifica esta transacción.

En una transacción, todas las lecturas reflejan el estado actual y coherente de Datastore en el momento en que se inició la transacción. Se garantiza que las consultas y las lecturas dentro de una transacción verán una única instantánea coherente de Datastore a partir del inicio de la transacción. Las entidades y las filas de índice del grupo de entidades de la transacción se actualizan por completo para que las consultas devuelvan el conjunto completo y correcto de entidades de resultados, sin los falsos positivos ni los falsos negativos que pueden producirse en las consultas fuera de las transacciones.

Esta vista de la captura coherente también se aplica a las lecturas posteriores a las escrituras dentro de las transacciones. A diferencia de la mayoría de las bases de datos, las consultas y las operaciones de obtención dentro de una transacción de Datastore no ven los resultados de las escrituras anteriores dentro de esa transacción. En concreto, si se modifica o elimina una entidad en una transacción, una consulta o una obtención devuelve la versión original de la entidad al principio de la transacción o nada si la entidad no existía entonces.

Usos de las transacciones

En este ejemplo se muestra un uso de las transacciones: actualizar una entidad con un nuevo valor de propiedad en relación con su valor actual. Como la API Datastore no vuelve a intentar las transacciones, podemos añadir lógica para que se vuelva a intentar la transacción en caso de que otra solicitud actualice el mismo MessageBoard o cualquiera de sus Messages al mismo tiempo.

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Esto requiere una transacción porque otro usuario podría actualizar el valor después de que este código obtenga el objeto, pero antes de que guarde el objeto modificado. Si no hay ninguna transacción, la solicitud del usuario usará el valor de count anterior a la actualización del otro usuario y la acción de guardar sobrescribirá el nuevo valor. Con una transacción, la aplicación recibe información sobre la actualización del otro usuario. Si la entidad se actualiza durante la transacción, esta falla y se devuelve un ConcurrentModificationException. La aplicación puede repetir la transacción para usar los nuevos datos.

Otro uso habitual de las transacciones es obtener una entidad con una clave con nombre o crearla si aún no existe:

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

Al igual que antes, es necesaria una transacción para gestionar el caso en el que otro usuario intente crear o actualizar una entidad con el mismo ID de cadena. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo sobrescribe al primero sin saber que ha ocurrido. Si se utiliza una transacción, el segundo intento falla de forma atómica. Si tiene sentido hacerlo, la aplicación puede volver a intentar obtener la entidad y actualizarla.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que se complete correctamente o puedes dejar que tus usuarios gestionen el error propagándolo al nivel de la interfaz de usuario de tu aplicación. No es necesario que crees un bucle de reintentos en torno a cada transacción.

Por último, puedes usar una transacción para leer una instantánea coherente de Datastore. Esto puede ser útil cuando se necesitan varias lecturas para renderizar una página o exportar datos que deben ser coherentes. Este tipo de transacciones se suelen denominar transacciones de solo lectura, ya que no realizan ninguna escritura. Las transacciones de un solo grupo de solo lectura nunca fallan debido a modificaciones simultáneas, por lo que no tienes que implementar reintentos en caso de fallo. Sin embargo, las transacciones entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deberían tener reintentos. Confirmar y revertir una transacción de solo lectura son operaciones nulas.

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

Puesta en cola de tareas transaccionales

Puedes poner en cola una tarea como parte de una transacción de Datastore, de forma que la tarea solo se ponga en cola (y se garantice que se ponga en cola) si la transacción se confirma correctamente. Si la transacción se confirma, la tarea se pondrá en cola. Una vez puesta en cola, no se garantiza que la tarea se ejecute inmediatamente y las operaciones que se realicen en la tarea se ejecutarán independientemente de la transacción original. Las tarea se procesa varias veces hasta que, finalmente, se lleva a cabo. Esto se aplica a cualquier tarea puesta en cola en el contexto de una transacción.

Las tareas transaccionales son útiles porque te permiten incluir acciones que no sean de Datastore en una transacción de Datastore (como enviar un correo para confirmar una compra). También puedes vincular acciones de Datastore a la transacción, como confirmar cambios en grupos de entidades adicionales fuera de la transacción, solo si la transacción se realiza correctamente.

Una aplicación no puede insertar más de cinco tareas transaccionales en colas de tareas durante una sola transacción. Las tareas de transacción no deben tener nombres especificados por el usuario.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();