Transactions

Cloud Datastore prend en charge les transactions. Une transaction est une opération ou un ensemble d'opérations atomique : toutes les opérations d'une transaction se produisent, ou aucune. Une application peut effectuer plusieurs opérations et calculs au sein d'une transaction unique.

Utiliser des transactions

Une transaction est un ensemble d'opérations Cloud Datastore sur une ou plusieurs entités. Chaque transaction est atomique, ce qui signifie que les transactions ne sont jamais partiellement appliquées. Toutes les opérations de la transaction sont appliquées ou aucune d'entre elles ne l'est. Les transactions ont une durée maximale de 60 secondes avec un délai d'inactivité avant expiration de 10 secondes après 30 secondes.

Une opération peut échouer lorsque :

  • trop de modifications simultanées sont envoyées sur le même groupe d'entités ;
  • la transaction dépasse une limite de ressources ;
  • Cloud Datastore rencontre une erreur interne.

Dans tous ces cas, l'API Cloud Datastore déclenche une exception.

Les transactions sont une fonctionnalité facultative de Cloud Datastore. Vous n'êtes pas obligé de les utiliser pour effectuer des opérations Cloud Datastore.

Voici un exemple de mise à jour du champ nommé vacationDays dans une entité de type Employee nommée 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();
  }
}

Notez que, pour que nos exemples soient plus succincts, nous ignorons parfois le bloc finally qui réalise un rollback si la transaction est toujours active. Dans le code de production, il est important de s'assurer que chaque transaction est validée ou fait l'objet d'un rollback de façon explicite.

Groupes d'entités

Chaque entité appartient à un groupe d'entités, un ensemble formé d'une ou de plusieurs entités susceptibles d'être manipulées au sein d'une seule transaction. Les relations de groupes d'entités indiquent à App Engine de stocker plusieurs entités dans la même partie du réseau distribué. Une transaction définit des opérations Cloud Datastore pour un groupe d'entités et toutes les opérations sont appliquées en tant que groupe, ou pas du tout si la transaction échoue.

Lorsque l'application crée une entité, elle peut affecter une autre entité comme parent de la nouvelle entité. En cas d'affectation d'un parent à une nouvelle entité, cette dernière est insérée dans le même groupe d'entités que l'entité parente.

Une entité sans parent est une entité racine. Une entité parente d'une autre entité peut elle-même avoir un parent. Une chaîne d'entités parentes, depuis une entité jusqu'à la racine, représente le chemin d'accès de l'entité, et les membres du chemin d'accès sont les ancêtres de l'entité. Le parent d'une entité est défini lors de la création de l'entité et ne peut pas être modifié ultérieurement.

Toutes les entités ayant comme ancêtre une entité racine donnée appartiennent au même groupe d'entités. Toutes les entités d'un groupe sont stockées dans le même nœud Cloud Datastore. Par l'intermédiaire d'une même transaction, vous pouvez modifier plusieurs entités d'un même groupe, ou ajouter de nouvelles entités à un groupe en faisant du parent de la nouvelle entité une entité existante du groupe. L'extrait de code suivant illustre des transactions portant sur différents types d'entités :

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

Créer une entité dans un groupe d'entités spécifique

Lorsque votre application construit une nouvelle entité, vous pouvez attribuer cette dernière à un groupe d'entités en fournissant la clé d'une autre entité. L'exemple ci-après construit la clé d'une entité MessageBoard, puis utilise cette clé pour créer et déclarer comme persistante une entité Message appartenant au même groupe d'entités 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();

Utiliser des transactions entre groupes

Les transactions entre groupes (également appelées transactions XG) opèrent sur plusieurs groupes d'entités. Elle se comportent comme les transactions à groupe unique, mais les transactions entre groupes n'échouent pas si le code tente de mettre à jour des entités provenant de plusieurs groupes d'entités.

L'utilisation d'une transaction entre groupes est semblable à celle d'une transaction à groupe unique, sauf que vous devez spécifier que vous souhaitez une transaction entre groupes au début de la transaction, à l'aide de 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();

