Relazioni di entità in JDO

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 tipi Key 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 List contactInfos;

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.