Definizione delle classi di dati con JDO

Puoi utilizzare JDO per memorizzare oggetti dati Java semplici (a volte indicati come "POJO" o "Plain Old Java Object") nel datastore. Ogni oggetto reso permanente con PersistenceManager diventa un'entità nel datastore. Utilizzi le annotazioni per indicare a JDO come memorizzare e ricreare le istanze delle tue classi di dati.

Nota:le versioni precedenti di JDO utilizzano file XML .jdo anziché annotazioni Java. Questi funzionano ancora con JDO 2.3. Questa documentazione riguarda solo l'utilizzo delle annotazioni Java con le classi di dati.

Annotazioni di classi e campi

Ogni oggetto salvato da JDO diventa un'entità nel datastore di App Engine. Il tipo di entità è dedotto dal nome semplice della classe (le classi interne utilizzano il percorso $ senza il nome del pacchetto). Ogni campo persistente della classe rappresenta una proprietà dell'entità, con il nome della proprietà uguale al nome del campo (con la maiuscola conservata).

Per dichiarare una classe Java come archiviabile e recuperabile dal datastore con JDO, assegna alla classe un'annotazione @PersistenceCapable. Ad esempio:

import javax.jdo.annotations.PersistenceCapable;

@PersistenceCapable
public class Employee {
    // ...
}

I campi della classe di dati da archiviare nel datastore devono essere dichiarati come campi permanenti. Per dichiarare un campo come persistente, assegnagli un'annotazione @Persistent:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Per dichiarare un campo come non persistente (non viene archiviato nel datastore e non viene ripristinato quando viene recuperato l'oggetto), assegnagli un'annotazione @NotPersistent.

Suggerimento:JDO specifica che i campi di determinati tipi sono permanenti per impostazione predefinita se non vengono specificate le annotazioni @Persistent o @NotPersistent e che i campi di tutti gli altri tipi non sono permanenti per impostazione predefinita. Per una descrizione completa di questo comportamento, consulta la documentazione di DataNucleus. Poiché non tutti i tipi di valore principali del datastore di App Engine sono permanenti per impostazione predefinita in base alla specifica JDO, ti consigliamo di annotare esplicitamente i campi come @Persistent o @NotPersistent per chiarire.

Il tipo di un campo può essere uno dei seguenti. Questi sono descritti in dettaglio di seguito.

  • uno dei tipi principali supportati dal datastore
  • una raccolta (ad esempio un java.util.List<...>) o un array di valori di un tipo di datastore di base
  • un'istanza o una raccolta di istanze di una classe @PersistenceCapable
  • un'istanza o una raccolta di istanze di una classe Serializable
  • una classe incorporata, memorizzata come proprietà nell'entità

Una classe di dati deve avere un solo campo dedicato allo stoccaggio della chiave primaria dell'entità del data store corrispondente. Puoi scegliere tra quattro diversi tipi di campi chiave, ognuno con un tipo di valore e annotazioni diversi. Per ulteriori informazioni, consulta Creazione di dati: chiavi. Il tipo di campo chiave più flessibile è un oggetto Key che viene compilato automaticamente da JDO con un valore univoco in tutte le altre istanze della classe quando l'oggetto viene salvato nel datastore per la prima volta. Le chiavi principali di tipo Key richiedono un'annotazione @PrimaryKey e un'annotazione @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY):

Suggerimento:imposta tutti i campi permanenti su private o protected (o protetti dal pacchetto) e fornisci l'accesso pubblico solo tramite i metodi di accesso. L'accesso diretto a un campo persistente da un'altra classe potrebbe aggirare il miglioramento della classe JDO. In alternativa, puoi creare altri corsi @PersistenceAware. Per ulteriori informazioni, consulta la documentazione di DataNucleus.

import com.google.appengine.api.datastore.Key;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PrimaryKey;

// ...
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

Ecco un esempio di classe di dati:

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

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

    @Persistent
    private String firstName;

    @Persistent
    private String lastName;

    @Persistent
    private Date hireDate;

    public Employee(String firstName, String lastName, Date hireDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.hireDate = hireDate;
    }

    // Accessors for the fields. JDO doesn't use these, but your application does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

Tipi di valori fondamentali

Per rappresentare una proprietà contenente un singolo valore di un tipo di base, dichiara un campo del tipo Java e utilizza l'annotazione @Persistent:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Oggetti serializzabili

