Transazioni

Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni di natura atomica, o che si verificano tutte le operazioni nella transazione o nessuna di esse. 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 è atomica, il che significa che le transazioni non vengono mai applicate parzialmente. Vengono applicate tutte le operazioni nella transazione o nessuna. 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 se:

  • Vengono tentate troppe modifiche simultanee allo stesso gruppo di entità.
  • La transazione supera il limite di risorse.
  • Datastore riscontra un errore interno.

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

Le transazioni sono una funzionalità facoltativa di Datastore; non è necessario utilizzare le transazioni per eseguire operazioni di 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 mantenere i nostri esempi più concisi, a volte omettiamo il blocco finally che esegue un rollback se la transazione è ancora attiva. Nel codice di produzione è importante garantire che ogni transazione sia stata sottoposta a commit esplicito o rollback.

Gruppi di entità

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

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

Un'entità senza un elemento padre è un'entità root. Un'entità padre di un'altra entità può anche avere un elemento padre. Una catena di entità padre da un'entità fino alla radice è il percorso dell'entità e i membri del percorso sono i antenati 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 sono archiviate nello stesso nodo Datastore. Una singola transazione può modificare più entità in un singolo gruppo o aggiungere nuove entità al gruppo trasformando l'entità padre della nuova entità un'entità esistente nel gruppo. Il seguente snippet di codice illustra 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();

Creazione di 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 mantenere un'entità Message che risiede 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();

Utilizzare le transazioni tra gruppi

Le transazioni tra gruppi (chiamate anche transazioni XG) operano in più gruppi di entità, comportandosi come transazioni per gruppi singoli descritti sopra, ma le transazioni tra gruppi non hanno esito negativo se il codice cerca di aggiornare le entità da più gruppi di entità.

L'utilizzo di una transazione tra gruppi è simile a quella di una transazione di un singolo gruppo, ad eccezione del fatto che devi specificare che la transazione deve essere cross-group all'inizio della transazione, 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 su ciò che può essere fatto all'interno di una singola transazione.

Tutte le operazioni Datastore in una transazione devono essere eseguite su entità nello stesso gruppo di entità se si tratta di una transazione di un singolo gruppo o su entità in un massimo di venticinque gruppi di entità se la transazione è una transazione tra gruppi. Ciò include l'esecuzione di query sulle entità per predecessore, il recupero di entità per chiave, l'aggiornamento delle entità ed 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 principale, a meno che non si tratti di una transazione tra gruppi.

Quando due o più transazioni tentano di modificare contemporaneamente le entità in uno o più gruppi di entità comuni, può avere esito positivo solo la prima transazione per il commit delle modifiche; tutte le altre hanno esito negativo dopo il commit. Per via di questa struttura, l'uso di 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 ottimistico della contemporaneità 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 è cambiata 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 recuperare le entità Datastore per chiave durante una transazione. Puoi preparare le chiavi prima della transazione oppure creare chiavi all'interno della transazione con ID o nomi delle chiavi.

Isolamento e coerenza

Al di fuori delle transazioni, il livello di isolamento di Datastore è il più vicino alla lettura impegnata. 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. Per le query e l'interno di una transazione è garantito che venga visualizzato un unico snapshot coerente di Datastore all'inizio della transazione. Le entità e le righe di indice del gruppo di entità della transazione vengono completamente aggiornate in modo che le query restituiscano l'insieme completo e corretto di entità di risultati, senza falsi positivi o 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 del 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 get restituisce la versione originale dell'entità all'inizio della transazione o nulla se l'entità non esisteva all'inizio della transazione.

Utilizzi per le transazioni

Questo esempio mostra un utilizzo delle transazioni: l'aggiornamento di un'entità con un nuovo valore della proprietà in relazione al suo valore attuale. Poiché l'API Datastore non riprova a eseguire le transazioni, possiamo aggiungere una logica che consenta un nuovo tentativo della transazione nel caso in cui un'altra richiesta aggiorni contemporaneamente la stessa MessageBoard o una qualsiasi delle sue Messages.

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 venga salvato l'oggetto modificato. 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, all'applicazione viene comunicato l'aggiornamento dell'altro utente. Se l'entità viene aggiornata durante la transazione, la transazione non va a buon fine con un ConcurrentModificationException. L'applicazione può ripetere la transazione per utilizzare i nuovi dati.

Un altro uso comune delle transazioni è il recupero di un'entità con una chiave denominata o creazione di un'entità 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, una transazione è necessaria 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, il secondo sovrascrive la prima senza sapere che si è verificata. Con una transazione, il secondo tentativo non va a buon fine a livello atomico. Se ha senso farlo, l'applicazione può riprovare a recuperare l'entità e aggiornarla.

Se una transazione non va a buon fine, puoi fare in modo che l'app esegua un nuovo tentativo finché non va a buon fine oppure puoi consentire agli utenti di gestire l'errore propagandolo al livello dell'interfaccia utente della tua app. Non è necessario creare un ciclo di nuovi tentativi per ogni transazione.

Infine, puoi utilizzare una transazione per leggere uno snapshot coerente di Datastore. Questo 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 denominati transazioni di sola lettura, in quanto non eseguono operazioni di scrittura. Le transazioni per gruppo singolo di sola lettura non vengono mai completate a causa di modifiche contemporanee, quindi non è necessario implementare nuovi tentativi in caso di errore. Tuttavia, le transazioni tra gruppi possono avere esito negativo a causa di modifiche contemporanee, perciò è necessario riprovare. Eseguire il commit e il rollback di una transazione di sola lettura sono due operazioni autonome.

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

Attività transazionale in coda

Puoi accodare un'attività nell'ambito di una transazione Datastore, in modo che l'attività venga accodata solo in coda e se il commit della transazione viene eseguito correttamente. Se viene eseguito il commit della transazione, l'attività è garantita per essere accodata. Una volta accodato, non è garantito che l'attività venga eseguita immediatamente e tutte le operazioni eseguite al suo interno vengono eseguite indipendentemente dalla transazione originale. Verrà eseguito un nuovo tentativo finché l'attività non va a buon fine. Ciò si applica a qualsiasi attività in coda nel contesto di una transazione.

Le attività transazionali sono utili perché consentono di inserire azioni non relative al datastore in una transazione Datastore (come l'invio di un'email per confermare un acquisto). Puoi anche collegare le azioni del datastore alla transazione, ad esempio eseguire il commit di modifiche su altri 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 in 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();