Transazioni

Nota: gli sviluppatori che creano nuove applicazioni sono vivamente incoraggiati a utilizzare la libreria client NDB, che offre diversi vantaggi rispetto a questa libreria client, ad esempio la memorizzazione nella cache automatica delle entità tramite l'API Memcache. Se al momento utilizzi la libreria client DB precedente, leggi la guida alla migrazione da DB a NDB

Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni atomiche: o si verificano tutte le operazioni nella transazione o non si verifica nessuna. Un'applicazione può eseguire più operazioni e calcoli in una singola transazione.

Utilizzo delle transazioni

Una transazione è un insieme di operazioni di Datastore su una o più entità. Ogni transazione è garantita come atomica, il che significa che le transazioni non vengono mai applicate parzialmente. Vengono applicate tutte le operazioni nella transazione o nessuna di esse. Le transazioni hanno una durata massima di 60 secondi con un periodo di scadenza di inattività di 10 secondi dopo 30 secondi.

Un'operazione potrebbe non riuscire quando:

  • Sono state tentate troppe modifiche simultanee sullo stesso gruppo di entità.
  • La transazione supera un limite di risorse.
  • Datastore rileva un errore interno.

In tutti questi casi, l'API Datastore solleva un'eccezione.

Le transazioni sono una funzionalità facoltativa di Datastore. Non è obbligatorio utilizzare le transazioni per eseguire operazioni di Datastore.

Un'applicazione può eseguire un insieme di istruzioni e operazioni dello datastore in una singola transazione, in modo che se un'istruzione o un'operazione genera un'eccezione, nessuna delle operazioni dello Datastore nel set viene applicata. L'applicazione definisce le azioni da eseguire nella transazione utilizzando una funzione Python. L'applicazione avvia la transazione utilizzando uno dei metodi run_in_transaction, a seconda che la transazione acceda alle entità all'interno di un singolo gruppo di entità o se si tratta di una transazione tra gruppi.

Per il caso d'uso comune di una funzione utilizzata solo all'interno delle transazioni, utilizza il decoratore @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)

Se la funzione viene chiamata a volte senza una transazione, anziché decorarla, chiama db.run_in_transaction() con la funzione come argomento:

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() prende l'oggetto funzione e gli argomenti posizionali e con parole chiave da passare alla funzione. Se la funzione restituisce un valore, db.run_in_transaction() restituisce quel valore.

Se la funzione restituisce un valore, la transazione viene confermata e vengono applicati tutti gli effetti delle operazioni di Datastore. Se la funzione genera un'eccezione, la transazione viene "annullata" e gli effetti non vengono applicati. Consulta la nota sopra riportata sulle eccezioni.

Quando una funzione di transazione viene chiamata da un'altra transazione, @db.transactional e db.run_in_transaction() hanno un comportamento predefinito diverso. @db.transactional lo consentirà e la transazione interna diventerà la stessa transazione della transazione esterna. La chiamata a db.run_in_transaction() tenta di "nidificare" un'altra transazione all'interno della transazione esistente, ma questo comportamento non è ancora supportato e viene generato db.BadRequestError. Puoi specificare un altro comportamento. Per maggiori dettagli, consulta il riferimento alla funzione sulle opzioni di transazione.

Utilizzare le transazioni tra gruppi (XG)

Le transazioni tra gruppi, che operano su più gruppi di entità, si comportano come le transazioni a gruppo singolo, ma non falliscono se il codice tenta di aggiornare le entità di più di un gruppo di entità. Per invocare una transazione tra gruppi, utilizza le opzioni di transazione.

Utilizzo di @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()

Utilizzo di 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)

Cosa si può fare in una transazione

Datastore impone limitazioni a ciò che è possibile fare all'interno di una singola transazione.