Un valore di campo può contenere un'istanza di una classe Serializable, memorizzando il valore serializzato dell'istanza in un singolo valore della proprietà di tipo Blob. Per indicare a JDO di serializzare il valore, il campo utilizza l'annotazione @Persistent(serialized=true). I valori blob non sono indicizzati e non possono essere utilizzati nei filtri delle query o negli ordini di ordinamento.

Ecco un esempio di una semplice classe Serializable che rappresenta un file, inclusi i contenuti del file, un nome file e un tipo MIME. Non si tratta di una classe di dati JDO, pertanto non sono presenti annotazioni di persistenza.

import java.io.Serializable;

public class DownloadableFile implements Serializable {
    private byte[] content;
    private String filename;
    private String mimeType;

    // ... accessors ...
}

Per memorizzare un'istanza di una classe Serializable come valore Blob in una proprietà, dichiara un campo di tipo corrispondente alla classe e utilizza l'annotazione @Persistent(serialized = "true"):

import javax.jdo.annotations.Persistent;
import DownloadableFile;

// ...
    @Persistent(serialized = "true")
    private DownloadableFile file;

Oggetti secondari e relazioni

Un valore di campo che è un'istanza di una classe @PersistenceCapable crea una relazione uno a uno di proprietà tra due oggetti. Un campo che è una raccolta di questi riferimenti crea una relazione one-to-many di proprietà.

Importante:le relazioni di proprietà hanno implicazioni per le transazioni, i gruppi di entità e le eliminazioni a cascata. Per ulteriori informazioni, consulta Transazioni e Relazioni.

Ecco un semplice esempio di una relazione uno a uno di proprietà tra un oggetto Employee e un oggetto 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;

    @Persistent
    private String city;

    @Persistent
    private String stateOrProvince;

    @Persistent
    private String zipCode;

    // ... accessors ...
}

Employee.java

import ContactInfo;
// ... imports ...

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

    @Persistent
    private ContactInfo myContactInfo;

    // ... accessors ...
}

In questo esempio, se l'app crea un'istanza Employee, compila il relativo campo myContactInfo con una nuova istanza ContactInfo e salva l'istanza Employee con pm.makePersistent(...), il datastore crea due entità. Uno è del tipo "ContactInfo" e rappresenta l'istanza ContactInfo. L'altra è di tipo "Employee". La chiave dell'entità ContactInfo ha la chiave dell'entità Employee come gruppo di entità principale.

Classi incorporate

Le classi incorporate ti consentono di modellare un valore di campo utilizzando una classe senza creare una nuova entità del data store e formare una relazione. I campi del valore dell'oggetto vengono archiviati direttamente nell'entità del datastore per l'oggetto contenitore.

Qualsiasi classe di dati @PersistenceCapable può essere utilizzata come oggetto incorporato in un'altra classe di dati. I campi @Persistent della classe sono incorporati nell'oggetto. Se fornisci il corso da incorporare all'annotazione @EmbeddedOnly, il corso potrà essere utilizzato solo come corso incorporato. La classe incorporata non ha bisogno di un campo chiave primaria perché non viene archiviata come entità separata.

Ecco un esempio di classe incorporata. Questo esempio rende la classe incorporata una classe interna della classe di dati che la utilizza. Questa operazione è utile, ma non necessaria per rendere una classe incorporabile.

import javax.jdo.annotations.Embedded;
import javax.jdo.annotations.EmbeddedOnly;
// ... imports ...

@PersistenceCapable
public class EmployeeContacts {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    Key key;
    @PersistenceCapable
    @EmbeddedOnly
    public static class ContactInfo {
        @Persistent
        private String streetAddress;

        @Persistent
        private String city;

        @Persistent
        private String stateOrProvince;

        @Persistent
        private String zipCode;

        // ... accessors ...
    }

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;
}

I campi di una classe incorporata vengono archiviati come proprietà nell'entità, utilizzando il nome di ogni campo e il nome della proprietà corrispondente. Se nell'oggetto è presente più di un campo di tipo classe incorporata, devi rinominare i campi di uno in modo che non entrino in conflitto con un altro. Specifica i nuovi nomi di campo utilizzando gli argomenti dell'annotazione @Embedded. Ad esempio:

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;

    @Persistent
    @Embedded(members = {
        @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")),
        @Persistent(name="city", columns=@Column(name="workCity")),
        @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")),
        @Persistent(name="zipCode", columns=@Column(name="workZipCode")),
    })
    private ContactInfo workContactInfo;

