Transazioni

Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni atomiche: tutte le operazioni nella transazione vengono eseguite o nessuna di loro. 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 nella transazione oppure nessuna. 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 stati tentati troppi modifiche simultanee allo stesso gruppo di entità.
  • La transazione supera un limite di risorse.
  • Datastore rileva un errore interno.

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

Le transazioni sono una funzionalità facoltativa di Datastore; non è necessario utilizzarle per eseguire operazioni Datastore.

Ecco un esempio di aggiornamento del campo denominato vacationDays in un'entità di tipo Employee denominata Joe:

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

Tieni presente che, per rendere gli esempi più concisi, a volte omettiamo il blocco finally che esegue un rollback se la transazione è ancora attiva. Nel codice di produzione è importante assicurarsi che ogni transazione venga eseguita o annullata in modo esplicito.

Gruppi di entità

Ogni entità appartiene a un gruppo di entità, un insieme di una o più entità che possono essere manipolate in una singola transazione. Le relazioni tra gruppi di entità indicano ad App Engine di archiviare diverse entità nella stessa parte della rete distribuita. Una transazione configura le operazioni Datastore per un gruppo di entità e tutte le operazioni vengono applicate come gruppo oppure non vengono applicate affatto se la transazione non va a buon fine.

Quando l'applicazione crea un'entità, può assegnarne un'altra come entità padre della nuova entità. L'assegnazione di un elemento principale a una nuova entità inserisce la nuova entità nello stesso gruppo di entità dell&#39entità padree.

Un'entità senza un elemento principale è un'entità radice. Un'entità che è un elemento principale per un'altra entità può avere anche un elemento principale. Una catena di entità padre da un'entità fino alla radice è il percorso dell'entità e i membri del percorso sono i predecessori dell'entità. L'entità padre di un'entità viene definita al momento della creazione dell'entità e non può essere modificata in un secondo momento.

Ogni entità con una determinata entità base come predecessore si trova nello stesso gruppo di entità. Tutte le entità di un gruppo vengono archiviate nello stesso nodo Datastore. Una singola transazione può modificare più entità in un singolo gruppo o aggiungere nuove entità al gruppo impostando l'entità principale della nuova entità come un'entità esistente nel gruppo. Il seguente snippet di codice mostra le transazioni su vari tipi di entità:

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

Creare un'entità in un gruppo di entità specifico

Quando l'applicazione crea una nuova entità, puoi assegnarla a un gruppo di entità fornendo la chiave di un'altra entità. L'esempio seguente crea la chiave di un'entità MessageBoard, quindi la utilizza per creare e rendere persistente un'entità Message che si trova nello stesso gruppo di entità di MessageBoard:

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

Utilizzo di transazioni cross-group

Le transazioni tra gruppi (chiamate anche transazioni XG) operano su più gruppi di entità, comportandosi come le transazioni di un singolo gruppo descritte sopra, tranne per il fatto che le transazioni tra gruppi non hanno esito negativo se il codice tenta di aggiornare le entità di più di un gruppo di entità.

L'utilizzo di una transazione cross-group è simile all'utilizzo di una transazione single-group, tranne per il fatto che devi specificare che vuoi che la transazione sia cross-group quando la inizi, utilizzando TransactionOptions:

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

Cosa si può fare in una transazione

Datastore impone limitazioni a 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 è una transazione a gruppo singolo oppure su entità in un massimo di 25 gruppi di entità se la transazione è una transazione tra gruppi. Ciò include l'interrogazione delle 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&#3entità 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 a eseguire il commit delle modifiche può avere esito positivo; tutte le altre non riusciranno a eseguire il commit. A causa di questa progettazione, l'utilizzo di gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità nei 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 del commit di una transazione per i gruppi di entità, Datastore controlla di nuovo 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 degli antenati. Un'app può anche ottenere 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 delle chiavi.

Isolamento e coerenza

Al di fuori delle transazioni, il livello di isolamento di Datastore è più vicino a read committed. 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 attuale e coerente di Datastore al momento dell'inizio della transazione. Le query e le operazioni di recupero all'interno di una transazione vedono un singolo snapshot coerente di Datastore all'inizio della transazione. Le entità e le righe dell'indice nel gruppo di entità della transazione vengono aggiornate completamente in modo che le query restituiscano l'insieme completo e corretto di entità dei risultati, senza i falsi positivi o i falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.

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

Utilizzi per le transazioni

Questo esempio mostra un utilizzo delle transazioni: l'aggiornamento di un'entità con un nuovo valore della proprietà rispetto al suo valore attuale. Poiché l'API Datastore non riprova le transazioni, possiamo aggiungere una logica per riprovare la transazione nel caso in cui un'altra richiesta aggiorni lo stesso MessageBoard o uno qualsiasi dei relativi Messages contemporaneamente.

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

Ciò richiede una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice recupera l'oggetto, ma prima che lo salvi. 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, l'applicazione viene informata dell'aggiornamento dell'altro utente. Se l'entità viene aggiornata durante la transazione, quest'ultima non va a buon fine e viene visualizzato un ConcurrentModificationException. L'applicazione può ripetere la transazione per utilizzare i nuovi dati.

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

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

Come in precedenza, è necessaria una transazione per gestire il caso in cui un altro utente tenta 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, la seconda sovrascrive la prima senza che l'utente se ne accorga. Con una transazione, il secondo tentativo non va a buon fine in modo atomico. Se ha senso farlo, l'applicazione può provare di nuovo a recuperare l'entità e aggiornarla.

Quando una transazione non va a buon fine, puoi fare in modo che la tua app riprovi la transazione finché non va a buon fine oppure puoi lasciare che i tuoi utenti gestiscano l'errore propagandolo al livello dell'interfaccia utente della tua app. Non è necessario creare un ciclo di nuovi tentativi intorno a ogni transazione.

Infine, puoi utilizzare una transazione per leggere uno snapshot coerente di Datastore. Questa opzione può essere utile quando sono necessarie più letture per visualizzare una pagina o esportare dati che devono essere coerenti. Questi tipi di transazioni sono spesso chiamati transazioni di sola lettura, poiché non eseguono scritture. Le transazioni di sola lettura con un solo gruppo non hanno mai esito negativo a causa di modifiche simultanee, quindi non devi implementare i nuovi tentativi in caso di errore. Tuttavia, le transazioni tra gruppi possono non riuscire a causa di modifiche simultanee, pertanto devono essere riprovate. Il commit e il rollback di una transazione di sola lettura sono entrambi no-op.

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

Accodamento delle attività transazionali

Puoi mettere in coda un'attività nell'ambito di una transazione Datastore, in modo che l'attività venga messa in coda, e garantita per la messa in coda, solo se la transazione viene eseguita correttamente. Se viene eseguito il commit della transazione, la garanzia di accodamento dell'attività è assicurata. Una volta accodata, l'attività non viene eseguita immediatamente e le operazioni eseguite al suo interno vengono eseguite indipendentemente dalla transazione originale. L'attività viene ritentata finché non va a buon fine. Ciò vale per qualsiasi attività accodata nel contesto di una transazione.

Le attività transazionali sono utili perché ti consentono di includere azioni non Datastore in una transazione Datastore (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à aggiuntivi 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.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();