Tutte le operazioni del datastore in una transazione devono essere eseguite su entità nello stesso gruppo di entità se la transazione è di un singolo gruppo o su entità in un massimo di venticinque gruppi di entità se la transazione è tra gruppi. Sono incluse le query per le entità in base all'antenato, il recupero delle entità in base alla chiave, l'aggiornamento e l'eliminazione delle entità. Tieni presente che ogni entità base appartiene a un gruppo di entità distinto, pertanto una singola transazione non può creare o operare su più di un&#entità base principale, a meno che non si tratti di una transazione tra gruppi.

Quando due o più transazioni tentano contemporaneamente di modificare entità in uno o più gruppi di entità comuni, solo la prima transazione che esegue il commit delle modifiche può avere esito positivo; tutte le altre non andranno a buon fine al momento del commit. A causa di questo design, l'utilizzo dei gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità dei gruppi. Quando inizia una transazione, Datastore utilizza il controllo della concorrenza ottimistico controllando l'ora dell'ultimo aggiornamento dei gruppi di entità utilizzati nella transazione. Al momento dell'commit di una transazione per i gruppi di entità, Datastore controlla di nuovo l'ora dell'ultimo aggiornamento dei gruppi di entità utilizzati nella transazione. Se è stato modificato dal controllo iniziale, viene lanciata un'eccezione.

Un'app può eseguire una query durante una transazione, ma solo se include un filtro di antenato. Un'app può anche recuperare le entità Datastore per chiave durante una transazione. Puoi preparare le chiavi prima della transazione oppure puoi crearle all'interno della transazione con nomi o ID chiave.

All'interno di una funzione di transazione è consentito qualsiasi altro codice Python. Puoi determinare se l'ambito corrente è nidificato in una funzione di transazione utilizzando db.is_in_transaction(). La funzione di transazione non deve avere effetti collaterali diversi dalle operazioni di Datastore. La funzione di transazione può essere chiamata più volte se un'operazione di Datastore non riesce a causa dell'aggiornamento da parte di un altro utente delle entità nel gruppo di entità contemporaneamente. In questo caso, l'API Datastore riprova la transazione un numero fisso di volte. Se non vanno a buon fine, db.run_in_transaction() genera un TransactionFailedError. Puoi modificare il numero di volte in cui viene riprovato l'esecuzione della transazione utilizzando db.run_in_transaction_custom_retries() instead of db.run_in_transaction().

Analogamente, la funzione di transazione non deve avere effetti collaterali che dipendono dal completamento della transazione, a meno che il codice che chiama la funzione di transazione non sappia annullare questi effetti. Ad esempio, se la transazione memorizza una nuova entità Datastore, salva l'ID dell'entità creata per un uso successivo, la transazione non va a buon fine, l'ID salvato non fa riferimento all'entità prevista perché è stato eseguito il rollback della creazione dell'entità. In questo caso, il codice di chiamata deve fare attenzione a non utilizzare l'ID salvato.

Isolamento e coerenza

Al di fuori delle transazioni, il livello di isolamento di Datastore è più simile alla lettura confermata. All'interno delle transazioni viene applicato l'isolamento serializzabile. Ciò significa che un'altra transazione non può modificare contemporaneamente i dati letti o modificati da questa transazione.

In una transazione, tutte le letture riflettono lo stato corrente e coerente di Datastore al momento dell'inizio della transazione. Le query e i get all'interno di una transazione hanno la garanzia di vedere un singolo snapshot coerente di Datastore dall'inizio della transazione. Le entità e le righe di indice nel gruppo di entità della transazione sono completamente aggiornate in modo che le query restituiscano l'insieme completo e corretto di entità di risultato, senza i falsi positivi o i falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.

Questa vista istantanea coerente si estende anche alle letture dopo le scritture all'interno delle transazioni. A differenza della maggior parte dei database, le query e i get all'interno di una transazione Datastore non vedono i risultati delle scritture precedenti all'interno della transazione. Nello specifico, se un'entità viene modificata o eliminata all'interno di una transazione, una query o un get restituisce la versione originale dell'entità dall'inizio della transazione o nessuna entità se l'entità non esisteva.

