Definizione delle classi di dati con JDO

Puoi utilizzare JDO per archiviare oggetti di dati Java semplici (a volte chiamati "Plain Old Java Object" o "POJO") nel datastore. Ogni oggetto reso persistente con PersistenceManager diventa un'entità nel datastore. Utilizzi le annotazioni per indicare a JDO come archiviare e ricreare le istanze delle 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 classe e campo

Ogni oggetto salvato da JDO diventa un'entità nel datastore di App Engine. Il tipo di entità deriva 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 distinzione tra maiuscole e minuscole).

Per dichiarare una classe Java in grado di 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 persistente, aggiungi l'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 l'oggetto viene recuperato), aggiungi un'annotazione @NotPersistent.

Suggerimento:JDO specifica che i campi di determinati tipi sono persistenti per impostazione predefinita se non vengono specificate le annotazioni @Persistent e @NotPersistent, mentre i campi di tutti gli altri tipi non sono persistenti per impostazione predefinita. Per una descrizione completa di questo comportamento, consulta la documentazione di DataNucleus. Poiché non tutti i tipi di valori principali di App Engine Datastore sono persistenti per impostazione predefinita in base alla specifica JDO, ti 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 principali supportati dal datastore
  • una raccolta (ad esempio un 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 serializzabile
  • una classe incorporata, memorizzata come proprietà dell'entità

Una classe di dati deve avere un solo campo dedicato all'archiviazione della chiave primaria dell'entità del datastore corrispondente. Puoi scegliere tra quattro diversi tipi di campi chiave, ognuno con un tipo di valore e annotazioni diversi. Per saperne di più, consulta Creazione di dati: chiavi. Il tipo più flessibile di campo chiave è 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 per la prima volta nel datastore. Le chiavi primarie di tipo Key richiedono un'annotazione @PrimaryKey e un'annotazione @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY):

Suggerimento:rendi tutti i campi persistenti 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 ignorare il miglioramento della classe JDO. In alternativa, puoi rendere altre classi @PersistenceAware. Per saperne di più, 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 una classe di dati di esempio:

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 serializzabile, memorizzando il valore serializzato dell'istanza 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 di query o negli ordinamenti.

Di seguito è riportato un esempio di una semplice classe serializzabile che rappresenta un file, inclusi i contenuti, 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 memorizzare un'istanza di una classe serializzabile come valore Blob in una proprietà, dichiara un campo il cui tipo è la classe e utilizza l'annotazione @Persistent(serialized = "true"):

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

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

Oggetti e relazioni 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à hanno implicazioni per transazioni, gruppi di entità ed eliminazioni a cascata. Per ulteriori informazioni, consulta Transazioni e Relazioni.

Ecco un semplice esempio di 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 è di tipo "ContactInfo" e rappresenta l'istanza ContactInfo. L'altro è 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à datastore e formare una relazione. I campi del valore dell'oggetto vengono archiviati direttamente nell'entità 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 assegni alla classe l'annotazione @EmbeddedOnly, la classe può essere utilizzata solo come classe incorporata. 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à dell'entità, utilizzando il nome di ogni campo e il nome della proprietà corrispondente. Se hai più di un campo nell'oggetto il cui tipo è una classe incorporata, devi rinominare i campi di uno in modo che non siano in conflitto con un altro. Specifichi i nuovi nomi dei campi 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;

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

Poiché le proprietà persistenti della classe incorporata vengono archiviate nella stessa entità degli altri campi, puoi utilizzare i campi persistenti 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. 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 datastore può avere più di un valore. In JDO, questo è rappresentato da un singolo campo di tipo Collection, in cui la raccolta è di uno dei tipi di valori di base 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 viene dichiarato come SortedSet, il datastore restituisce un TreeSet.

Ad esempio, un campo di tipo List<String> viene memorizzato 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 (di classi @PersistenceCapable) crea più entità con una relazione one-to-many. Vedi Relazioni.

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

Campi oggetto e proprietà dell'entità

Il datastore 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, possibilmente 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, la proprietà dell'entità risultante viene 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 del campo è un tipo a valore singolo nullable, il campo viene 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 Nullable, il caricamento di un'entità senza la proprietà corrispondente genera un'eccezione. Ciò 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 una classe serializzabile e non sono presenti valori per la proprietà nell'entità, la raccolta vuota viene rappresentata nel datastore impostando la proprietà su un singolo valore null. Se il tipo del 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 nuovamente nel datastore, la proprietà aggiuntiva viene eliminata.

Se un'entità ha una proprietà il cui valore è di un tipo diverso rispetto al campo corrispondente nell'oggetto, JDO tenta di eseguire il cast del valore al tipo di campo. Se il valore non può essere convertito nel tipo di campo, JDO genera un'eccezione ClassCastException. Nel caso di numeri (interi lunghi e numeri in virgola mobile a doppia larghezza), il valore viene convertito, non eseguito il cast. 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 su cosa significa che una proprietà non è indicizzata, consulta la sezione Proprietà non indicizzate della documentazione principale.

Ereditarietà

La creazione di classi di dati che utilizzano l'ereditarietà è una cosa naturale da fare e JDO la supporta. 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 tornare qui. Completato? Ok. L'ereditarietà JDO su App Engine funziona come descritto nella documentazione di DataNucleus con alcune limitazioni aggiuntive. Esamineremo queste limitazioni e poi forniremo alcuni esempi concreti.

La strategia di ereditarietà "new-table" consente di dividere i dati di un singolo oggetto di dati in più "tabelle", ma poiché App Engine Datastore non supporta i join, l'operazione su un oggetto di 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 nelle classi di dati che non si trovano alla radice delle gerarchie di ereditarietà.

In secondo luogo, la strategia di ereditarietà "superclass-table" consente di archiviare i dati di un oggetto dati nella "tabella" della relativa superclasse. Sebbene non ci siano inefficienze intrinseche in questa strategia, al momento non è supportata. Potremmo rivedere questa funzionalità nelle versioni future.

Ora la buona notizia: le strategie "subclass-table" e "complete-table" funzionano come descritto nella documentazione di DataNucleus e puoi utilizzare anche "new-table" per qualsiasi oggetto dati che si trova 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. In questo modo, JDO memorizza tutti i campi persistenti di Worker nelle entità datastore delle relative sottoclassi. L'entità datastore creata in seguito alla chiamata di makePersistent() con un'istanza Employee ha due proprietà denominate "department" e "salary". L'entità datastore creata in seguito alla chiamata di makePersistent() con un'istanza Intern avrà due proprietà denominate "department" e "internshipEndDate". Il datastore non contiene entità di tipo "Worker".

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". In questo modo, JDO memorizza tutti i campi persistenti di FormerEmployee e delle relative superclassi in entità datastore corrispondenti alle istanze FormerEmployee. L'entità datastore creata in seguito alla chiamata di makePersistent() con un'istanza FormerEmployee avrà tre proprietà denominate "department", "salary" e "lastDay". Nessuna entità di tipo "Dipendente" corrisponde a un FormerEmployee. Tuttavia, se chiami makePersistent() con un oggetto il cui tipo di runtime è Employee, crei un'entità di tipo "Employee".

La combinazione di relazioni e ereditarietà funziona a condizione che i tipi dichiarati dei campi di relazione corrispondano ai tipi di runtime degli oggetti che stai assegnando a questi campi. Per saperne di più, consulta la sezione Relazioni polimorfiche.