Entità, proprietà e chiavi

Gli oggetti di dati in Datastore sono noti come entità. Un'entità ha una o più proprietà denominate, ciascuna delle quali può avere uno o più valori. Le entità dello stesso tipo non devono necessariamente avere le stesse proprietà e i valori di un'entità per una determinata proprietà non devono essere tutti dello stesso tipo di dati. (Se necessario, un'applicazione può stabilire e applicare queste restrizioni nel proprio modello dei dati.)

Datastore supporta diversi tipi di dati per i valori delle proprietà. Alcuni esempi sono:

  • Numeri interi
  • Numeri con virgola mobile
  • Stringhe
  • Date
  • Dati binari

Per un elenco completo dei tipi, consulta Proprietà e tipi di valori.

Ogni entità in Datastore ha una chiave che la identifica in modo univoco. La chiave è costituita dai seguenti componenti:

  • Lo spazio dei nomi dell'entità, che consente la multitenancy
  • Il kind dell'entità, che la classifica ai fini delle query Datastore
  • Un identificatore per la singola entità, che può essere:
    • una stringa di nome chiave
    • un ID numerico intero
  • Un percorso predecessore facoltativo che individua l'entità all'interno della gerarchia di Datastore

Un'applicazione può recuperare una singola entità da Datastore utilizzando la chiave dell'entità oppure può recuperare una o più entità inviando una query in base alle chiavi o ai valori delle proprietà delle entità.

L'SDK Java App Engine include una semplice API, fornita nel pacchetto com.google.appengine.api.datastore, che supporta direttamente le funzionalità di Datastore. Tutti gli esempi in questo documento si basano su questa API di basso livello; puoi scegliere di utilizzarla direttamente nella tua applicazione o come base su cui creare il tuo livello di gestione dei dati.

Datastore non applica restrizioni alla struttura delle entità, ad esempio se una determinata proprietà ha un valore di un determinato tipo; questa attività viene lasciata all'applicazione.

Tipi e identificatori

Ogni entità Datastore è di un tipo particolare,che classifica l'entità ai fini delle query: ad esempio, un'applicazione per le risorse umane potrebbe rappresentare ciascun dipendente di un'azienda con un'entità di tipo Employee. Nell'API Java Datastore, specifichi il tipo di un'entità al momento della creazione, come argomento per il costruttore Entity(). Tutti i nomi dei tipi che iniziano con due trattini bassi (__) sono riservati e non possono essere utilizzati.

L'esempio seguente crea un'entità di tipo Employee, compila i relativi valori delle proprietà e la salva in Datastore:

Entity employee = new Entity("Employee", "asalieri");
employee.setProperty("firstName", "Antonio");
employee.setProperty("lastName", "Salieri");
employee.setProperty("hireDate", new Date());
employee.setProperty("attendedHrTraining", true);

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(employee);

Oltre a un tipo, a ogni entità è assegnato un identificatore, che viene assegnato al momento della creazione dell'entità. Poiché fa parte della chiave dell'entità, l'identificatore è associato in modo permanente all'entità e non può essere modificato. Può essere assegnato in due modi:

  • L'applicazione può specificare la propria stringa del nome della chiave per l'entità.
  • Puoi fare in modo che Datastore assegni automaticamente all'entità un ID numerico intero.

Per assegnare un nome chiave a un'entità, fornisci il nome come secondo argomento al costruttore quando crei l'entità:

Entity employee = new Entity("Employee", "asalieri");

Per fare in modo che Datastore assegni automaticamente un ID numerico, ometti questo argomento:

Entity employee = new Entity("Employee");

Assegnazione degli identificatori

Datastore può essere configurato per generare ID automatici utilizzando due criteri ID automatici diversi:

  • Il criterio default genera una sequenza casuale di ID inutilizzati che sono distribuiti approssimativamente in modo uniforme. Ogni ID può contenere fino a 16 cifre decimali.
  • Il criterio legacy crea una sequenza di ID interi più piccoli consecutivi.

Se vuoi mostrare gli ID entità all'utente e/o dipendono dal loro ordine, la soluzione migliore è utilizzare l'allocazione manuale.

Datastore genera una sequenza casuale di ID inutilizzati che sono distribuiti approssimativamente in modo uniforme. Ogni ID può contenere fino a 16 cifre decimali.

Percorsi predecessori

Le entità in Cloud Datastore formano uno spazio strutturato in modo gerarchico simile alla struttura di directory di un file system. Quando crei un'entità, puoi scegliere di indicare un'altra entità come parent;; la nuova entità è un elemento parent; dell'entità padre (tieni presente che, a differenza di un file system, l'entità padre non deve necessariamente esistere effettivamente. Un'entità senza un elemento padre è un'entità root. L'associazione tra un'entità e l'entità padre è permanente e non può essere modificata una volta creata l'entità. Cloud Datastore non assegnerà mai lo stesso ID numerico a due entità con lo stesso elemento padre o a due entità principali (quelle senza un elemento padre).

L'entità padre, l'entità padre e così via in modo ricorsivo, sono i suoi antenati; i relativi figli, figli e così via, sono i suoi discendenti. Un'entità base e tutti i suoi discendenti appartengono allo stesso gruppo di entità. La sequenza di entità che inizia con un'entità radice e procede dalla risorsa principale a quella secondaria e conduce a una determinata entità, costituisce il percorso predecessore dell'entità. La chiave completa che identifica l'entità è composta da una sequenza di coppie tipo-identificatore che specifica il percorso predecessore e termina con quelli dell'entità stessa:

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

Per unentità base, il percorso predecessore è vuoto e la chiave è composta esclusivamente dal tipo e dall'identificatore dell'entità:

[Person:GreatGrandpa]

Questo concetto è illustrato dal seguente diagramma:

Mostra la relazione tra l'entità base e le entità figlio nel gruppo di entità

Per designare l'entità padre, fornisci la chiave dell'entità padre come argomento al costruttore Entity() durante la creazione dell'entità figlio. Puoi ottenere la chiave chiamando il metodo getKey() dell'entità padre:

Entity employee = new Entity("Employee");
datastore.put(employee);

Entity address = new Entity("Address", employee.getKey());
datastore.put(address);

Se la nuova entità ha anche un nome di chiave, fornisci questo nome come secondo argomento al costruttore Entity() e la chiave dell'entità padre come terzo argomento:

Entity address = new Entity("Address", "addr1", employee.getKey());

Transazioni e gruppi di entità

Ogni tentativo di creare, aggiornare o eliminare un'entità avviene nel contesto di una transazione. Una singola transazione può includere un numero illimitato di operazioni di questo tipo. Per mantenere la coerenza dei dati, la transazione garantisce che tutte le operazioni che contiene vengano applicate a Datastore come un'unità o, se una delle operazioni non va a buon fine, che nessuna di esse venga applicata. Inoltre, tutte le letture a elevata coerenza (query o operazioni sui predecessori) eseguite all'interno della stessa transazione osservano uno snapshot coerente dei dati.

Come accennato in precedenza, un gruppo di entità è un insieme di entità collegate tramite discendenza a un elemento radice comune. L'organizzazione dei dati in gruppi di entità può limitare le transazioni che possono essere eseguite:

  • Tutti i dati a cui accede una transazione devono essere contenuti in un massimo di 25 gruppi di entità.
  • Se vuoi utilizzare le query all'interno di una transazione, i dati devono essere organizzati in gruppi di entità in modo da poter specificare filtri dei predecessori che corrispondano ai dati corretti.
  • Esiste un limite di velocità effettiva di scrittura di circa una transazione al secondo all'interno di un singolo gruppo di entità. Questa limitazione esiste perché Datastore esegue la replica sincrona e masterless di ogni gruppo di entità su un'area geografica ampia per fornire un'elevata affidabilità e tolleranza di errore.

In molte applicazioni è accettabile utilizzare la coerenza finale (ovvero una query non predecessore su più gruppi di entità, che a volte possono restituire dati leggermente inattivi) quando si ottiene un'ampia visione di dati non correlati, quindi utilizzare un'elevata coerenza (una query da predecessore o un get di una singola entità) quando si visualizza o modifica un singolo set di dati altamente correlati. In queste applicazioni, di solito è un buon approccio utilizzare un gruppo di entità separato per ogni set di dati altamente correlati. Per ulteriori informazioni, consulta la sezione Strutturare per una coerenza elevata.

Proprietà e tipi di valori

I valori dei dati associati a un'entità sono costituiti da una o più proprietà. Ogni proprietà ha un nome e uno o più valori. Una proprietà può avere valori di più di un tipo e due entità possono avere valori di tipi diversi per la stessa proprietà. Le proprietà possono essere indicizzate o non indicizzate (le query che ordinano o filtrano su una proprietà P ignoreranno le entità in cui P non è indicizzato). Un'entità può avere al massimo 20.000 proprietà indicizzate.

Sono supportati i seguenti tipi di valori:

Tipo di valore Tipi di Java Ordinamento Note
Numero intero short
int
long
java.lang.Short
java.lang.Integer
java.lang.Long
Numerico Archiviato come numero intero lungo, poi convertito nel tipo di campo

overflow dei valori fuori intervallo
Numero con virgola mobile float
double
java.lang.Float
java.lang.Double
Numerico Precisione doppia a 64 bit,
IEEE 754
Booleano boolean
java.lang.Boolean
false<true
Stringa di testo (breve) java.lang.String Unicode Fino a 1500 byte

Valori superiori a 1500 byte vengono generati IllegalArgumentException
Stringa di testo (lunga) com.google.appengine.api.datastore.Text Nessuna Fino a 1 megabyte

Non indicizzato
Stringa byte (breve) com.google.appengine.api.datastore.ShortBlob Ordine byte Fino a 1500 byte

I valori più lunghi di 1500 byte vengono generati IllegalArgumentException
Stringa byte (lunga) com.google.appengine.api.datastore.Blob Nessuna Fino a 1 megabyte

Non indicizzato
Data e ora java.util.Date Cronologica
Punto geografico com.google.appengine.api.datastore.GeoPt Per latitudine,
poi per longitudine
Indirizzo postale com.google.appengine.api.datastore.PostalAddress Unicode
Numero di telefono com.google.appengine.api.datastore.PhoneNumber Unicode
Indirizzo email com.google.appengine.api.datastore.Email Unicode
Utente Account Google com.google.appengine.api.users.User Indirizzo email
nell'ordine Unicode
Handle della messaggistica immediata com.google.appengine.api.datastore.IMHandle Unicode
Link com.google.appengine.api.datastore.Link Unicode
Categoria com.google.appengine.api.datastore.Category Unicode
Valutazione com.google.appengine.api.datastore.Rating Numerico
Chiave Datastore com.google.appengine.api.datastore.Key
o l'oggetto a cui viene fatto riferimento (come elemento secondario)
Per elementi del percorso
(kind, identifier,
kind, identifier...)
Fino a 1500 byte

I valori più lunghi di 1500 byte vengono generati IllegalArgumentException
Chiave Blobstore com.google.appengine.api.blobstore.BlobKey Ordine byte
Entità incorporata com.google.appengine.api.datastore.EmbeddedEntity Nessuna Non indicizzato
Null null Nessuna

Importante:ti consigliamo vivamente di evitare di archiviare users.User come valore di proprietà, perché include l'indirizzo email e l'ID univoco. Se un utente cambia il proprio indirizzo email e confronti il precedente valore user.User archiviato con il nuovo valore di user.User, non corrisponderanno. Utilizza invece il valore ID utente User come identificatore univoco stabile dell'utente.

Per le stringhe di testo e i dati binari non codificati (stringhe di byte), Datastore supporta due tipi di valori:

  • Le stringhe brevi (fino a 1500 byte) vengono indicizzate e possono essere utilizzate nelle condizioni di filtro delle query e negli ordini di ordinamento.
  • Le stringhe lunghe (fino a 1 megabyte) non vengono indicizzate e non possono essere utilizzate nei filtri delle query e negli ordini di ordinamento.
Nota: il tipo di stringa di byte lungo è denominato Blob nell'API Datastore. Questo tipo non è correlato ai BLOB utilizzati nell'API Blobstore.

Quando una query riguarda una proprietà con valori di tipi misti, Datastore utilizza un ordinamento deterministico basato sulle rappresentazioni interne:

  1. Valori null
  2. Numeri a virgola fissa
    • Numeri interi
    • Date e ore
    • Valutazioni
  3. Valori booleani
  4. Sequenze di byte
    • Stringa byte
    • Stringa Unicode
    • Chiavi dell'archivio BLOB
  5. Numeri con virgola mobile
  6. Punti geografici
  7. Utenti con account Google
  8. Chiavi Datastore

Poiché le stringhe di testo lunghe, le stringhe di byte lunghi e le entità incorporate non sono indicizzate, non hanno un ordine definito.

Utilizzare le entità

Le applicazioni possono utilizzare l'API Datastore per creare, recuperare, aggiornare ed eliminare le entità. Se l'applicazione conosce la chiave completa di un'entità (o può ricavarla dalla chiave, dal tipo e dall'identificatore padre), può utilizzare la chiave per operare direttamente sull'entità. Un'applicazione può anche ottenere la chiave di un'entità come risultato di una query Datastore; consulta la pagina Query Datastore per ulteriori informazioni.