Utilizzi per le transazioni

Questo esempio mostra un utilizzo delle transazioni: l'aggiornamento di un'entità con un nuovo valore della proprietà rispetto al valore corrente.

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

È necessaria una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice ha recuperato l'oggetto, ma prima che lo salvi. Senza una transazione, la richiesta dell'utente utilizza il valore count prima dell'aggiornamento dell'altro utente e il salvataggio sovrascrive il nuovo valore. Con una transazione, l'applicazione viene informata dell'aggiornamento dell'altro utente. Se l'entità viene aggiornata durante la transazione, la transazione viene riprovata fino al completamento di tutti i passaggi senza interruzioni.

Un altro utilizzo comune delle transazioni è recuperare un'entità con una chiave denominata o crearla se non esiste ancora:

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

Come in precedenza, è necessaria una transazione per gestire il caso in cui un altro utente stia tentando di creare o aggiornare un'entità con lo stesso ID stringa. Senza una transazione, se l'entità non esiste e due utenti tentano di crearla, il secondo sovrascrive la prima senza sapere che è successo. Con una transazione, il secondo tentativo viene eseguito di nuovo, viene rilevato che l'entità ora esiste e l'entità viene aggiornata.

Quando una transazione non va a buon fine, puoi chiedere all'app di riprovare finché non va a buon fine oppure puoi lasciare che siano gli utenti a gestire l'errore trasmettendolo al livello dell'interfaccia utente dell'app. Non è necessario creare un ciclo di ripetizione per ogni transazione.

La funzionalità Get-or-create è così utile che esiste un metodo integrato per questo: Model.get_or_insert() accetta un nome chiave, un elemento padre facoltativo e gli argomenti da passare al costruttore del modello se non esiste un'entità con quel nome e quel percorso. Il tentativo di recupero e la creazione avvengono in una transazione, quindi (se la transazione va a buon fine) il metodo restituisce sempre un'istanza del modello che rappresenta un'entità effettiva.

Infine, puoi utilizzare una transazione per leggere uno snapshot coerente di Datastore. Questa opzione può essere utile quando sono necessarie più letture per eseguire il rendering di una pagina o esportare dati che devono essere coerenti. Questi tipi di transazioni sono spesso chiamate transazioni di sola lettura, poiché non eseguono scritture. Le transazioni di gruppo singolo di sola lettura non falliscono mai a causa di modifiche contemporaneamente, quindi non è necessario implementare i tentativi di nuovo in caso di errore. Tuttavia, le transazioni tra gruppi possono non riuscire a causa di modifiche contemporanee, pertanto dovrebbero essere previste delle ripetizioni. L'commit e il rollback di una transazione di sola lettura sono entrambi operazioni senza effetti.

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

Coda delle attività transazionali

Puoi mettere in coda un'attività nell'ambito di una transazione Datastore, in modo che venga messa in coda solo se la transazione viene eseguita correttamente. Se la transazione non viene eseguita, il compito non viene inserito in coda. Se la transazione viene eseguita, l'attività viene messa in coda. Una volta inserita in coda, la task non verrà eseguita immediatamente, pertanto non è atomica con la transazione. Tuttavia, una volta messa in coda, l'attività riproverà finché non andrà a buon fine. Questo vale per qualsiasi attività in coda durante una funzione run_in_transaction().

Le attività transazionali sono utili perché ti consentono di combinare azioni non di Datastore con una transazione che dipende dall'esito della transazione (ad esempio l'invio di un'email per confermare un acquisto). Puoi anche collegare le azioni di Datastore alla transazione, ad esempio per eseguire il commit delle modifiche ai gruppi di entità al di fuori della transazione se e solo se la transazione va a buon fine.

Un'applicazione non può inserire più di cinque attività di transazione nelle code di lavoro durante una singola transazione. Le attività di transazione non devono avere nomi specificati dall'utente.

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

db.run_in_transaction(do_something_in_transaction, ....)