Relazioni di entità in JDO

Puoi modellare le relazioni tra oggetti permanenti utilizzando i campi 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 relazioni one-to-one e one-to-many sia di proprietà che non possedute, sia unidirezionali che bidirezionali.

Le relazioni senza proprietà non sono supportate nella versione 1.0 di DataNucleus plug-in per App Engine, ma puoi gestire queste relazioni autonomamente per l'archiviazione diretta delle 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 i file senza proprietà relazioni con una sintassi naturale. La Nella sezione Relazioni senza proprietario viene illustrato come per 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 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 viene rappresentata utilizzando relazione di gruppo di entità: la chiave dell'asset secondario utilizza la chiave dell'elemento padre come entità principale del gruppo. 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 principale. Se il campo di un oggetto è compilato, il codice corrispondente sull'altro oggetto viene compilato automaticamente.

ContactInfo.java

import Employee;

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

Gli oggetti figlio vengono caricati dal datastore quando si accede al datastore per la prima volta. Se non accedi all'oggetto figlio su un oggetto padre, l'entità dell'oggetto secondario non viene mai caricato. 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 one-to-many è simile a una relazione one-to-one, con una sulla classe padre utilizzando l'annotazione @Persistent(mappedBy = "..."), dove il valore è il nome dell'oggetto campo sulla 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 Classi di dati: le raccolte sono supportate 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à figlio. Puoi eseguire query su una proprietà di una classe incorporata perché le classi incorporate memorizzano le proprietà nell'entità principale. 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 mantengano questo ordine archiviando la posizione di ogni oggetto come proprietà dell'oggetto. App Engine lo archivia come una proprietà dell'entità corrispondente, utilizzando un nome della proprietà uguale al nome del campo dell'elemento principale seguito da _INTEGER_IDX. Le proprietà di posizione non sono efficienti. Se viene aggiunto, rimosso o spostato nella raccolta, da tutte le entità successive al luogo modificato nella raccolta devono essere aggiornati. Questa operazione può essere lenta e soggetta a errori se non viene eseguita in una transazione.

Se non è necessario preservare un ordine arbitrario in una raccolta, ma per utilizzare un tipo di raccolta ordinato, puoi specificare un ordine basato su delle proprietà degli elementi utilizzando un'annotazione, un'estensione del JDO 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 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; vedi il Pagina Indici di Datastore per ulteriori informazioni.

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

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. L'archiviazione degli oggetti chiave è come la modellazione di un modello "chiave esterna" tra due oggetti. Il datastore non garantisce integrità referenziale con questi riferimenti chiave, ma l'uso della chiave rende molto facile modellare (e poi recuperare) qualsiasi relazione tra due oggetti.

    Tuttavia, se scegli questa opzione, devi assicurarti che le chiavi siano del 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 ritenere necessario creare modelli una relazione di proprietà, come se fosse senza proprietario. 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 senza proprietà

Supponiamo che tu voglia modellare una persona e un 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 fare quello.

In JDO 2.3

In questo esempio, forniamo a Person un membro di tipo Key, dove Key è l'identificatore univoco di un Food oggetto. Se un'istanza di Person e l'istanza di Food a cui si fa riferimento Person.favoriteFood non si trovano nello stesso gruppo di entità, tu non può aggiornare la persona e il suo cibo preferito in un unico posto transazione, a meno che la configurazione JDO non sia impostata su attiva 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, invece di fornire a Person una chiave che rappresenti il loro 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 senza proprietà

Ora supponiamo di voler lasciare che una persona abbia 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 Persona un membro di tipo Set<Food> in cui il set rappresenta i 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 many-to-many

Possiamo modellare una relazione many-to-many mantenendo raccolte di chiavi da entrambi i lati della relazione. Modifichiamo l'esempio per Food tiene traccia delle persone che lo considerano preferita:

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à datastore, tutti gli altri oggetti che possono essere raggiunti tramite relazioni e che devono (sono nuovi o sono stati modificati dopo l'ultimo caricamento), vengono salvate automaticamente. Ciò ha importanti implicazioni per transazioni ed entità gruppi.

Considera l'esempio seguente in cui viene utilizzata 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 nuovo, App Engine crea due nuove entità nello stesso gruppo di entità, utilizzando l'entità Employee come padre di Entità ContactInfo. Analogamente, se l'oggetto Employee è già stato salvato e l'oggetto ContactInfo correlato è nuovo, App Engine crea l'entità ContactInfo utilizzando il modello Entità Employee come principale.

Nota, tuttavia, che la chiamata a pm.makePersistent() in questo nel caso in cui non venga utilizzata una transazione. Senza una transazione esplicita, entrambe le entità vengono create utilizzando azioni atomiche distinte. In questo caso, possibili per la creazione dell'entità Employee, ma la creazione l'entità ContactInfo in errore. 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 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 assegnate 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'eccezione JDOFatalUser.

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 dipendenti e Eliminazioni a cascata

Una relazione di proprietà può essere "dipendente", vale a dire che il figlio non può esistere senza quello principale. Se una relazione è dipendente e un oggetto padre è vengono eliminati, vengono eliminati anche tutti gli oggetti figlio. Rottura di una persona dipendente di relazione assegnando un nuovo valore al campo dipendente sull'elemento padre anch'esso il 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 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 si occupa di eliminare oggetti secondari dipendenti, non il datastore. Se elimini un'entità principale 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 relazioni polimorfiche non sono ancora supportate in App Engine FAI l'implementazione. 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 base comune consigliamo la stessa strategia utilizzata per implementare relazioni: archivia un riferimento chiave. Ad esempio, se disponi di una Classe base Recipe con Appetizer, Entree, e le specializzazioni Dessert, oltre a voler modellare il modello Recipe di Chef, puoi modellarlo come segue:

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 un Entree e lo assegni a Chef.favoriteRecipe riceverai un UnsupportedOperationException quando provi a mantenere il Chef oggetto. perché il tipo di runtime dell'oggetto, Entree, non corrisponde al campo del tipo di relazione dichiarato, Recipe. Per risolvere il problema, cambia il tipo 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 con un modello non di proprietà devi gestirla manualmente.