Relazioni di entità in JDO

Puoi modellare le relazioni tra oggetti permanenti utilizzando i campi dei tipi di oggetti. Una relazione tra oggetti permanenti può essere descritta come con proprietario, dove uno degli oggetti non può esistere senza l'altro, o come senza proprietà, dove entrambi gli oggetti possono esistere indipendentemente dalla loro relazione l'uno con l'altro. L'implementazione di App Engine dell'interfaccia JDO può creare un modello di relazioni one-to-one e di proprietà sia di proprietà che non gestite, sia unidirezionali che bidirezionali.

Le relazioni senza proprietario non sono supportate nella versione 1.0 del plug-in DataNucleus per App Engine, ma puoi gestire queste relazioni autonomamente archiviando le chiavi del datastore direttamente nei campi. App Engine crea automaticamente entità correlate nei gruppi di entità per supportare l'aggiornamento collettivo 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 relazioni senza proprietà con una sintassi naturale. La sezione Relazioni senza proprietà illustra come creare relazioni senza proprietario 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 individuali di proprietà

Puoi creare una relazione unidirezionale di proprietà uno a uno tra due oggetti permanenti utilizzando un campo il cui tipo corrisponde alla classe della classe correlata.

L'esempio seguente definisce una classe di dati ContactInfo e una classe di dati Employee, con una relazione one-to-one 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, di due tipi diversi. La relazione è rappresentata mediante una relazione di gruppo di entità: la chiave secondaria utilizza la chiave dell'entità padre come padre del gruppo di entità. Quando l'app accede all'oggetto figlio utilizzando il campo dell'oggetto padre, l'implementazione JDO esegue una query padre del gruppo di entità per ottenere l'oggetto secondario.

La classe figlio deve avere un campo chiave il cui tipo può contenere le informazioni sulla chiave padre: una chiave o un valore di chiave codificato come stringa. Consulta Creazione di dati: chiavi per informazioni sui tipi di campi chiave.

Puoi creare una relazione one-to-one bidirezionale utilizzando i campi di entrambe le classi, con un'annotazione nel campo della classe figlio per dichiarare che i campi rappresentano una relazione bidirezionale. Il campo della classe figlio deve avere un'annotazione @Persistent con l'argomento mappedBy = "...", dove il valore è il nome del campo nella classe padre. Se il campo su un oggetto è compilato, il campo di riferimento corrispondente sull'altro oggetto viene completato automaticamente.

ContactInfo.java

import Employee;

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

Gli oggetti figlio vengono caricati dal datastore quando vi si accede per la prima volta. Se non accedi all'oggetto figlio su un oggetto padre, l'entità per l'oggetto figlio non viene mai caricata. Se vuoi caricare il file secondario, puoi "toccarlo" prima di chiudere PersistenceManager (ad esempio, chiamando getContactInfo() nell'esempio riportato sopra) oppure aggiungere esplicitamente il campo secondario al gruppo di recupero predefinito in modo che venga recuperato e caricato insieme al file principale:

Employee.java

import ContactInfo;

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

Relazioni uno a molti

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 one-to-many è simile a una relazione one-to-one, con un campo nella classe padre che utilizza l'annotazione @Persistent(mappedBy = "..."), dove il valore è il nome del campo nella classe figlio:

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 di classi di dati: raccolte sono supportati per 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à figlio. Puoi eseguire query su una proprietà di una classe incorporata perché le classi incorporate archiviano le proprietà sull'entità padre. Consulta Definizione delle classi di dati: classi incorporate.)

In che modo le collezioni ordinate mantengono l'ordine

Le raccolte ordinate, come List<...>, conservano l'ordine degli oggetti quando viene salvato l'oggetto principale. JDO richiede che i database conservino questo ordine memorizzando la posizione di ciascun oggetto come proprietà dell'oggetto. App Engine la archivia come una proprietà dell'entità corrispondente, utilizzando un nome della proprietà uguale a quello del campo dell'entità padre seguito da _INTEGER_IDX. Le proprietà di posizione non sono efficaci. 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 conservare un ordine arbitrario in una raccolta, ma devi utilizzare un tipo di raccolta ordinata, puoi specificare un ordinamento basato sulle 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 (utilizzando 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 di Datastore.

Per maggiore efficienza, se possibile utilizza sempre una clausola di ordinamento esplicita per le relazioni one-to-many dei tipi di raccolte ordinate.

Relazioni senza proprietà

Oltre alle relazioni di proprietà, l'API JDO consente anche di gestire relazioni non di proprietà. Questa struttura 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 relazioni senza proprietà utilizzando una sintassi naturale, ma puoi comunque gestire queste relazioni utilizzando valori Key al posto delle istanze (o raccolte di istanze) degli oggetti modello. L'archiviazione di oggetti Key può essere paragonata alla modellazione di una "chiave esterna" arbitraria tra due oggetti. Il datastore non garantisce l'integrità referenziale con questi riferimenti chiave, ma l'utilizzo della chiave semplifica la modellazione (e quindi il recupero) di qualsiasi relazione tra due oggetti.

    Tuttavia, se scegli questa route, devi assicurarti che le chiavi siano del tipo appropriato. JDO e il compilatore non controllano automaticamente i tipi Key.
  • La versione 2.x del plug-in DataNucleus implementa relazioni senza proprietario utilizzando una sintassi naturale.

