Transacciones

Cloud Datastore admite transacciones. Una transacción es una operación (o un conjunto de operaciones) atómica: se producen todas las operaciones en 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 Cloud Datastore en una o más entidades. Se garantiza que toda transacción sea atómica, es decir, que nunca se aplica 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 ocurren las siguientes situaciones:

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

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

Las transacciones son una función opcional de Cloud Datastore. No es obligatorio que las utilices para realizar operaciones de Cloud Datastore.

Este es un ejemplo de la actualización del campo llamada vacationDays en una entidad de la categoría de Employee llamada Joe:

Java 8

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

Java 7

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 veces omitimos el bloqueo de finally que hace una reversión si la transacción sigue activa para mantener nuestros ejemplos más concisos. 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 del almacén de datos en la nube para un grupo de entidades, y todas las operaciones se aplican como grupo o no se aplican 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 del almacén de datos en la nube. 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:

Java 8

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

Java 7

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 de MessageBoard y luego usa esa clave para crear y conservar una entidad de Message que reside en el mismo grupo de entidad que MessageBoard:

Java 8

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

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

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

Transaction txn = datastore.beginTransaction();

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

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

Java 8

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

Java 7

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?

Cloud Datastore impone restricciones sobre qué puede hacerse dentro de una transacción única.

Todas las operaciones de Cloud Datastore en una transacción deben operar en entidades del mismo grupo de entidad si la transacción es de un solo grupo, o en entidades de veinticinco grupos de entidad como máximo si la transacción es 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 entidad 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 entidad limita el número de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Cloud Datastore usa un control de simultaneidad optimista. Para ello, se verifica la hora de la última actualización de los grupos de entidad que se usaron en la transacción. Cuando se confirma una transacción en los grupos de entidad, Cloud Datastore vuelve a verificar la hora de la última actualización de los grupos de entidad que se usaron en la transacción. Si ha cambiado desde la revisión inicial, se genera una excepción. Para ver una explicación de los grupos de entidades, consulta la página de Descripción general de Cloud Datastore.

Una aplicación puede realizar una consulta durante una transacción, pero solo si incluye un filtro principal. Una aplicación también puede obtener entidades por clave de Datastore 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 Cloud Datastore es el más cercano a las lecturas confirmadas. Dentro de las transacciones, se aplica el aislamiento de serialización. Esto significa que una transacción no puede modificar de forma simultánea los datos que lee o modifica esta transacción. Lee el artículo de la wiki sobre el aislamiento de serialización 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 actual y coherente de Cloud Datastore al momento en el que se inició la transacción. Se garantiza que las consultas y las operaciones get dentro de una transacción visualicen una instantánea única y coherente de Cloud 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.

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 que ingresan a una transacción de Cloud Datastore no visualizan los resultados de las escrituras previas dentro de la transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una consulta o una operación get muestra la versión original de la entidad según como estaba al principio de la transacción, o no devuelve nada si la entidad no existía en ese entonces.

Usos de las transacciones

Este ejemplo demuestra que uno de los usos de las transacciones es la actualización de una entidad con un valor de propiedad nuevo relativo a su valor actual. Debido a que la API de Cloud 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.

Java 8

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

Java 7

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 utiliza el valor de count antes de la actualización de otro usuario y, cuando se guarda, se reemplaza el valor nuevo. Mediante una transacción, la aplicación se entera de la actualización del otro usuario. 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.

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

Java 8

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

Java 7

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

Como antes, la transacción es necesaria para gestionar el caso en que otro usuario intente 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á al 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 la transacción para visualizar una instantánea coherente de Cloud 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 ninguna operación 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 tener reintentos. Confirmar y revertir una transacción de solo lectura son ambas operaciones autónomas.

Java 8

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

Java 7

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 Cloud Datastore, para que la tarea solo esté en cola, y se garantice que esté en cola, si la transacción se confirma correctamente. 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 Cloud Datastore en una transacción de Cloud Datastore (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular las acciones de Cloud Datastore con la transacción, por ejemplo, confirmar los cambios en los grupos de entidad fuera de la transacción únicamente si la transacción tiene éxito.

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.

Java 8

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

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

// ...

txn.commit();

Java 7

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

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

// ...

txn.commit();
¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Java 8