Relazioni di entità in JDO

Puoi modellare le relazioni tra oggetti persistenti utilizzando i campi dei tipi di oggetti. Una relazione tra oggetti persistenti può essere descritta come di proprietà, in cui uno degli oggetti non può esistere senza l'altro, o non di proprietà, in cui entrambi gli oggetti possono esistere indipendentemente dalla loro relazione tra loro. L'implementazione dell'interfaccia JDO di App Engine può modellare relazioni one-to-one e one-to-many di proprietà e non di proprietà, unidirezionali e bidirezionali.

Le relazioni non proprietarie 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 nei gruppi di entità per supportare l'aggiornamento congiunto degli 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 proprietarie con una sintassi naturale. La sezione Relazioni non di proprietà indica 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 Migrazione alla versione 2.x del plug-in DataNucleus per App Engine.

Relazioni one-to-one di proprietà

Crei una relazione di proprietà unidirezionale one-to-one tra due oggetti persistenti 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 persistenti sono rappresentati come due entità distinte nel datastore, con due tipi diversi. La relazione è rappresentata utilizzando una relazione tra gruppi di entità: la chiave del figlio utilizza la chiave del genitore come genitore del gruppo di entità. Quando l'app accede all'oggetto secondario utilizzando il campo dell'oggetto principale, l'implementazione JDO esegue una query principale del gruppo di entità per ottenere l'oggetto secondario.

La classe secondaria deve avere un campo chiave il cui tipo può contenere le informazioni della chiave principale: una chiave o un valore chiave codificato come stringa. Per informazioni sui tipi di campi chiave, consulta Creazione di dati: chiavi.

Crei una relazione bidirezionale uno-a-uno 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 di un oggetto viene compilato, il campo di riferimento corrispondente dell'altro oggetto viene compilato automaticamente.

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