Analogamente, i campi dell'oggetto non devono utilizzare nomi in conflitto con i campi delle classi incorporate, a meno che i campi incorporati non vengano rinominati.

Poiché le proprietà permanenti della classe incorporata sono memorizzate nella stessa entità degli altri campi, puoi utilizzare i campi permanenti della classe incorporata nei filtri e negli ordini di ordinamento delle query JDOQL. Puoi fare riferimento al campo incorporato utilizzando il nome del campo esterno, un punto (.) e il nome del campo incorporato. Questo funziona indipendentemente dal fatto che i nomi delle proprietà per i campi incorporati siano stati modificati utilizzando le annotazioni @Column.

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

Raccolte

Una proprietà del data store può avere più di un valore. In JDO, è rappresentato da un singolo campo con un tipo Collection, in cui la raccolta è di uno dei tipi di valore di base o di una classe Serializable. Sono supportati i seguenti tipi di raccolte:

  • java.util.ArrayList<...>
  • java.util.HashSet<...>
  • java.util.LinkedHashSet<...>
  • java.util.LinkedList<...>
  • java.util.List<...>
  • java.util.Map<...>
  • java.util.Set<...>
  • java.util.SortedSet<...>
  • java.util.Stack<...>
  • java.util.TreeSet<...>
  • java.util.Vector<...>

Se un campo è dichiarato come elenco, gli oggetti restituiti dal data store hanno un valore ArrayList. Se un campo è dichiarato come Set, il datastore restituisce un HashSet. Se un campo è dichiarato come SortedSet, il data store restituisce un TreeSet.

Ad esempio, un campo di tipo List<String> viene archiviato come zero o più valori di stringa per la proprietà, uno per ogni valore in List.

import java.util.List;
// ... imports ...

// ...
    @Persistent
    List<String> favoriteFoods;

Una raccolta di oggetti secondari (delle classi @PersistenceCapable) crea più entità con una relazione one-to-many. Vedi Relazioni.

Le proprietà Datastore con più di un valore hanno un comportamento speciale per i filtri delle query e gli ordini di ordinamento. Per ulteriori informazioni, consulta la pagina Query Datastore.

Campi oggetto e proprietà entità

Il datastore di App Engine distingue tra un'entità senza una determinata proprietà e un'entità con un valore null per una proprietà. JDO non supporta questa distinzione: ogni campo di un oggetto ha un valore, eventualmente null. Se un campo con un tipo di valore nullable (diverso da un tipo integrato come int o boolean) è impostato su null, quando l'oggetto viene salvato, l'entità risultante avrà la proprietà impostata con un valore null.

Se un'entità del data store viene caricata in un oggetto e non ha una proprietà per uno dei campi dell'oggetto e il tipo del campo è un tipo con valore singolo nullable, il campo viene impostato su null. Quando l'oggetto viene salvato di nuovo nel data store, la proprietà null viene impostata nel data store sul valore null. Se il campo non è di tipo nullable, il caricamento di un'entità senza la proprietà corrispondente genera un'eccezione. Questo non accade se l'entità è stata creata dalla stessa classe JDO utilizzata per ricreare l'istanza, ma può accadere se la classe JDO cambia o se l'entità è stata creata utilizzando l'API di basso livello anziché JDO.

Se il tipo di un campo è una raccolta di un tipo di dati di base o di una classe Serializable e non sono presenti valori per la proprietà nell'entità, la raccolta vuota viene rappresentata nel data store impostando la proprietà su un singolo valore null. Se il tipo di campo è un tipo di array, viene assegnato un array di 0 elementi. Se l'oggetto viene caricato e non è presente alcun valore per la proprietà, al campo viene assegnata una raccolta vuota del tipo appropriato. Internamente, il datastore conosce la differenza tra una raccolta vuota e una raccolta contenente un valore null.

Se l'entità ha una proprietà senza un campo corrispondente nell'oggetto, questa proprietà non è accessibile dall'oggetto. Se l'oggetto viene salvato di nuovo nel data store, la proprietà aggiuntiva viene eliminata.

