Transactions NDB

Une transaction est une opération ou un ensemble d'opérations dont l'exécution est garantie atomique, ce qui signifie que les transactions ne sont jamais partiellement exécutées. Toutes les opérations de la transaction sont exécutées ou aucune d'entre elles ne l'est. Les transactions ont une durée maximale de 60 secondes avec un délai d'expiration de 10 secondes au bout de 30 secondes.

À l'aide de l'API asynchrone NDB, une application peut gérer plusieurs transactions simultanément si elles sont indépendantes. L'API synchrone offre une API simplifiée utilisant le décorateur @ndb.transactional(). La fonction décorée est exécutée dans le contexte de la transaction.

@ndb.transactional
def insert_if_absent(note_key, note):
    fetch = note_key.get()
    if fetch is None:
        note.put()
        return True
    return False
note_key = ndb.Key(Note, note_title, parent=parent)
note = Note(key=note_key, content=note_text)
inserted = insert_if_absent(note_key, note)

Si la transaction entre en conflit avec une autre, elle échoue. NDB tente plusieurs fois d'exécuter les transactions échouées. La fonction peut être appelée plusieurs fois si la transaction est à nouveau tentée. Le nombre de tentatives d'exécution d'une transaction ayant échoué est limité à 3 par défaut. Si la transaction n'aboutit toujours pas, NDB génère alors l'erreur TransactionFailedError. Vous pouvez modifier le nombre de tentatives en transmettant retries=N au décorateur transactional(). Un nombre de tentatives égal à 0 signifie que la transaction est tentée une fois, mais qu'elle ne sera pas tentée à nouveau si elle échoue. Un nombre de tentatives égal à N signifie que la transaction peut être tentée N fois +1 au total. Exemple :

@ndb.transactional(retries=1)
def insert_if_absent_2_retries(note_key, note):
    # do insert

Dans les transactions, seules les requêtes ancêtres sont autorisées. Par défaut, une transaction ne peut fonctionner qu'avec des entités du même groupe d'entités (entités dont les clés ont le même "ancêtre").

Vous pouvez spécifier des transactions entre groupes (transactions "XG" qui autorisent jusqu'à vingt-cinq groupes d'entités) en transmettant xg=True :

@ndb.transactional(xg=True)
def insert_if_absent_xg(note_key, note):
    # do insert

Les transactions entre groupes fonctionnent avec plusieurs groupes d'entités et se comportent comme des transactions avec un seul groupe, mais elles n'échouent pas si le code tente de mettre à jour les entités de plusieurs groupes d'entités.

Si la fonction génère une exception, la transaction est immédiatement annulée. Dans ce cas, NDB génère à nouveau l'exception afin que le code appelant en tienne compte. Vous pouvez forcer une transaction à échouer en silence en générant l'exception ndb.Rollback (l'appel de fonction renvoie None dans ce cas). Il n'y a pas de mécanisme pour forcer une nouvelle tentative.

Il peut y avoir une fonction que vous ne voulez pas toujours exécuter dans une transaction. Au lieu de décorer une telle fonction avec @ndb.transactional, transmettez-la en tant que fonction de rappel à ndb.transaction().

def insert_if_absent_sometimes(note_key, note):
    # do insert
inserted = ndb.transaction(lambda:
                           insert_if_absent_sometimes(note_key, note))

Pour vérifier si une transaction comporte du code en cours d'exécution, appelez la fonction in_transaction().

Vous pouvez spécifier la façon dont une fonction "transactionnelle" doit se comporter si elle est invoquée par un code déjà présent dans une transaction. Le décorateur @ndb.non_transactional indique qu'une fonction ne doit pas être exécutée dans une transaction. Si elle est appelée dans une transaction, elle s'exécute en dehors de celle-ci. Le décorateur @ndb.transactional et la fonction ndb.transaction prennent un argument de mot clé propagation. Par exemple, si une fonction doit démarrer une nouvelle transaction indépendante, décorez-la comme suit :

@ndb.transactional(propagation=ndb.TransactionOptions.INDEPENDENT)
def insert_if_absent_indep(note_key, note):
    # do insert

Les types de propagation sont répertoriés avec les autres options de contexte et options de transaction.

Le comportement d'une transaction peut se combiner à celui de la mise en cache NDB et semer la confusion. Si vous modifiez une entité dans une transaction que vous n'avez pas encore validée, le cache contextuel NDB contient la valeur modifiée alors que la valeur non modifiée se trouve toujours dans le datastore sous-jacent.

Ajouter une tâche transactionnelle à la file d'attente

Dans le cadre d'une transaction Datastore, vous pouvez placer une tâche en file d'attente à condition que la transaction ait été validée avec succès. 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 qu'elle réussisse. Cela s'applique à toute tâche mise en file d'attente pendant l'exécution d'une fonction décorée.

Les tâches transactionnelles sont utiles, car elles vous permettent de combiner dans une transaction des actions non liées à Cloud Datastore à une transaction qui dépend de la réussite de cette dernière (par exemple, envoyer un e-mail pour confirmer un achat). Vous pouvez également associer des actions Cloud Datastore à la transaction (par exemple, procéder au 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 doivent pas porter de noms spécifiés par l'utilisateur.

from google.appengine.api import taskqueue
from google.appengine.ext import ndb
@ndb.transactional
def insert_if_absent_taskq(note_key, note):
    taskqueue.add(url=flask.url_for('taskq_worker'), transactional=True)
    # do insert