Gli oggetti secondari vengono caricati dal datastore quando vengono accessibili 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 il figlio, puoi "toccarlo" prima di chiudere PersistenceManager (ad es. chiamando getContactInfo() nell'esempio precedente) o aggiungere esplicitamente il campo figlio al gruppo di recupero predefinito in modo che venga recuperato e caricato con il genitore:

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

Relazioni one-to-many di proprietà

Per creare una relazione one-to-many tra gli oggetti di una classe e più 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 relazione 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 raccolta elencati in Definizione delle 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 unione: 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 archiviano le proprietà nell&#39entità padree. Vedi Definizione delle classi di dati: classi incorporate.)

Come le raccolte ordinate mantengono il loro ordine

Le raccolte ordinate, come List<...>, conservano l'ordine degli oggetti quando l'oggetto principale viene salvato. JDO richiede che i database conservino questo ordine memorizzando la posizione di ogni oggetto come proprietà dell'oggetto. App Engine lo memorizza come proprietà dell'entità corrispondente, utilizzando un nome proprietà uguale al nome del campo del genitore seguito da _INTEGER_IDX. Le proprietà di posizione sono inefficienti. 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 ordinamento 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 ordinamento, la query richiede un indice Datastore. Per ulteriori informazioni, consulta la pagina Indici Datastore.

Per efficienza, utilizza sempre una clausola di ordinamento esplicita per le relazioni uno-a-molti dei tipi di raccolta ordinati, se possibile.

Relazioni non di proprietà

Oltre alle relazioni proprietarie, l'API JDO fornisce anche uno strumento per gestire le relazioni non proprietarie. Questa funzionalità funziona in modo diverso a seconda della versione del plug-in DataNucleus per App Engine che utilizzi:

  • La versione 1 del plug-in DataNucleus non implementa le relazioni non proprietarie utilizzando una sintassi naturale, ma puoi comunque gestire queste relazioni utilizzando i valori Key al posto di istanze (o raccolte di istanze) degli oggetti modello. Puoi pensare di archiviare gli oggetti Key come se modellassi una "chiave esterna" arbitraria tra due oggetti. Il datastore non garantisce l'integrità referenziale con questi riferimenti alle chiavi, ma l'utilizzo delle chiavi rende molto semplice modellare (e poi recuperare) 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 relazioni non di proprietà utilizzando una sintassi naturale.

Suggerimento:in alcuni casi, potrebbe essere necessario modellare una relazione di proprietà come se non lo fosse. 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 1 a 10 scritture al secondo. Ad esempio, se un oggetto principale riceve 0,75 scritture al secondo e un oggetto secondario riceve 0, 75 scritture al secondo, potrebbe essere opportuno modellare questa relazione come non di proprietà in modo che sia l'oggetto principale sia quello secondario risiedano nei propri gruppi di entità indipendenti.

Relazioni one-to-one non di proprietà

Supponiamo di voler modellare una persona e il suo cibo, dove una persona può avere un solo cibo preferito, ma un cibo preferito non appartiene alla persona perché può essere il cibo preferito di un numero qualsiasi di persone. Questa sezione mostra come farlo.

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 si trovano nello stesso gruppo di entità, non puoi aggiornare la persona e il suo cibo preferito in una singola transazione, a meno che la configurazione JDO non sia impostata per attivare le transazioni cross-group (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 rappresenta 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. Anche in questo caso, un cibo preferito non appartiene alla persona perché può essere il cibo preferito di un numero qualsiasi di persone.

In JDO 2.3

In questo esempio, anziché assegnare a Person un membro di tipo Set<Food> per rappresentare i cibi preferiti della persona, assegniamo a Person un membro di tipo Set<Key>, in cui l'insieme contiene gli identificatori univoci degli oggetti Food. Tieni presente che, se un'istanza di Person e un'istanza di Food contenute in Person.favoriteFoods non si trovano nello stesso gruppo di entità, devi impostare la configurazione JDO in modo da 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 della persona.

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 molti-a-molti mantenendo raccolte di chiavi su entrambi i lati della relazione. Modifichiamo il nostro 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 mantiene un insieme di valori Key che identificano in modo univoco gli oggetti Food preferiti, mentre Food mantiene 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 mantenere 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 contenuta 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 attivare le transazioni tra gruppi (XG).

Relazioni, gruppi di entità e transazioni

Quando l'applicazione salva un oggetto con relazioni di proprietà nel datastore, vengono salvati automaticamente tutti gli altri oggetti raggiungibili tramite le relazioni e che devono essere salvati (sono nuovi o sono stati modificati dall'ultimo caricamento). Ciò ha importanti implicazioni per le transazioni e i gruppi di entità.

Considera 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 entità principale.

Tieni presente, tuttavia, 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 separate. In questo caso, è possibile che la creazione dell'entità Employee riesca, 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 che venisse stabilita la 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 si troveranno 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 attivare le transazioni cross-group (XG). Se non utilizzi le transazioni XG, il tentativo di aggiornare o eliminare entità di gruppi diversi nella stessa transazione genererà un'eccezione JDOFatalUserException.

Se salvi un oggetto padre i cui oggetti secondari sono stati modificati, le modifiche agli oggetti secondari verranno salvate. È consigliabile consentire agli oggetti principali di mantenere la persistenza per tutti gli oggetti secondari correlati in questo modo e di utilizzare le transazioni durante il salvataggio delle modifiche.

Eliminazione a cascata di figli a carico

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. L'interruzione di una relazione di proprietà e dipendente assegnando un nuovo valore al campo dipendente dell'elemento principale elimina anche l'elemento secondario precedente. 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 al 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 avvenga in una singola azione atomica, devi eseguire l'eliminazione in una transazione.

Nota:l'implementazione JDO si occupa di eliminare gli oggetti secondari dipendenti, non il 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 DO di App Engine. Si tratta di una limitazione che speriamo di rimuovere nelle versioni future del prodotto. Se devi fare riferimento a più tipi di oggetti tramite una classe base comune, ti consigliamo la stessa strategia utilizzata per implementare le relazioni non di proprietà: memorizza un riferimento Key. Ad esempio, se hai una classe base Recipe con specializzazioni Appetizer, Entree e Dessert e vuoi modellare il Recipe preferito di un Chef, puoi modellarlo 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 crei un'istanza di Entree e la assegni a Chef.favoriteRecipe, riceverai un UnsupportedOperationException quando tenti di rendere persistente l'oggetto Chef. Questo perché il tipo di runtime dell'oggetto, Entree, non corrisponde al tipo dichiarato del campo di 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.