Puoi modellare le relazioni tra oggetti permanenti utilizzando i campi dei tipi di oggetti. Una relazione tra oggetti persistenti può essere descritta come di proprietà, se uno degli oggetti non può esistere senza l'altro, o senza proprietario, se entrambi gli oggetti possono esistere indipendentemente dalla loro relazione tra loro. L'implementazione di App Engine dell'interfaccia JDO può modellare sia le relazioni one-to-one sia quelle one-to-many, sia di proprietà che non di proprietà, sia unidirezionali che bidirezionali.
Le relazioni non di proprietà non sono supportate nella versione 1.0 del plug-in DataNucleus per App Engine, ma puoi gestirle autonomamente memorizzando direttamente le chiavi del datastore nei campi. App Engine crea automaticamente entità correlate in gruppi di entità per supportare l'aggiornamento di oggetti correlati, ma è responsabilità dell'app sapere quando utilizzare le transazioni del datastore.
La versione 2.x del plug-in DataNucleus per App Engine supporta le relazioni non di proprietà con una sintassi naturale. La sezione Relazioni non di proprietà illustra come creare relazioni non di proprietà in ogni versione del plug-in. Per eseguire l'upgrade alla versione 2.x del plug-in DataNucleus per App Engine, consulta Eseguire la migrazione alla versione 2.x del plug-in DataNucleus per App Engine.
Relazioni one-to-one di proprietà
Puoi creare una relazione di proprietà one-to-one unidirezionale tra due oggetti permanenti utilizzando un campo il cui tipo è la classe della classe correlata.
L'esempio seguente definisce una classe di dati ContactInfo e una classe di dati Employee, con una relazione uno a uno da Employee a ContactInfo.
ContactInfo.java
import com.google.appengine.api.datastore.Key; // ... imports ... @PersistenceCapable public class ContactInfo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private String streetAddress; // ... }
Employee.java
import ContactInfo; // ... imports ... @PersistenceCapable public class Employee { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private ContactInfo contactInfo; ContactInfo getContactInfo() { return contactInfo; } void setContactInfo(ContactInfo contactInfo) { this.contactInfo = contactInfo; } // ... }
Gli oggetti permanenti sono rappresentati come due entità distinte nel datastore, con due tipi diversi. La relazione è rappresentata utilizzando un rapporto tra gruppi di entità: la chiave del gruppo secondario utilizza la chiave del gruppo principale come gruppo di entità principale. Quando l'app accede all'oggetto secondario utilizzando il campo dell'oggetto principale, l'implementazione JDO esegue una query sul gruppo di entità principale per ottenere l'oggetto secondario.
La classe secondaria deve avere un campo chiave il cui tipo può contenere le informazioni sulla chiave principale: una chiave o un valore della chiave codificato come stringa. Consulta Creazione di dati: chiavi per informazioni sui tipi di campi chiave.
Puoi creare una relazione uno a uno bidirezionale utilizzando i campi di entrambe le classi, con un'annotazione sul campo della classe secondaria per dichiarare che i campi rappresentano una relazione bidirezionale. Il campo della classe secondaria deve avere un'annotazione @Persistent
con l'argomento mappedBy = "..."
, dove il valore è il nome del campo nella classe principale. Se il campo in un oggetto è compilato, il corrispondente
campo di riferimento nell'altro oggetto viene compilato automaticamente.
ContactInfo.java
import Employee; // ... @Persistent(mappedBy = "contactInfo") private Employee employee;
Gli oggetti secondari vengono caricati dal datastore quando vengono acceduti per la prima volta. Se non accedi all'oggetto secondario in un oggetto principale, l'entità per l'oggetto secondario non viene mai caricata. Se vuoi caricare l'elemento secondario, puoi "toccarlo" prima di chiudere PersistenceManager (ad es. chiamando getContactInfo()
nell'esempio precedente) o aggiungere esplicitamente il campo secondario al gruppo di recupero predefinito in modo che venga recuperato e caricato con l'elemento principale:
Employee.java
import ContactInfo; // ... @Persistent(defaultFetchGroup = "true") private ContactInfo contactInfo;
Relazioni one-to-many di proprietà
Per creare una relazione one-to-many tra oggetti di una classe e diversi oggetti di un'altra, utilizza una raccolta della classe correlata:
Employee.java
import java.util.List; // ... @Persistent private List<ContactInfo> contactInfoSets;
Una relazione bidirezionale uno a molti è simile a una uno a uno, con un
campo nella classe principale che utilizza l'annotazione
@Persistent(mappedBy = "...")
, dove il valore è il nome del
campo nella classe secondaria:
Employee.java
import java.util.List; // ... @Persistent(mappedBy = "employee") private List<ContactInfo> contactInfoSets;
ContactInfo.java
import Employee; // ... @Persistent private Employee employee;
I tipi di raccolte elencati in Definire le classi di dati: raccolte sono supportati per le relazioni one-to-many. Tuttavia, gli array non sono supportati per le relazioni one-to-many.
App Engine non supporta le query di join: non puoi eseguire query su un'entità padre utilizzando un attributo di un'entità secondaria. Puoi eseguire query su una proprietà di una classe incorporata perché le classi incorporate memorizzano le proprietà nell'entità padre. Consulta Definire le classi di dati: classi incorporate.
Come le raccolte ordinate mantengono l'ordine
Le raccolte ordinate, come List<...>
, mantengono l'ordine
degli oggetti quando viene salvato l'oggetto principale. JDO richiede che i database conservino questo ordine archiviando la posizione di ogni oggetto come proprietà dell'oggetto.
App Engine la memorizza come proprietà dell'entità corrispondente, utilizzando un nome proprietà uguale al nome del campo del relativo elemento principale seguito da _INTEGER_IDX
. Le proprietà di posizione non sono efficienti. Se un elemento viene aggiunto, rimosso o spostato nella raccolta, tutte le entità successive al luogo modificato nella raccolta devono essere aggiornate. Questa operazione può essere lenta e soggetta a errori se non viene eseguita in una transazione.
Se non devi mantenere un ordine arbitrario in una raccolta, ma devi utilizzare un tipo di raccolta ordinata, puoi specificare un ordine in base alle proprietà degli elementi utilizzando un'annotazione, un'estensione di JDO fornita da DataNucleus:
import java.util.List; import javax.jdo.annotations.Extension; import javax.jdo.annotations.Order; import javax.jdo.annotations.Persistent; // ... @Persistent @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc")) private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();
L'annotazione @Order
(che utilizza l'estensione list-ordering
) specifica l'ordine desiderato degli elementi della raccolta come clausola di ordinamento JDOQL. L'ordinamento utilizza i valori delle proprietà degli elementi.
Come per le query, tutti gli elementi di una raccolta devono avere valori per le proprietà utilizzate nella clausola di ordinamento.
L'accesso a una raccolta esegue una query. Se la clausola di ordinamento di un campo utilizza più di un ordine di ordinamento, la query richiede un indice Datastore. Per ulteriori informazioni, consulta la pagina Indici Datastore.
Per maggiore efficienza, se possibile, utilizza sempre una clausola di ordinamento esplicita per le relazioni uno a molti dei tipi di raccolte ordinate.
Relazioni non di proprietà
Oltre alle relazioni di proprietà, l'API JDO fornisce anche un'utilità per gestire le relazioni non di proprietà. Questa funzionalità funziona in modo diverso a seconda della versione del plug-in DataNucleus per App Engine in uso:
- La versione 1 del plug-in DataNucleus non implementa le relazioni non di proprietà utilizzando una sintassi naturale, ma puoi comunque gestire queste relazioni utilizzando i valori
Key
al posto delle istanze (o delle raccolte di istanze) degli oggetti del tuo modello. Puoi pensare alla memorizzazione di oggetti Key come alla definizione di una "chiave esterna" arbitraria tra due oggetti. Il datastore non garantisce l'integrità referenziale con questi riferimenti Key, ma l'utilizzo di Key consente di modellare (e quindi recuperare) molto facilmente qualsiasi relazione tra due oggetti.
Tuttavia, se scegli questa strada, devi assicurarti che le chiavi siano del tipo appropriato. JDO e il compilatore non controllano i tipiKey
per te. - La versione 2.x del plug-in DataNucleus implementa le relazioni non di proprietà utilizzando una sintassi naturale.
Suggerimento:in alcuni casi, potresti dover modellare un rapporto di proprietà come se non fosse di proprietà. Questo perché tutti gli oggetti coinvolti in una relazione di proprietà vengono inseriti automaticamente nello stesso gruppo di entità e un gruppo di entità può supportare solo da una a dieci scritture al secondo. Ad esempio, se un oggetto principale riceve 0,75 scritture al secondo e un oggetto secondario ne riceve altre 0, 75, potrebbe essere opportuno modellare questa relazione come non di proprietà in modo che sia l'oggetto principale che quello secondario si trovino in gruppi di entità indipendenti.
Relazioni one-to-one non di proprietà
Supponiamo che tu voglia modellare una persona e un alimento, dove una persona può avere un solo alimento preferito, ma un alimento preferito non appartiene alla persona perché può essere l'alimento preferito di un numero qualsiasi di persone. Questa sezione illustra come fare.
In JDO 2.3
In questo esempio, assegniamo a Person
un membro di tipo
Key
, dove Key
è l'identificatore univoco di un
oggetto Food
. Se un'istanza di Person
e l'istanza di Food
a cui fa riferimento
Person.favoriteFood
non appartengono allo stesso gruppo di entità, non puoi
aggiornare la persona e il suo cibo preferito in un'unica
transazione, a meno che la configurazione JDO non sia impostata su
attiva
le transazioni tra gruppi (XG).
Person.java
// ... imports ... @PersistenceCapable public class Person { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Key favoriteFood; // ... }
Food.java
import Person; // ... imports ... @PersistenceCapable public class Food { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; // ... }
In JDO 3.0
In questo esempio, anziché assegnare a Person
una chiave che rappresenti il suo cibo preferito, creiamo un membro privato di tipo Food
:
Person.java
// ... imports ... @PersistenceCapable public class Person { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent @Unowned private Food favoriteFood; // ... }
Food.java
import Person; // ... imports ... @PersistenceCapable public class Food { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; // ... }
Relazioni one-to-many non di proprietà
Supponiamo ora di voler consentire a una persona di avere più cibi preferiti. Ancora una volta, un piatto preferito non appartiene alla persona perché può essere il piatto preferito di un numero qualsiasi di persone.
In JDO 2.3
In questo esempio, anziché assegnare a Persona un membro di tipo
Set<Food>
per rappresentare i cibi preferiti della persona, assegnamo a Persona un membro di tipo Set<Key>
, dove l'insieme
contiene gli identificatori univoci degli oggetti Food
. Tieni presente che, se
un'istanza di Person
e un'istanza di Food
contenuto in Person.favoriteFoods
non si trovano nello stesso gruppo di entità,
devi impostare la configurazione JDO su
attivare
le transazioni tra gruppi (XG) se vuoi aggiornarle nella stessa
transazione.
Person.java
// ... imports ... @PersistenceCapable public class Person { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Set<Key> favoriteFoods; // ... }
In JDO 3.0
In questo esempio, assegniamo a Person un membro di tipo Set<Food>
in cui l'insieme rappresenta i cibi preferiti di Person.
Person.java
// ... imports ... @PersistenceCapable public class Person { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Set<Food> favoriteFoods; // ... }
Relazioni many-to-many
Possiamo modellare una relazione many-to-many mantenendo raccolte di chiavi su entrambi i lati della relazione. Modifichiamo l'esempio per consentire a Food
di tenere traccia delle persone che lo considerano un preferito:
Person.java
import java.util.Set; import com.google.appengine.api.datastore.Key; // ... @Persistent private Set<Key> favoriteFoods;
Food.java
import java.util.Set; import com.google.appengine.api.datastore.Key; // ... @Persistent private Set<Key> foodFans;
In questo esempio, Person
gestisce un insieme di valori Key
che identificano in modo univoco gli oggetti Food
che sono preferiti e Food
gestisce un insieme di valori Key
che identificano in modo univoco gli oggetti Person
che lo considerano un preferito.
Quando modelli una relazione molti a molti utilizzando i valori Key
, tieni presente che è responsabilità dell'app gestire entrambi i lati della relazione:
Album.java
// ... public void addFavoriteFood(Food food) { favoriteFoods.add(food.getKey()); food.getFoodFans().add(getKey()); } public void removeFavoriteFood(Food food) { favoriteFoods.remove(food.getKey()); food.getFoodFans().remove(getKey()); }
Se un'istanza di Person
e un'istanza di
Food
contenute in Person.favoriteFoods
non si trovano nello stesso gruppo di entità e vuoi aggiornarle in un'unica transazione, devi impostare la configurazione JDO in modo da
abilitare le transazioni tra gruppi (XG).
Relazioni, gruppi di entità e transazioni
Quando l'applicazione salva un oggetto con relazioni di proprietà nel datastore, tutti gli altri oggetti che possono essere raggiunti tramite le relazioni e devono essere salvati (sono nuovi o sono stati modificati dall'ultimo caricamento) vengono salvati automaticamente. Ciò ha importanti implicazioni per le transazioni e i gruppi di entità.
Prendi in considerazione l'esempio seguente che utilizza una relazione unidirezionale tra le classi Employee
e ContactInfo
riportate sopra:
Employee e = new Employee(); ContactInfo ci = new ContactInfo(); e.setContactInfo(ci); pm.makePersistent(e);
Quando il nuovo oggetto Employee
viene salvato utilizzando il metodo pm.makePersistent()
, il nuovo oggetto ContactInfo
correlato viene salvato automaticamente. Poiché entrambi gli oggetti sono nuovi, App Engine crea due nuove entità nello stesso gruppo di entità, utilizzando l'entità Employee
come entità principale dell'entità ContactInfo
. Analogamente, se l'oggetto Employee
è già stato salvato e l'oggetto ContactInfo
correlato è nuovo, App Engine crea l'entità ContactInfo
utilizzando l'entità Employee
esistente come principale.
Tieni presente, però, che la chiamata a pm.makePersistent()
in questo
esempio non utilizza una transazione. Senza una transazione esplicita, entrambe le entità vengono create utilizzando azioni atomiche distinte. In questo caso, è possibile che la creazione dell'entità Employee vada a buon fine, ma che la creazione dell'entità ContactInfo non riesca. Per assicurarti che entrambe le entità vengano create correttamente o che nessuna delle due venga creata, devi utilizzare una transazione:
Employee e = new Employee(); ContactInfo ci = new ContactInfo(); e.setContactInfo(ci); try { Transaction tx = pm.currentTransaction(); tx.begin(); pm.makePersistent(e); tx.commit(); } finally { if (tx.isActive()) { tx.rollback(); } }
Se entrambi gli oggetti sono stati salvati prima dell'istituzione della relazione, App Engine non può "spostare" l'entità ContactInfo
esistente nel gruppo di entità dell'entità Employee
, perché i gruppi di entità possono essere assegnati solo al momento della creazione delle entità. App Engine può stabilire la relazione con un riferimento, ma le entità correlate non saranno nello stesso gruppo. In questo caso, le due entità possono essere aggiornate o eliminate nella stessa transazione se imposti la configurazione JDO in modo da abilitare le transazioni tra gruppi (XG). Se non utilizzi le transazioni XG, il tentativo di aggiornare o eliminare entità di gruppi diversi nella stessa transazione comporterà l'emissione di un'JDOFatalUserException.
Se salvi un oggetto principale i cui oggetti secondari sono stati modificati, verranno salvate anche le modifiche apportate agli oggetti secondari. È buona norma consentire agli oggetti principali di mantenere la persistenza per tutti gli oggetti secondari correlati in questo modo e di utilizzare le transazioni quando si salvano le modifiche.
Elementi secondari e cancellazioni con propagazione
Una relazione di proprietà può essere "dipendente", il che significa che l'elemento secondario non può esistere senza l'elemento principale. Se una relazione è dipendente e un oggetto principale viene eliminato, vengono eliminati anche tutti gli oggetti secondari. Se interrompi una relazione di proprietà e dipendenza assegnando un nuovo valore al campo dipendente nel principale, viene eliminato anche il vecchio elemento secondario. Puoi dichiarare una relazione uno a uno di proprietà come dipendente aggiungendo dependent="true"
all'annotazione Persistent
del campo nell'oggetto principale che fa riferimento a quello secondario:
// ... @Persistent(dependent = "true") private ContactInfo contactInfo;
Puoi dichiarare una relazione uno a molti di proprietà come dipendente aggiungendo
un'annotazione @Element(dependent = "true")
al campo dell'
oggetto principale che fa riferimento alla raccolta secondaria:
import javax.jdo.annotations.Element; // ... @Persistent @Element(dependent = "true") private ListcontactInfos;
Come per la creazione e l'aggiornamento degli oggetti, se vuoi che ogni eliminazione in un'eliminazione a cascata venga eseguita in un'unica azione atomica, devi eseguire l'eliminazione in una transazione.
Nota:l'implementazione JDO esegue l'eliminazione degli oggetti secondari dipendenti, non del datastore. Se elimini un'entità padre utilizzando l'API di basso livello o la console Google Cloud, gli oggetti secondari correlati non verranno eliminati.
Relazioni polimorfiche
Anche se la specifica JDO include il supporto per le relazioni polimorfiche, queste non sono ancora supportate nell'implementazione JDO di App Engine. Si tratta di una limitazione che speriamo di rimuovere nelle release future del prodotto. Se devi fare riferimento a più tipi di oggetti tramite una classe di base comune, ti consigliamo la stessa strategia utilizzata per implementare le relazioni non di proprietà: memorizza un riferimento a Key. Ad esempio, se hai una classe di base Recipe
con specializzazioni Appetizer
, Entree
e Dessert
e vuoi modellare il preferito Recipe
di un Chef
, puoi farlo nel seguente modo:
Recipe.java
import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.Inheritance; import javax.jdo.annotations.InheritanceStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; @PersistenceCapable @Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE) public abstract class Recipe { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private int prepTime; }
Appetizer.java
// ... imports ... @PersistenceCapable public class Appetizer extends Recipe { // ... appetizer-specific fields }
Entree.java
// ... imports ... @PersistenceCapable public class Entree extends Recipe { // ... entree-specific fields }
Dessert.java
// ... imports ... @PersistenceCapable public class Dessert extends Recipe { // ... dessert-specific fields }
Chef.java
// ... imports ... @PersistenceCapable public class Chef { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent(dependent = "true") private Recipe favoriteRecipe; }
Purtroppo, se esegui l'inizializzazione di un Entree
e lo assegni a
Chef.favoriteRecipe
, verrà visualizzato un
UnsupportedOperationException
quando provi a mantenere l'oggetto
Chef
. Questo accade perché il tipo di runtime dell'oggetto,
Entree
, non corrisponde al tipo dichiarato del campo della relazione,
Recipe
. La soluzione alternativa consiste nel modificare il tipo di
Chef.favoriteRecipe
da Recipe
a
Key
:
Chef.java
// ... imports ... @PersistenceCapable public class Chef { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Key favoriteRecipe; }
Poiché Chef.favoriteRecipe
non è più un campo di relazione, può fare riferimento a un oggetto di qualsiasi tipo. Lo svantaggio è che, come per una relazione non di proprietà, devi gestire questa relazione manualmente.