L'API Java Datastore utilizza i metodi dell'interfaccia DatastoreService per operare sulle entità. Per ottenere un oggetto DatastoreService, richiama il metodo statico DatastoreServiceFactory.getDatastoreService():

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Creazione di un'entità

Puoi creare una nuova entità costruendo un'istanza di classe Entity, fornendo il tipo di entità come argomento al costruttore di Entity().

Dopo aver compilato le proprietà dell'entità, se necessario, la salvi nel datastore passandola come argomento al metodo DatastoreService.put(). Puoi specificare il nome della chiave dell'entità passandolo come secondo argomento al costruttore:

Entity employee = new Entity("Employee", "asalieri");
// Set the entity properties.
// ...
datastore.put(employee);

Se non specifichi un nome per la chiave, Datastore genererà automaticamente un ID numerico per la chiave dell'entità:

Entity employee = new Entity("Employee");
// Set the entity properties.
// ...
datastore.put(employee);

Recupero di un'entità

Per recuperare un'entità identificata da una determinata chiave, passa l'oggetto Key al metodo DatastoreService.get():

// Key employeeKey = ...;
Entity employee = datastore.get(employeeKey);

Aggiornamento di un'entità

Per aggiornare un'entità esistente, modifica gli attributi dell'oggetto Entità, quindi passalo al metodo DatastoreService.put(). I dati dell'oggetto sovrascrivono l'entità esistente. L'intero oggetto viene inviato a Datastore con ogni chiamata a put().