Que peut-on faire dans une transaction ?

Cloud Datastore impose des restrictions sur les opérations qui peuvent être effectuées au sein d'une transaction unique.

Toutes les opérations Cloud Datastore d'une transaction doivent s'effectuer sur des entités du même groupe d'entités si la transaction est une transaction à groupe unique, ou sur des entités d'un maximum de 25 groupes d'entités s'il s'agit d'une transaction entre groupes. Cela inclut l'interrogation d'entités par ancêtre, la récupération d'entités par clé, la mise à jour et la suppression d'entités. Notez que chaque entité racine appartient à un groupe d'entités distinct. Ainsi, une transaction à groupe unique ne peut pas se créer ou s'effectuer sur plusieurs entités racines à moins qu'il ne s'agisse d'une transaction entre groupes.

Lorsque deux ou plusieurs transactions tentent simultanément de modifier des entités dans un ou plusieurs groupes d'entités communs, seule la première transaction qui valide ses modifications peut aboutir. Toutes les autres échoueront lors du commit. En raison de cette conception, l'utilisation de groupes d'entités limite le nombre d'écritures simultanées qu'il est possible d'effectuer sur n'importe quelle entité des groupes. Lorsqu'une transaction démarre, Cloud Datastore utilise un contrôle de simultanéité optimiste en vérifiant la dernière heure de mise à jour pour les groupes d'entités utilisés dans la transaction. Lors du commit d'une transaction pour les groupes d'entités, Cloud Datastore vérifie à nouveau la dernière heure de mise à jour pour les groupes d'entités utilisés dans la transaction. Si elle a changé depuis la vérification initiale, une exception est levée. Pour plus d'informations sur les groupes d'entités, voir la page Présentation de Cloud Datastore.

Une application peut effectuer une requête lors d'une transaction, mais uniquement si elle inclut un filtre ancêtre. Une application peut également obtenir des entités du datastore par clé lors d'une transaction. Vous pouvez préparer les clés avant la transaction ou les générer au sein de la transaction à l'aide d'identifiants ou de noms de clés.

Isolation et cohérence

En dehors des transactions, le niveau d'isolation de Cloud Datastore est celui qui est le plus proche de la lecture. À l'intérieur des transactions, l'isolation sérialisable est appliquée. Cela signifie qu'une autre transaction ne peut pas modifier simultanément les données lues ou modifiées par cette transaction. Pour en savoir plus sur les niveaux d'isolation, consultez le wiki concernant l'isolation sérialisable et l'article sur l'isolation des transactions.

Dans une transaction, toutes les opérations de lecture reflètent l'état actuel et cohérent de Cloud Datastore au moment du démarrage de la transaction. Les requêtes et les méthodes "get" au sein d'une transaction sont certaines d'afficher un seul instantané cohérent de Cloud Datastore au début de la transaction. Les entités et les lignes d'index du groupe d'entités de la transaction sont entièrement mises à jour de façon à ce que les requêtes renvoient l'ensemble complet et correct d'entités de résultat, sans les faux positifs, ni les faux négatifs décrits dans la section Isolation de transaction pouvant survenir dans les requêtes en dehors des transactions.

Cette capture d'instantané cohérente s'étend également aux opérations de lecture ayant lieu après les opérations d'écriture au sein des transactions. Contrairement à la plupart des bases de données, les requêtes et les méthodes "get" au sein d'une transaction Cloud Datastore n'affichent pas les résultats des écritures précédentes dans cette transaction. Plus spécifiquement, si une entité est modifiée ou supprimée dans une transaction, une requête ou une méthode "get" renvoie la version originale de l'entité telle qu'elle existait au début de la transaction, ou aucun résultat si elle n'existait pas encore.

Possibilités d'utilisation des transactions

