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, come la memorizzazione automatica nella cache delle entità tramite l'API Memcache. Se attualmente utilizzi la libreria client di 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: si verificano tutte o nessuna delle operazioni della transazione. Un'applicazione può eseguire più operazioni e calcoli in una singola transazione.

Utilizzo delle transazioni

Una transazione è un insieme di operazioni 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 della transazione o nessuna di queste. Le transazioni hanno una durata massima di 60 secondi con una scadenza di inattività di 10 secondi dopo 30 secondi.

Un'operazione potrebbe non riuscire quando:

  • Sono state tentate troppe modifiche simultanee nello 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 devi utilizzare le transazioni per eseguire le operazioni di Datastore.

Un'applicazione può eseguire un insieme di istruzioni e operazioni di datastore in una singola transazione; pertanto, se un'istruzione o un'operazione genera un'eccezione, non venga applicata nessuna delle operazioni Datastore nel set. 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 a entità all'interno di un singolo gruppo di entità o che si tratti di una transazione tra gruppi.

Per il caso d'uso comune di una funzione utilizzata solo nelle transazioni, utilizza il decorator @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 a volte la funzione viene chiamata senza una transazione, invece di 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 della funzione e gli argomenti posizionale e parola chiave da passare alla funzione. Se la funzione restituisce un valore, db.run_in_transaction() restituisce quel valore.

Se la funzione viene restituita, viene eseguito il commit della transazione e vengono applicati tutti gli effetti delle operazioni di Datastore. Se la funzione solleva un'eccezione, la transazione viene "rollback" e gli effetti non vengono applicati. Consulta la nota riportata sopra sulle eccezioni.

Quando una funzione di transazione viene richiamata dall'interno di un'altra transazione, @db.transactional e db.run_in_transaction() hanno un comportamento predefinito diverso. @db.transactional consentirà questa operazione, e la transazione interna diventerà la stessa 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 genera db.BadRequestError. Puoi specificare un altro comportamento. Per informazioni dettagliate, consulta la guida di riferimento alle funzioni nelle opzioni di transazione.

Utilizzo delle transazioni tra gruppi (XG)

Le transazioni tra gruppi, che operano su più gruppi di entità, si comportano come transazioni di un singolo gruppo, ma non hanno esito negativo se il codice tenta di aggiornare le entità di più gruppi di entità. Per richiamare una transazione tra gruppi, usa 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)

Operazioni consentite in una transazione

Datastore impone delle restrizioni su ciò che può essere fatto all'interno di una singola transazione.

Tutte le operazioni Datastore in una transazione devono operare su entità nello stesso gruppo di entità se la transazione è a gruppo singolo o su entità in un massimo di 25 gruppi di entità se la transazione è tra gruppi. Ciò include l'esecuzione di query sulle entità per predecessore, il recupero delle entità per chiave, l'aggiornamento delle entità e l'eliminazione delle entità. Tieni presente che ogni entità base appartiene a un gruppo di entità separato, quindi una singola transazione non può creare o operare su più di un'entità base, 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 per eseguire il commit delle modifiche può avere esito positivo; tutte le altre avranno esito negativo al momento del commit. A causa di questa progettazione, l'uso dei gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità dei gruppi. Quando viene avviata una transazione, Datastore utilizza il controllo della contemporaneità ottimistico controllando l'ora dell'ultimo aggiornamento per i gruppi di entità utilizzati nella transazione. Dopo aver eseguito il commit di una transazione per i gruppi di entità, Datastore controlla nuovamente l'ora dell'ultimo aggiornamento per i gruppi di entità utilizzati nella transazione. Se è cambiato rispetto al controllo iniziale, viene generata un'eccezione.

Un'app può eseguire una query durante una transazione, ma solo se include un filtro dei predecessori. Un'app può anche ottenere entità Datastore per chiave durante una transazione. Puoi preparare le chiavi prima della transazione oppure puoi creare chiavi all'interno della transazione con i relativi nomi o ID.

Tutto il resto del codice Python è consentito all'interno di una funzione di transazione. Puoi determinare se l'ambito attuale è 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 Datastore non riesce a causa dell'aggiornamento simultaneo di un altro utente nel gruppo di entità. In questo caso, l'API Datastore proverà nuovamente a effettuare la transazione per un numero fisso di volte. Se tutti gli errori non vanno a buon fine, db.run_in_transaction() genera un TransactionFailedError. Puoi modificare il numero di nuovi tentativi della transazione utilizzando db.run_in_transaction_custom_retries() anziché db.run_in_transaction().

