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 sea atómica, es decir, que nunca se aplique parcialmente. 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 muestra una excepción.

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

A continuación, se muestra un ejemplo de un campo de actualización llamado vacationDays en una entidad de la categoría 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, a fin de que nuestros ejemplos sean más breves, 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 grupo de entidades le dicen 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 entidad y todas las operaciones se aplican como un grupo o no se realizan si la transacción falla.

Cuando la aplicación crea una entidad, puede asignar otra entidad como superior de la nueva entidad. 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 superiores 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 está 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 en un solo grupo o agregar entidades nuevas al grupo haciendo que el superior de la entidad nueva sea una entidad existente en el grupo. El siguiente fragmento de código muestra 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 entidad específico

Cuando tu aplicación construye una entidad nueva, puedes asignarla a un grupo de entidad proporcionando la clave de otra entidad. El siguiente ejemplo construye la clave de una entidad MessageBoard y, luego, usa esa clave para crear y mantener una entidad Message que reside en el mismo grupo de entidad que el 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 entidad y se comportan como transacciones de un solo grupo, pero 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 grupo único, excepto que necesitas especificar 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 entidad si la transacción es de un solo grupo o en entidades en un máximo de veinticinco grupos de entidad 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. Para ello, verifica la hora de la última actualización de los grupos de entidad que se usan en la transacción. Luego de confirmar una transacción para los grupos de entidad, Datastore verifica otra vez la hora de la última actualización de los grupos de entidad que se usan en la transacción. Si cambió desde la revisión inicial, se genera una excepción. Para obtener una explicación de los grupos de entidad, consulta la página Descripción general de Datastore.

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. Lee la wiki sobre el aislamiento serializable y el artículo sobre el aislamiento de transacciones para obtener más información sobre los niveles de aislamiento.

En una transacción, todas las lecturas 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 al comienzo de la transacción. Las filas de las entidades y los índices de los grupos de entidades de la transacción se actualizan de forma completa para que las consultas muestren el conjunto completo y correcto de las entidades del resultado, sin los falsos positivos o falsos negativos que se describen en el artículo sobre el aislamiento de transacciones y 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 operaciones get en una transacción de Datastore no ven los resultados de las escrituras anteriores dentro de esa transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una consulta u operación get mostrará la versión original de la entidad según su estado al comienzo de la transacción, o no devolverá nada si la entidad no existía entonces.

Usos de las transacciones

En este ejemplo se muestra un uso 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, la app se entera de la actualización del otro usuario. Si se actualiza la entidad durante la transacción, la transacción falla con un ConcurrentModificationException. La aplicación puede repetir la transacción para usar los datos nuevos.

Otro uso común de las transacciones es 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 lecturas para procesar 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. Confirmar y revertir una transacción de solo lectura son ambas operaciones autónomas.

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 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 son de Datastore en una transacción de Datastore (como enviar un correo electrónico 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, siempre y cuando la transacción se realice correctamente.

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