Eliminazione di un'entità

Data la chiave di un'entità, puoi eliminarla con il metodo DatastoreService.delete():

// Key employeeKey = ...;
datastore.delete(employeeKey);

Proprietà ripetute

Puoi archiviare più valori all'interno di una singola proprietà.

Entity employee = new Entity("Employee");
ArrayList<String> favoriteFruit = new ArrayList<String>();
favoriteFruit.add("Pear");
favoriteFruit.add("Apple");
employee.setProperty("favoriteFruit", favoriteFruit);
datastore.put(employee);

// Sometime later
employee = datastore.get(employee.getKey());
@SuppressWarnings("unchecked") // Cast can't verify generic type.
    ArrayList<String> retrievedFruits = (ArrayList<String>) employee
    .getProperty("favoriteFruit");

Entità incorporate

A volte potrebbe essere pratico incorporare un'entità come proprietà di un'altra entità. Questo può essere utile, ad esempio, per creare una struttura gerarchica dei valori delle proprietà all'interno di un'entità. La classe Java EmbeddedEntity consente di eseguire queste operazioni:

// Entity employee = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
embeddedContactInfo.setProperty("phoneNumber", "555-555-5555");
embeddedContactInfo.setProperty("emailAddress", "test@example.com");

employee.setProperty("contactInfo", embeddedContactInfo);