Se un'entità ha una proprietà il cui valore è di tipo diverso rispetto al campo corrispondente nell'oggetto, JDO tenta di eseguire il casting del valore al tipo di campo. Se il valore non può essere sottoposto a conversione al tipo di campo, JDO genera un'eccezione ClassCastException. Nel caso dei numeri (numeri interi lunghi e numeri a virgola mobile a doppia larghezza), il valore viene convertito, non eseguito il casting. Se il valore della proprietà numerica è maggiore del tipo di campo, la conversione genera un overflow senza generare un'eccezione.

Puoi dichiarare una proprietà non indicizzata aggiungendo la riga

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

sopra la proprietà nella definizione della classe. Per ulteriori informazioni sul significato di una proprietà non indicizzata, consulta la sezione Proprietà non indicizzate della documentazione principale.

Ereditarietà

La creazione di classi di dati che utilizzano l'eredità è una cosa naturale e JDO la supporta. Prima di parlare di come funziona l'eredità JDO su App Engine, ti consigliamo di leggere la documentazione di DataNucleus su questo argomento e di tornare qui. Completato? Ok. L'eredità JDO su App Engine funziona come descritto nella documentazione di DataNucleus, con alcune limitazioni aggiuntive. Discuteremo di queste limitazioni e poi daremo alcuni esempi concreti.

La strategia di ereditarietà "new-table" consente di suddividere i dati di un singolo oggetto dati in più "tabelle", ma poiché il datastore di App Engine non supporta le unioni, l'utilizzo di un oggetto dati con questa strategia di ereditarietà richiede una chiamata di procedura remota per ogni livello di ereditarietà. Questo approccio è potenzialmente molto inefficiente, pertanto la strategia di ereditarietà "new-table" non è supportata per le classi di dati che non si trovano alla radice delle relative gerarchie di ereditarietà.

In secondo luogo, la strategia di ereditarietà "tabella superclasse" consente di archiviare i dati di un oggetto dati nella "tabella" del relativo superclasse. Sebbene questa strategia non presenti inefficienze intrinseche, al momento non è supportata. Potremmo rivedere questo aspetto nelle release future.

Ecco le buone notizie: le strategie "subclass-table" e "complete-table" funzionano come descritto nella documentazione di DataNucleus e puoi anche utilizzare "new-table" per qualsiasi oggetto dati alla radice della gerarchia di ereditarietà. Vediamo un esempio:

Worker.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 Worker {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String department;
}

Employee.java

// ... imports ...

@PersistenceCapable
public class Employee extends Worker {
    @Persistent
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@PersistenceCapable
public class Intern extends Worker {
    @Persistent
    private Date internshipEndDate;
}

In questo esempio abbiamo aggiunto un'annotazione @Inheritance alla dichiarazione della classe Worker con l'attributo strategy> impostato su InheritanceStrategy.SUBCLASS_TABLE. Questo indica a JDO di archiviare tutti i campi permanenti di Worker nelle entità del datastore dei suoi sottoclassi. L'entità del datastore creata come risultato della chiamata a makePersistent() con un'istanza Employee ha due proprietà denominate "department" e "salary". L'entità del datastore creata come risultato della chiamata a makePersistent() con un'istanza Intern avrà due proprietà denominate "department" e "internshipEndDate". Il datastore non contiene entità di tipo "Lavoratore".

Ora rendiamo le cose un po' più interessanti. Supponiamo che, oltre a Employee e Intern, vogliamo anche una specializzazione di Employee che descriva i dipendenti che hanno lasciato l'azienda:

FormerEmployee.java

import java.util.Date;
// ... imports ...

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

In questo esempio, abbiamo aggiunto un'annotazione @Inheritance alla dichiarazione della classe FormerEmployee con l'attributo custom-strategy> impostato su "complete-table". Questo indica a JDO di memorizzare tutti i campi permanenti di FormerEmployee e dei relativi superclassi nelle entità del datastore corrispondenti alle istanze di FormerEmployee. L'entità del datastore creata come risultato della chiamata a makePersistent() con un'istanza FormerEmployee avrà tre proprietà denominate "department", "salary" e "lastDay". Nessun'entità di tipo"Employee" corrisponde a un FormerEmployee. Tuttavia, se chiami makePersistent() con un oggetto il cui tipo di runtime è Employee, crei un'entità di tipo "Dipendente".

La combinazione di relazioni con l'eredità funziona a condizione che i tipi dichiarati dei campi delle relazioni corrispondano ai tipi di runtime degli oggetti che assegni a questi campi. Per ulteriori informazioni, consulta la sezione sulle relazioni polimorfiche.