Cet exemple présente une utilisation possible des transactions : la mise à jour d'une entité avec une nouvelle valeur de propriété relative à sa valeur en cours. Étant donné que l'API Cloud Datastore ne relance pas les transactions, nous pouvons ajouter une logique pour la transaction à relancer, au cas où une autre demande met à jour le même MessageBoard ou l'un de ses Messages en même temps.

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

Ici, une transaction est nécessaire, car la valeur peut être mise à jour par un autre utilisateur après que ce code a récupéré l'objet, mais avant qu'il n'enregistre l'objet modifié. Sans transaction, la requête de l'utilisateur utilise la valeur count avant la mise à jour de l'autre utilisateur, puis la sauvegarde écrase la nouvelle valeur. Avec une transaction, l'application est prévenue de la mise à jour effectuée par l'autre utilisateur. Si l'entité est mise à jour pendant la transaction, celle-ci échoue avec une ConcurrentModificationException. L'application peut répéter la transaction afin d'utiliser les nouvelles données.

Une autre utilisation courante des transactions consiste à récupérer une entité avec une clé nommée ou de la créer si elle n'existe pas encore :

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

Comme précédemment, une transaction est nécessaire dans le cas où un autre utilisateur tente de créer ou de mettre à jour une entité avec le même ID de chaîne. Sans transaction, si l'entité n'existe pas et que deux utilisateurs tentent de la créer, la seconde remplace la première sans savoir ce qu'il s'est passé. Avec une transaction, la seconde tentative échoue atomiquement. Si besoin, l'application peut réitérer la tentative pour récupérer l'entité et la mettre à jour.

Lorsqu'une transaction échoue, vous pouvez demander à l'application de relancer la transaction jusqu'à ce qu'elle réussisse, ou laisser les utilisateurs traiter l'erreur en la propageant au niveau de l'interface utilisateur de l'application. Il n'est pas nécessaire de créer une boucle afin de relancer chaque transaction.

Enfin, vous pouvez utiliser une transaction pour lire un instantané cohérent de Cloud Datastore. Cela peut s'avérer utile lorsque plusieurs opérations de lecture sont nécessaires pour afficher une page ou exporter des données qui doivent être cohérentes. Les transactions de ce type sont souvent appelées transactions en lecture seule, car elles n'effectuent aucune opération d'écriture. Les transactions à groupe unique en lecture seule n'échouent jamais en raison de modifications simultanées. Ainsi, vous n'avez pas besoin de mettre en œuvre des tentatives en cas d'échec. Toutefois, les transactions entre groupes peuvent échouer à cause de modifications simultanées. Par conséquent, celles-ci doivent comprendre plusieurs tentatives. Le commit et le rollback d'une transaction en lecture seule sont sans effet.

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

Ajouter une tâche transactionnelle en file d'attente

Vous pouvez placer une tâche en file d'attente dans le cadre d'une transaction Cloud Datastore, de telle sorte que la tâche n'est mise en file d'attente (et garantie d'être mise en file d'attente) que si la transaction est correctement validée. La validation de la transaction garantit le placement en file d'attente de la tâche. Une fois placée en file d'attente, la tâche ne s'exécute pas nécessairement de façon immédiate, et toutes les opérations définies dans la tâche s'exécutent indépendamment de la transaction d'origine. La tâche fait l'objet de plusieurs tentatives jusqu'à ce qu'elle aboutisse. Cette règle s'applique à toutes les tâches placées en file d'attente dans le contexte d'une transaction.

Les tâches transactionnelles sont utiles car elles vous permettent d'inscrire des actions différentes de Cloud Datastore dans une transaction Cloud Datastore (comme l'envoi d'un courrier électronique pour confirmer un achat). Vous pouvez également lier des actions Cloud Datastore à la transaction, telles que la validation de modifications à des groupes d'entités supplémentaires en dehors de la transaction si et seulement si la transaction aboutit.

Une application ne peut pas insérer plus de cinq tâches transactionnelles dans des files d'attente de tâches au cours d'une même transaction. Les tâches transactionnelles ne doivent pas porter de noms spécifiés par l'utilisateur.

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();
Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java