Suggerimento: in alcuni casi, potrebbe essere necessario 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. Quindi, ad esempio, se un oggetto padre riceve 0,75 scritture al secondo e un oggetto figlio 0,75 scritture al secondo, può avere senso modellare questa relazione come senza proprietà, in modo che sia l'oggetto principale sia quello figlio risiedono in gruppi di entità indipendenti.

Relazioni uno a uno senza proprietario

Supponiamo che tu voglia modellare una persona e un alimento, in cui una persona può avere solo un cibo preferito ma uno di questi non gli appartiene perché può essere il cibo preferito da un numero qualsiasi di persone. Questa sezione mostra 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 sono 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 su abilita 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é fornire a Person una chiave che rappresenti il suo piatto 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 uno a molti senza proprietario

Ora supponiamo di voler lasciare a una persona più cibi preferiti. Ancora una volta, non appartiene a una persona un alimento preferito perché può essere il cibo preferito di qualsiasi persona.

In JDO 2.3

In questo esempio, anziché assegnare a Person un membro di tipo Set<Food> per rappresentare i suoi cibi preferiti, assegniamo a Person 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 contenuta in Person.favoriteFoods non si trovano nello stesso gruppo di entità, devi impostare la configurazione JDO in modo da abilitare le transazioni cross-group (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> dove l'insieme rappresenta i suoi cibi preferiti dell'utente.

Person.java

// ... imports ...

@PersistenceCapable
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relazioni molti-a-molti

Possiamo modellare una relazione many-to-many gestendo 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 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 preferiti.

Quando crei un modello many-to-many 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 contenute in Person.favoriteFoods non si trovano nello stesso gruppo di entità e vuoi aggiornarle in una singola transazione, devi impostare la configurazione JDO in modo da abilitare le transazioni cross-group (XG).

Relazioni, gruppi di entità e transazioni

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

Considera l'esempio seguente utilizzando una relazione unidirezionale tra le classi Employee e ContactInfo precedenti:

    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à padre dell'entità ContactInfo. Allo stesso modo, 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à padre.

Tuttavia, tieni presente che la chiamata a pm.makePersistent() in questo esempio non utilizza una transazione. Senza una transazione esplicita, entrambe le entità vengono create usando azioni atomiche separate. In questo caso, è possibile che la creazione dell'entità Employee abbia esito positivo, ma la creazione dell'entità ContactInfo non vada a buon fine. Per assicurarti che entrambe le entità vengano create correttamente o che non venga creata nessuna delle due entità, 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 della relazione, App Engine non può "spostare" l'entità ContactInfo esistente nel gruppo di entità dell'entità Employee, poiché 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 su abilita 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.

Il salvataggio di un oggetto padre i cui oggetti figlio sono stati modificati salverà le modifiche agli oggetti figlio. È consigliabile consentire agli oggetti padre di mantenere la persistenza per tutti gli oggetti secondari correlati in questo modo e utilizzare le transazioni durante il salvataggio delle modifiche.

Figli dipendenti ed eliminazioni a cascata

Una relazione di proprietà può essere "dipendente", ovvero il figlio non può esistere senza il padre. Se una relazione è dipendente e un oggetto padre viene eliminato, vengono eliminati anche tutti gli oggetti secondari. La rottura di una relazione di proprietà e dipendente assegnando un nuovo valore al campo dipendente del livello padre comporta anche l'eliminazione del precedente elemento secondario. Puoi dichiarare che una relazione one-to-one di proprietà dipende aggiungendo dependent="true" all'annotazione Persistent del campo nell'oggetto padre che fa riferimento all'oggetto figlio:

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

Puoi dichiarare che una relazione one-to-many di proprietà è dipendente aggiungendo un'annotazione @Element(dependent = "true") al campo dell'oggetto padre che fa riferimento alla raccolta figlio:

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

Come per la creazione e l'aggiornamento degli oggetti, se è necessario che ogni eliminazione in un'eliminazione a cascata avvenga in una singola azione atomica, devi eseguirla in una transazione.

Nota: l'implementazione JDO elimina 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, le relazioni polimorfiche non sono ancora supportate nell'implementazione DO di App Engine. Questo è un limite che speriamo di rimuovere nelle future versioni del prodotto. Se devi fare riferimento a più tipi di oggetti tramite una classe base comune, ti consigliamo la stessa strategia utilizzata per implementare relazioni non di proprietà: archiviare un riferimento chiave. Ad esempio, se hai una classe base Recipe con le specializzazioni Appetizer, Entree e Dessert e vuoi creare il modello del Recipe preferito di una 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;
}

Sfortunatamente, se crei un'istanza Entree e lo assegni a Chef.favoriteRecipe, riceverai un UnsupportedOperationException quando provi a salvare in modo permanente l'oggetto Chef. Il motivo è che il tipo di runtime dell'oggetto, Entree, non corrisponde al tipo di campo di relazione dichiarato Recipe. La soluzione alternativa consiste nel cambiare 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 relazione, può fare riferimento a un oggetto di qualsiasi tipo. Lo svantaggio è che, come nel caso di una relazione senza proprietari, devi gestirla manualmente.