Transacciones

Datastore admite transacciones. Una transacción es una operación (o un conjunto de operaciones) atómica: se producen todas las operaciones de la transacción o no se produce ninguna. Una aplicación puede realizar varias operaciones y cálculos en una única transacción.

Usa transacciones

Una transacción es un conjunto de operaciones de Datastore en una o más entidades. Se garantiza que toda transacción es atómica, es decir, que nunca se aplica de forma parcial. Se aplican todas las operaciones de la transacción o ninguna de ellas. Las transacciones tienen una duración máxima de 60 segundos, con un tiempo de caducidad por inactividad de 10 segundos una vez transcurridos 30 segundos.

Las operaciones pueden fallar cuando se presenta alguna de las siguientes situaciones:

  • Se intentan aplicar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Se produce un error interno en Datastore.

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

Las transacciones son una función opcional de Datastore; no es necesario que las uses para realizar operaciones de Datastore.

Este es un ejemplo de la actualización del campo llamado vacationDays en una entidad de la categoría Employee con el nombre 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 aún está activa. En el código de producción, es importante asegurarse de que todas las transacciones se confirmen o reviertan de manera explícita.

Grupos de entidad

Cada entidad pertenece a un grupo de entidades, un conjunto de una o más entidades que se pueden manipular en una sola transacción. Las relaciones de grupos de entidades le indican a App Engine que almacene varias entidades en la misma sección de la red distribuida. Una transacción configura las operaciones de Datastore para un grupo de entidades, y 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 el superior de la entidad nueva. Asignar un superior a una nueva entidad coloca a la nueva entidad en el mismo grupo de la entidad superior.

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

Cada entidad con una entidad raíz determinada como un principal se encuentra en el mismo grupo de entidades. Todas las entidades en un grupo se almacenan en el mismo nodo de Datastore. Una sola transacción puede modificar varias entidades en un solo grupo o agregar entidades nuevas al grupo si el superior de la entidad nueva se convierte en una entidad existente en el grupo. En el siguiente fragmento de código, se muestran transacciones en 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();

Crea una entidad en un grupo de entidades específico

Cuando tu aplicación construye una entidad nueva, puedes asignarla a un grupo de entidades si proporcionas la clave de otra entidad. En el siguiente ejemplo, se construye la clave de una entidad MessageBoard y, luego, se usa esa clave para crear y mantener 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();

Usa transacciones entre grupos

Las transacciones entre grupos (también conocidas como transacciones XG) operan entre varios grupos de entidades y se comportan como las transacciones de un solo grupo descritas con anterioridad, con la excepción de que no fallan si el código intenta actualizar las 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 necesitas especificar que deseas que la transacción sea entre grupos cuando comienzas la transacción, 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 en una transacción deben operar en entidades dentro del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades en un máximo de veinticinco grupos de entidades si se trata de una transacción entre grupos. Esto incluye las consultas de entidades por entidad principal, la recuperación de entidades por clave y la actualización y eliminación de entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades separado, de modo que una única transacción no puede crearse 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 entidades de forma simultánea en uno o más grupos de entidades comunes, solo la primera transacción que confirme los cambios se realizará correctamente, y la confirmación de las demás fallará. Debido a este diseño, el uso de grupos de entidades limita la cantidad de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de simultaneidad optimista mediante la verificación de la última hora de actualización de los grupos de entidades que se usan en la transacción. Luego de confirmar una transacción en los grupos de entidades, Datastore verifica una vez más la última hora de actualización de los grupos de entidades que se usan en la transacción. Si cambió desde la revisión inicial, se genera una excepción.

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

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más cercano al de las lecturas confirmadas. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que una transacción no puede modificar de forma simultánea los datos que lee o modifica otra transacción.

En una transacción, todas las operaciones de lectura reflejan el estado coherente actual de Datastore en el momento en que comenzó la transacción. Se garantiza que las consultas y las operaciones GET dentro de una transacción vean una instantánea única y coherente de Datastore desde el comienzo de la transacción. Las filas de las entidades y de los índices en el grupo de entidades de la transacción se actualizan por completo para que las consultas muestren el conjunto completo y correcto de las entidades del resultado, sin los falsos positivos ni falsos negativos que pueden generarse en las consultas realizadas fuera de las transacciones.

La vista de esta instantánea coherente también se extiende 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 GET en una transacción de Datastore no ven los resultados de las operaciones de escritura anteriores dentro de esa transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una operación GET o una consulta mostrará la versión original de la entidad según su estado al comienzo de la transacción, o no mostrará nada si la entidad no existía en ese momento.

Usos de las transacciones

En este ejemplo, se muestra uno de los usos de las transacciones: la actualización de una entidad con un valor de propiedad nuevo relativo a su valor actual. Debido a que la API de Datastore no reintenta las transacciones, podemos agregar la 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();
    }
  }
}

Esta técnica requiere una transacción porque otro usuario puede actualizar el valor luego de que este código recupere el objeto, pero guarda el objeto modificado antes. Sin una transacción, la solicitud del usuario usa el valor que tenía count antes de la actualización del otro usuario, y el valor guardado reemplaza el valor nuevo. Mediante una transacción, se le notifica a la aplicación que otro usuario realizó una actualización. Si se actualiza la entidad durante la transacción, la transacción falla con una ConcurrentModificationException. La aplicación puede repetir la transacción para usar los datos nuevos.

Las transacciones también se suelen usar para recuperar 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, se necesita una transacción para resolver el caso en el que otro usuario intenta crear o actualizar una entidad con el mismo ID de string. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo reemplazará los datos del primero sin saberlo. Con una transacción, el segundo intento falla de manera atómica. Si parece lógico hacerlo, la aplicación puede reintentar recuperar la entidad y actualizarla.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que tenga éxito, o bien puedes dejar que tus usuarios gestionen el error. Para ello, deberás propagarlo al nivel de la interfaz de usuario de la aplicación. No es necesario crear un bucle de reintento en 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 operaciones lecturas para renderizar una página o exportar datos que deben ser coherentes. Estos tipos de transacciones suelen llamarse de solo lectura, ya que no realizan operaciones de escritura. Las transacciones de solo lectura que se realizan en un solo grupo no fallan nunca debido a modificaciones simultáneas, por lo que no debes implementar reintentos en caso de fallas. Sin embargo, las transacciones que se realizan entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deben incluir reintentos. Tanto confirmar como revertir transacciones de solo lectura constituyen operaciones no-ops.

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();

Tareas transaccionales en cola

Puedes poner una tarea en cola como parte de una transacción de Datastore para que la tarea solo esté en cola (y se garantice que lo está) si la transacción se confirma de forma correcta. Si la transacción se confirma, se garantiza que la tarea se pondrá en cola. Una vez en cola, no se garantiza que la tarea se ejecutará de inmediato, y cualquier operación realizada dentro de la tarea se ejecutará independientemente de la transacción original. La tarea reintenta hasta tener éxito. Esto se aplica a cualquier tarea en cola en el contexto de una transacción.

Las tareas transaccionales son útiles porque te permiten enumerar acciones que no pertenecen a Datastore en una transacción de Datastore (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular las acciones de Datastore a la transacción, como confirmar cambios en grupos de entidades adicionales fuera de la transacción, siempre y cuando la transacción se realice de forma correcta.

Una aplicación no puede insertar más de cinco tareas transaccionales en las listas de tareas en cola durante una sola transacción. Las tareas transaccionales 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();