Definizione delle classi di dati con JDO

Puoi utilizzare JDO per archiviare semplici oggetti di dati Java (a volte indicati come "Plain Old Java Objects" o "POJO") 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 i file XML .jdo al posto delle annotazioni Java. 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 permanente della classe rappresenta una proprietà dell'entità, con il nome della proprietà uguale a quello del campo (con distinzione tra maiuscole e minuscole).

Per dichiarare che una classe Java può essere archiviata e recuperata 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 permanente, assegnagli un'annotazione @Persistent:

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

// ...
    @Persistent
    private Date hireDate;

Per dichiarare un campo come non permanente (non viene archiviato nel datastore e non viene ripristinato quando viene recuperato l'oggetto), assegna 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, secondo la specifica JDO, consigliamo di annotare esplicitamente i campi come @Persistent o @NotPersistent per renderlo chiaro.

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

  • uno dei tipi di core supportati dal datastore
  • una raccolta (ad esempio java.util.List<...>) o un array di valori di un tipo di datastore principale
  • 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 dei quali utilizza un tipo di valore e annotazioni diversi. (Consulta la sezione Creazione dei dati: chiavi per ulteriori informazioni.) 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 primarie 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 permanente di un'altra classe potrebbe ignorare il miglioramento della classe JDO. In alternativa, puoi impostare altri corsi come @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 principali

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, in cui il valore serializzato dell'istanza viene memorizzato in un singolo valore di 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. Questa non è una classe di dati JDO, quindi 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 archiviare un'istanza di una classe Serializable come valore BLOB in una proprietà, dichiara un campo il cui tipo corrisponde alla classe e utilizza l'annotazione @Persistent(serialized = "true"):

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

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

Relazioni e oggetti secondari

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à influiscono su transazioni, gruppi di entità ed 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 campo myContactInfo con una nuova istanza ContactInfo e poi salva l'istanza Employee con pm.makePersistent(...), il datastore crea due entità. Uno è del tipo "ContactInfo", che rappresenta l'istanza ContactInfo. L'altro è del 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 principale 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; è una funzionalità utile, ma non obbligatoria per rendere un corso 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 quello 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. Puoi specificare nuovi nomi di campo utilizzando argomenti per l'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 vengono archiviate nella stessa entità degli altri campi, puoi utilizzare campi permanenti della classe incorporata nei filtri di query e nell'ordinamento 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 o meno utilizzando le annotazioni @Column.

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

Raccolte

Una proprietà datastore può avere più di un valore. In JDO, è rappresentato da un singolo campo con tipo Raccolta, in cui la raccolta fa parte di uno dei tipi di valori principali o di una classe Serializzabile. Sono supportati i seguenti tipi di raccolta:

  • 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 viene dichiarato come elenco, gli oggetti restituiti dal datastore hanno un valore ArrayList. Se un campo viene dichiarato come Set, il datastore restituisce un HashSet. Se un campo è dichiarato come SortedSet, il datastore restituisce un TreeSet.

Ad esempio, un campo di tipo List<String> viene archiviato come zero o più valori 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. Consulta Relazioni.

Le proprietà del data store 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à delle 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 null (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à datastore viene caricata in un oggetto e non ha una proprietà per uno dei campi dell'oggetto e il tipo di campo è un tipo a valore singolo null, il campo è impostato su null. Quando l'oggetto viene salvato di nuovo nel datastore, la proprietà null viene impostata nel datastore sul valore null. Se il campo non è di un tipo di valore null, il caricamento di un'entità senza la proprietà corrispondente genera un'eccezione. Questo non si verifica se l'entità è stata creata dalla stessa classe JDO utilizzata per ricreare l'istanza, ma può verificarsi 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 nullo.

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 va in overflow senza generare un'eccezione.

Puoi dichiarare una proprietà non indicizzata aggiungendo la riga

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

al di sopra della proprietà nella definizione della classe. Consulta la sezione Proprietà non indicizzate della documentazione principale per ulteriori informazioni su cosa significa non indicizzare una proprietà.

Ereditarietà

La creazione di classi di dati che utilizzano l'ereditarietà è una cosa naturale da fare e JDO supporta questa operazione. Prima di parlare di come funziona l'ereditarietà JDO su App Engine, ti consigliamo di leggere la documentazione di DataNucleus su questo argomento e poi di tornare. Completato? Ok. L'ereditarietà JDO su App Engine funziona come descritto nella documentazione di DataNucleus con alcune limitazioni aggiuntive. Discuteremo di queste restrizioni e poi forniremo 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 è potenzialmente molto inefficiente, quindi la "nuova tabella" strategia di ereditarietà non è supportata sulle classi di dati che non si trovano alla base delle loro gerarchie di ereditarietà.

La seconda è la "tabella-superclass" la strategia di ereditarietà consente di archiviare i dati per un oggetto dati nella "tabella" della sua superclasse. Sebbene questa strategia non presenti inefficienze intrinseche, al momento non è supportata. Potremmo rivedere questo aspetto nelle release future.

Ora 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 delle sue sottoclassi. L'entità datastore creata come risultato della chiamata a makePersistent() con un'istanza Employee ha due proprietà denominate "repartment" e "stipendio". L'entità datastore creata come risultato della chiamata a makePersistent() con un'istanza Intern avrà due proprietà denominate "reparto" 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à datastore creata come risultato della chiamata a makePersistent() con un'istanza FormerEmployee avrà tre proprietà denominate "repartment", "salary" e "lastDay". Nessun'entità di tipo "Impiegato" 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 delle relazioni con l'ereditarietà funziona purché i tipi dichiarati dei campi di relazione corrispondano ai tipi di runtime degli oggetti che stai assegnando a quei campi. Per ulteriori informazioni, consulta la sezione Relazioni polimorfiche.