Quando un'entità incorporata è inclusa negli indici, puoi eseguire query sulle proprietà secondarie. Se escludi un'entità incorporata dall'indicizzazione, anche tutte le proprietà secondarie verranno escluse dall'indicizzazione. Facoltativamente, puoi associare una chiave a un'entità incorporata, ma (a differenza di un'entità completa) la chiave non è obbligatoria e, anche se presente, non può essere utilizzata per recuperare l'entità.

Anziché compilare manualmente le proprietà dell'entità incorporata, puoi utilizzare il metodo setPropertiesFrom() per copiarle da un'entità esistente:

// Entity employee = ...;
// Entity contactInfo = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original.
embeddedContactInfo.setPropertiesFrom(contactInfo);

employee.setProperty("contactInfo", embeddedContactInfo);

In seguito, puoi utilizzare lo stesso metodo per recuperare l'entità originale dall'entità incorporata:

Entity employee = datastore.get(employeeKey);
EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo");

Key infoKey = embeddedContactInfo.getKey();
Entity contactInfo = new Entity(infoKey);
contactInfo.setPropertiesFrom(embeddedContactInfo);

Operazioni batch

I metodi DatastoreService put(), get(), e delete() (e le relative controparti AsyncDatastoreService) hanno versioni batch che accettano un oggetto iterabile (di classe Entity per put(), Key per get() e delete()) e lo utilizzano per operare su più entità in una singola chiamata Datastore:

Entity employee1 = new Entity("Employee");
Entity employee2 = new Entity("Employee");
Entity employee3 = new Entity("Employee");
// ...

List<Entity> employees = Arrays.asList(employee1, employee2, employee3);
datastore.put(employees);

Queste operazioni batch raggruppano tutte le entità o le chiavi per gruppo di entità e quindi eseguono l'operazione richiesta su ogni gruppo di entità in parallelo. Queste chiamate batch sono più veloci dell'esecuzione di chiamate separate per ogni singola entità, perché comportano l'overhead per una sola chiamata di servizio. Se sono coinvolti più gruppi di entità, il lavoro per tutti i gruppi viene eseguito in parallelo sul lato server.

Generazione di chiavi

Le applicazioni possono utilizzare la classe KeyFactory per creare un oggetto Key per un'entità da componenti noti, come il tipo e l'identificatore dell'entità. Per un'entità senza elemento principale, trasmetti il tipo e l'identificatore (una stringa con il nome della chiave o un ID numerico) al metodo statico KeyFactory.createKey() per creare la chiave. I seguenti esempi creano una chiave per un'entità di tipo Person con nome chiave "GreatGrandpa" o ID numerico 74219:

Key k1 = KeyFactory.createKey("Person", "GreatGrandpa");
Key k2 = KeyFactory.createKey("Person", 74219);

Se la chiave include un componente del percorso, puoi utilizzare la classe helper KeyFactory.Builder per creare il percorso. Il metodo addChild di questa classe aggiunge una singola entità al percorso e restituisce il generatore stesso, quindi puoi concatenare una serie di chiamate, a partire dall'entità base, per creare il percorso un'entità alla volta. Dopo aver creato il percorso completo, chiama getKey per recuperare la chiave risultante:

Key k =
    new KeyFactory.Builder("Person", "GreatGrandpa")
        .addChild("Person", "Grandpa")
        .addChild("Person", "Dad")
        .addChild("Person", "Me")
        .getKey();

La classe KeyFactory include anche i metodi statici keyToString e stringToKey per la conversione tra chiavi e le relative rappresentazioni di stringa:

String personKeyStr = KeyFactory.keyToString(k);

// Some time later (for example, after using personKeyStr in a link).
Key personKey = KeyFactory.stringToKey(personKeyStr);
Entity person = datastore.get(personKey);

La rappresentazione stringa di una chiave è "sicuro per il web": non contiene caratteri considerati speciali nel codice HTML o negli URL.

Utilizzo di un elenco vuoto

Storicamente Datastore non aveva una rappresentazione per una proprietà che rappresenta un elenco vuoto. L'SDK Java ha risolto questo problema archiviando le raccolte vuote come valori null, quindi non è possibile distinguere tra valori null e elenchi vuoti. Per mantenere la compatibilità con le versioni precedenti, questo rimane il comportamento predefinito, sintetizzato come segue:

  • Le proprietà null sono scritte come null in Datastore
  • Le raccolte vuote vengono scritte come null in Datastore
  • Un valore null viene letto come null in Datastore
  • Una raccolta vuota viene letta come nulla.

Tuttavia, se modifichi il comportamento predefinito, l'SDK per Java supporterà l'archiviazione di elenchi vuoti. Ti consigliamo di valutare le implicazioni della modifica del comportamento predefinito dell'applicazione e di attivare il supporto per gli elenchi vuoti.

Per modificare il comportamento predefinito in modo da utilizzare elenchi vuoti, imposta la proprietà DATASTORE_EMPTY_LIST_SUPPORT durante l'inizializzazione dell'app come segue:

System.setProperty(DatastoreServiceConfig.DATASTORE_EMPTY_LIST_SUPPORT, Boolean.TRUE.toString());

Con questa proprietà impostata su true come mostrato sopra:

  • Le proprietà null sono scritte come null in Datastore
  • Le raccolte vuote vengono scritte come elenco vuoto in Datastore
  • Un valore null viene letto come null in Datastore
  • Durante la lettura da Datastore, viene restituito un elenco vuoto come raccolta vuota.