Allo stesso modo, la funzione di transazione non dovrebbe avere effetti collaterali dipendenti dall'esito della transazione, a meno che il codice che chiama la funzione di transazione non sappia di annullarli. Ad esempio, se la transazione archivia una nuova entità Datastore, salva l'ID dell'entità creata per un utilizzo 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, occorre fare attenzione a non utilizzare l'ID salvato.

Isolamento e coerenza

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

In una transazione, tutte le letture riflettono lo stato attuale e coerente di Datastore al momento dell'avvio della transazione. All'inizio della transazione è garantito che le query e i contenuti in una transazione visualizzino un unico snapshot coerente di Datastore. Le entità e le righe di indice nel gruppo di entità della transazione vengono aggiornate in modo che le query restituiscano l'insieme completo e corretto di entità di risultati, senza i falsi positivi o i falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.

Questa visualizzazione snapshot coerente si estende anche alle letture dopo le scritture all'interno delle transazioni. A differenza della maggior parte dei database, le query e le entrate all'interno di una transazione Datastore non visualizzano i risultati delle scritture precedenti all'interno della transazione. In particolare, se un'entità viene modificata o eliminata all'interno di una transazione, una query o get restituisce la versione originale dell'entità all'inizio della transazione o nulla se l'entità non esisteva in quel momento.

Utilizzi delle transazioni

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

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

Questa operazione richiede una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice ha recuperato l'oggetto, ma prima di salvare l'oggetto modificato. Senza una transazione, la richiesta dell'utente utilizza il valore di count prima dell'aggiornamento dell'altro utente e il salvataggio sovrascrive il nuovo valore. Con una transazione, all'applicazione viene comunicato l'aggiornamento dell'altro utente. Se l'entità viene aggiornata durante la transazione, la transazione viene ritentata fino al completamento di tutti i passaggi senza interruzioni.

Un altro uso 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 riprova, nota che l'entità ora esiste e aggiorna l'entità.

Quando una transazione non va a buon fine, puoi chiedere all'app di riprovare la transazione fino a quando non riesce oppure puoi consentire agli utenti di gestire l'errore propagandosi a livello di interfaccia utente dell'app. Non è necessario creare un ciclo di nuovi tentativi intorno a ogni transazione.

Il comando get-or-create è talmente utile che esiste un metodo integrato: Model.get_or_insert() richiede un nome di chiave, un elemento padre facoltativo e argomenti da passare al costruttore del modello se non esiste un'entità di quel nome e di quel percorso. Il tentativo di recupero e la creazione avvengono in una transazione, quindi (se la transazione ha esito positivo) 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. Ciò può essere utile quando sono necessarie più letture per visualizzare una pagina o esportare dati che devono essere coerenti. Questi tipi di transazioni vengono spesso chiamate transazioni di sola lettura, poiché non eseguono operazioni di scrittura. Le transazioni in un singolo gruppo di sola lettura non vanno mai a buon fine a causa di modifiche contemporanee, quindi non è necessario implementare nuovi tentativi in caso di errore. Tuttavia, le transazioni tra gruppi possono non riuscire a causa di modifiche contemporanee, quindi dovrebbero essere previsti nuovi tentativi. Il commit e il rollback di una transazione di sola lettura sono entrambi autonomi.

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

Accodamento delle attività transazionali

Puoi accodare un'attività come parte di una transazione Datastore, in modo che venga accodata solo se il commit della transazione è stato eseguito correttamente. Se il commit della transazione non viene eseguito, l'attività non viene accodata. Se il commit della transazione viene eseguito, l'attività viene accodata. Una volta accodata, l'attività non verrà eseguita immediatamente, quindi non è limitata alla transazione. Tuttavia, una volta accodata, l'attività riproverà fino a quando non riesce. Questo vale per tutte le attività accodate durante una funzione run_in_transaction().

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

Un'applicazione non può inserire più di cinque attività transazionali nelle code di attività durante una singola transazione. Le attività transazionali 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, ....)