Transactions

Remarque : Les développeurs qui créent des applications sont vivement encouragés à utiliser la bibliothèque cliente NDB, qui présente plusieurs avantages supplémentaires par rapport à cette bibliothèque cliente, tels que la mise en cache automatique des entités via l'API Memcache. Si vous utilisez actuellement l'ancienne bibliothèque cliente DB, consultez le guide de migration de la bibliothèque cliente DB vers NDB.

Cloud Datastore accepte 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 du service Cloud Datastore. Vous n'êtes pas obligé de les utiliser pour effectuer des opérations avec ce dernier.

Une application peut exécuter un ensemble d'instructions et d'opérations de datastore au sein d'une même transaction. Ainsi, si une instruction ou une opération génère une exception, aucune des opérations Cloud Datastore de l'ensemble n'est appliquée. L'application définit les actions à effectuer dans le cadre de la transaction à l'aide d'une fonction Python. Elle démarre la transaction en utilisant l'une des méthodes run_in_transaction, selon que cette dernière accède aux entités d'un même groupe ou s'applique à plusieurs groupes.

Pour le cas d'utilisation courant d'une fonction qui n'est appliquée que dans le cadre des transactions, servez-vous du décorateur @db.transactional :

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

Si la fonction est parfois appelée sans transaction, utilisez db.run_in_transaction() en spécifiant la fonction comme argument au lieu de la décorer :

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() utilise l'objet fonction, ainsi que les arguments de position et de mot clé à transmettre à la fonction. Si cette dernière renvoie une valeur, db.run_in_transaction() la renvoie également.

Si la fonction renvoie un résultat, la transaction fait l'objet d'un commit. Tous les effets des opérations Cloud Datastore sont appliqués. Si la fonction génère une exception, la transaction fait l'objet d'un rollback. Les effets ne sont pas appliqués. Consultez la remarque ci-dessus relative aux exceptions.

Lorsqu'une fonction de transaction est appelée à partir d'une autre transaction, @db.transactional et db.run_in_transaction() ont un comportement par défaut différent. @db.transactional permet cette opération. La transaction interne devient identique à la transaction externe. Lorsque vous appelez la fonction db.run_in_transaction(), cette dernière tente d'imbriquer une autre transaction dans la transaction existante. Ce comportement n'est pas accepté pour l'instant et génère une erreur db.BadRequestError. Vous pouvez spécifier un autre comportement. Pour en savoir plus, consultez la documentation de référence sur les fonctions qui traite des options de transaction.

Utiliser des transactions entre groupes (XG)

Les transactions entre groupes, qui s'appliquent à plusieurs groupes d'entités, se comportent comme les transactions à groupe unique. Toutefois, les transactions entre groupes n'échouent pas si le code tente de mettre à jour des entités de plusieurs groupes. Pour appeler une transaction entre groupes, utilisez les options de transaction.

Exemple utilisant @db.transactional :

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

Exemple utilisant db.run_in_transaction_options :

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

Opérations pouvant être effectuées dans le cadre d'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.

Tous les autres codes Python sont autorisés au sein d'une fonction de transaction. Vous pouvez déterminer si le champ d'application actuel est imbriqué dans une fonction de transaction à l'aide de db.is_in_transaction(). La fonction de transaction ne doit pas avoir d'effets secondaires autres que les opérations Cloud Datastore. Elle peut être appelée plusieurs fois si une opération Cloud Datastore échoue en raison de la mise à jour simultanée des entités du groupe par un autre utilisateur. Dans ce cas, l'API Cloud Datastore tente à nouveau d'exécuter la transaction un nombre de fois déterminé. Si toutes les tentatives échouent, db.run_in_transaction() génère une erreur TransactionFailedError. Vous pouvez modifier le nombre d'exécutions de la transaction en utilisant db.run_in_transaction_custom_retries() au lieu de db.run_in_transaction().

De même, la fonction de transaction ne doit pas avoir d'effets secondaires qui dépendent de la réussite de la transaction, à moins que le code qui appelle la fonction puisse annuler ces effets. Imaginons, par exemple, que la transaction stocke une nouvelle entité Cloud Datastore, puis enregistre l'ID de l'entité créée en vue d'une utilisation ultérieure. Si la transaction échoue, l'ID enregistré ne fera pas référence à l'entité prévue, car la création de l'entité aura fait l'objet d'un rollback. Dans ce cas, le code d'appel ne devra pas utiliser ID enregistré.

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 un cas d'utilisation des transactions qui consiste à mettre à jour une entité avec une nouvelle valeur de propriété par rapport à sa valeur actuelle.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

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 au cours de la transaction, la transaction est alors relancée jusqu'à ce que toutes les étapes soient terminées sans interruption.

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 :

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

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é ayant le même ID de chaîne. Sans transaction, si l'entité n'existe pas et si deux utilisateurs tentent de la créer, la seconde entité créée écrase la première sans que les utilisateurs le sachent. Si une transaction est utilisée, une troisième tentative est effectuée. L'application constate que l'entité existe et la met à jour.

Lorsqu'une transaction échoue, vous pouvez demander à l'application de la relancer jusqu'à ce qu'elle aboutisse 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.

La fonction "Get-or-create" est si utile qu'une méthode intégrée lui est associée : Model.get_or_insert(). Cette dernière utilise un nom de clé, un parent facultatif et les arguments à transmettre au constructeur de modèle s'il n'existe aucune entité correspondant à ce nom et à ce chemin. Les opérations "Get" et "Create" s'effectuent dans le cadre d'une même transaction de sorte que la méthode renvoie toujours une instance de modèle représentant une entité réelle (si la transaction aboutit).

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.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

Ajouter une tâche transactionnelle en file d'attente

Vous pouvez mettre en file d'attente une tâche dans le cadre d'une transaction Cloud Datastore de sorte qu'elle n'y soit placée que si le commit de la transaction aboutit. Si le commit échoue, la tâche n'est pas mise en file d'attente. Si le commit réussit, la tâche est mise en file d'attente. Une fois placée en file d'attente, la tâche ne s'exécute pas immédiatement. La tâche et la transaction ne sont donc pas atomiques. Cependant, une fois en file d'attente, la tâche renouvelle la tentative jusqu'à ce que celle-ci réussisse. Cela s'applique à toute tâche mise en file d'attente dans le cadre d'une fonction run_in_transaction().

Les tâches transactionnelles sont utiles, car elles vous permettent de combiner dans une transaction des actions non liées à Cloud Datastore qui dépendent de la réussite de cette dernière (telles que l'envoi d'un e-mail pour la confirmation d'un achat). Vous pouvez également associer des actions Cloud Datastore à la transaction (telles que le commit des modifications apportées aux groupes d'entités en dehors de la transaction uniquement si celle-ci 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 peuvent pas avoir de nom spécifié par l'utilisateur.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)
Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Python 2