Définir des classes de données avec JDO

Vous pouvez utiliser JDO pour stocker des objets de données Java simples (parfois appelés "Plain Old Java Objects" ou "POJO") dans le magasin de données. Chaque objet rendu persistant à l'aide du gestionnaire PersistenceManager devient une entité du magasin de données. Vous utilisez des annotations pour indiquer à JDO comment stocker et recréer les instances de vos classes de données.

Remarque : Les versions antérieures de JDO utilisent des fichiers XML .jdo au lieu d'annotations Java. Ceux-ci fonctionnent toujours avec JDO 2.3. Cette documentation traite uniquement de l'utilisation des annotations Java avec des classes de données.

Annotations de classe et de champ

Chaque objet enregistré par JDO devient une entité dans le magasin de données App Engine. Le genre de l'entité découle du nom simple de la classe (les classes internes utilisent le chemin $ sans le nom du package). Chaque champ persistant de la classe représente une propriété de l'entité, dont le nom correspond au nom du champ (en respectant la distinction majuscules-minuscules).

Pour déclarer une classe Java comme stockable et récupérable à partir du magasin de données avec JDO, attribuez-lui une annotation @PersistenceCapable. Exemple :

import javax.jdo.annotations.PersistenceCapable;

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

Les champs de la classe de données à stocker dans le magasin de données doivent être déclarés en tant que champs persistants. Pour déclarer un champ comme persistant, attribuez-lui une annotation @Persistent :

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

// ...
    @Persistent
    private Date hireDate;

Pour déclarer un champ comme non persistant (champ non stocké dans le magasin de données et non restauré en cas de récupération de l'objet), attribuez-lui une annotation @NotPersistent.

Conseil : Dans JDO, les champs de certains types sont persistants par défaut si ni l'annotation @Persistent, ni l'annotation @NotPersistent ne sont spécifiées, tandis que les autres types de champs ne sont pas persistants par défaut. Consultez la documentation de DataNucleus pour obtenir une description détaillée de ce comportement. Comme certains types de valeurs de base de l'App Engine Datastore ne sont pas persistants par défaut, conformément à la spécification JDO, il est recommandé d'annoter explicitement les champs sous la forme de @Persistent ou @NotPersistent pour le préciser.

Les types de champs possibles sont répertoriés ci-dessous, et sont décrits en détail dans la suite de cette page :

  • L'un des types de base pris en charge par le magasin de données
  • Une collection (par exemple, java.util.List<...>) ou un tableau de valeurs d'un type de magasin de données de base
  • Une instance ou une collection d'instances d'une classe @PersistenceCapable
  • Une instance ou une collection d'instances d'une classe Serializable
  • Une classe encapsulée, stockée sous forme de propriétés dans l'entité

Une classe de données doit comporter un seul champ dédié au stockage de la clé primaire de l'entité correspondante dans le magasin de données. Vous pouvez choisir parmi quatre catégories de champs de clé, chacune de ces catégories utilisant un type de valeur et des annotations distincts. (Pour plus d'informations, consultez la page Créer des données : clés.) Le type de champ de clé le plus souple correspond à un objet Key qui est automatiquement renseigné par JDO avec une valeur unique pour l'ensemble des instances de la classe lorsque l'objet est enregistré dans le datastore pour la première fois. Les clés primaires de type Key requièrent une annotation @PrimaryKey, ainsi qu'une annotation @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) :

Conseil : Déclarez tous vos champs persistants private ou protected (ou "package protected"), et n'offrez un accès public que par l'intermédiaire de méthodes accesseur. L'accès direct à un champ persistant à partir d'une autre classe peut contourner l'enrichissement du code des classes JDO. Une autre solution consiste à déclarer les autres classes comme @PersistenceAware. Pour plus d'informations, consultez la documentation de 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;

Voici un exemple de classe de données :

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;
    }
}

Types de valeurs de base

Pour représenter une propriété contenant une valeur unique de l'un des types de base, déclarez un champ de type Java, puis utilisez l'annotation @Persistent :

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

// ...
    @Persistent
    private Date hireDate;

Objets Serializable

Une valeur de champ peut contenir une instance d'une classe Serializable en stockant la valeur sérialisée de l'instance dans une valeur de propriété unique de type Blob. Pour indiquer à JDO de sérialiser la valeur, le champ utilise l'annotation @Persistent(serialized=true). Les valeurs Blob ne sont ni indexées, ni utilisables dans les filtres de requête ou dans les ordres de tri.

Voici un exemple de classe Serializable simple qui représente un fichier en incluant le contenu du fichier, un nom de fichier et un type MIME. Puisqu'il ne s'agit pas d'une classe de données JDO, il n'existe aucune annotation de persistance.

import java.io.Serializable;

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

    // ... accessors ...
}

Pour stocker une instance d'une classe Serializable sous la forme d'une valeur Blob dans une propriété, déclarez un champ dont le type correspond à la classe, puis utilisez l'annotation @Persistent(serialized = "true") :

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

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

Objets enfants et relations

Une valeur de champ correspondant à une instance d'une classe @PersistenceCapable crée une relation d'appartenance un-à-un entre deux objets. Un champ qui constitue une collection de références de ce type crée une relation d'appartenance un à plusieurs.

Important : Les relations d'appartenance ont des répercussions sur les transactions, les groupes d'entités et les suppressions en cascade. Pour plus d'informations, consultez les pages Transactions et Relations.

Voici un exemple simple de relation d'appartenance un à un entre un objet Employee et un objet 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 ...
}

Dans cet exemple, si l'application crée une instance Employee, renseigne son champ myContactInfo avec une nouvelle instance ContactInfo, puis enregistre l'instance Employee avec pm.makePersistent(...), le datastore crée deux entités. L'une est du genre "ContactInfo" et représente l'instance ContactInfo. L'autre est du genre "Employee". La clé de l'entité ContactInfo reçoit la clé de l'entité Employee en guise de parent de groupe d'entités.

Classes encapsulées

Les classes encapsulées vous permettent de modéliser une valeur de champ à l'aide d'une classe sans créer d'entité dans le magasin de données, ni établir de relation. Les champs de la valeur d'objet sont directement stockés dans l'entité du magasin de données pour l'objet conteneur.

Toute classe de données @PersistenceCapable est utilisable en tant qu'objet encapsulé dans une autre classe de données. Les champs de la classe @Persistent sont encapsulés dans l'objet. Si vous attribuez l'annotation @EmbeddedOnly à la classe à encapsuler, cette dernière est uniquement utilisable en tant que classe encapsulée. La classe encapsulée n'a pas besoin d'un champ de clé primaire, car elle n'est pas stockée sous la forme d'une entité distincte.

Voici un exemple de classe encapsulée. Cet exemple fait de la classe encapsulée une classe interne de la classe de données qui l'utilise ; bien que non obligatoire, cette opération se révèle utile pour définir une classe comme encapsulable.

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;
}

Les champs d'une classe encapsulée sont stockés sous forme de propriétés dans l'entité, à l'aide du nom de chaque champ et du nom de la propriété correspondante. Si plusieurs champs de l'objet présentent un type correspondant à une classe encapsulée, vous devez renommer ces champs pour qu'ils n'entrent pas en conflit les uns avec les autres. Vous spécifiez de nouveaux noms de champ au moyen d'arguments de l'annotation @Embedded. Exemple :

    @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;

De la même façon, les champs de l'objet ne doivent pas utiliser de noms qui entrent en conflit avec les champs des classes encapsulées, à moins que les champs encapsulés ne soient renommés.

Étant donné que les propriétés persistantes de la classe encapsulée sont stockées dans la même entité que les autres champs, vous pouvez utiliser les champs persistants de la classe encapsulée dans les filtres de requête et les ordres de tri JDOQL. Vous pouvez faire référence au champ encapsulé en utilisant le nom du champ externe, un point (.), puis le nom du champ encapsulé. Cette procédure fonctionne, que les noms de propriété des champs encapsulés aient ou non été modifiés à l'aide d'annotations @Column.

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

Collections

Une propriété de magasin de données peut prendre plusieurs valeurs. Dans JDO, cela est représenté par un champ unique de type Collection, où la collection est de l'un des types de valeurs de base ou une classe Serializable. Les types de collections pris en charge sont les suivants :

  • 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<...>

Si un champ est déclaré en tant que List, les objets renvoyés par le magasin de données présentent une valeur ArrayList. Si un champ est déclaré en tant que Set, le magasin de données renvoie une valeur HashSet. Si un champ est déclaré en tant que SortedSet, le magasin de données renvoie une valeur TreeSet.

Par exemple, un champ de type List<String> est stocké sous la forme d'un nombre de valeurs de chaîne égal ou supérieur à zéro pour la propriété, à raison d'une valeur de chaîne pour chaque valeur de la List.

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

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

Une collection d'objets enfants (de classes @PersistenceCapable) crée plusieurs entités présentant une relation un-à-plusieurs. Consultez la page relative aux Relations.

Les propriétés de magasin de données à valeurs multiples présentent un comportement spécial pour les filtres de requête et les ordres de tri. Pour plus d'informations, consultez la page Requêtes de magasin de données.

Champs d'objet et propriétés d'entité

L'App Engine Datastore fait la distinction entre une entité dépourvue d'une propriété spécifique et une entité dotée d'une valeur null pour une propriété. JDO n'établit pas cette distinction : chaque champ d'un objet comporte une valeur, qui peut être null. Si un champ dont le type accepte les valeurs null (différent d'un type intégré tel que int ou boolean) est défini sur null, la propriété de l'entité résultante est définie sur une valeur null lorsque l'objet est enregistré.

Si une entité du datastore est chargée dans un objet, qu'elle n'est pas dotée d'une propriété pour l'un des champs de l'objet, et que le champ est de type valeur unique acceptant les valeurs null, ce champ est défini sur null. Lorsque l'objet est réenregistré dans le magasin de données, la propriété null est définie sur la valeur null dans ce dernier. Si le type du champ n'autorise pas les valeurs null, le chargement d'une entité sans la propriété correspondante lève une exception. Cette situation ne se produit pas si l'entité a été créée à partir de la même classe JDO que celle utilisée pour recréer l'instance. En revanche, elle risque de survenir si la classe JDO change, ou si l'entité a été créée à l'aide de l'API de bas niveau plutôt qu'avec JDO.

Si le type d'un champ correspond à une collection d'un type de données de base ou d'une classe Serializable, et qu'aucune valeur n'a été définie pour la propriété dans l'entité, la collection vide est représentée dans le magasin de données en définissant la propriété à une valeur null unique. Si le champ est de type tableau, un tableau de 0 élément lui est attribué. Si l'objet est chargé et qu'il n'existe aucune valeur pour la propriété, le champ est défini sur une collection vide du type approprié. En interne, le magasin de données fait la différence entre une collection vide et une collection contenant une valeur null.

Si l'entité comporte une propriété sans champ correspondant dans l'objet, cette propriété est inaccessible à partir de l'objet. La propriété inutile est supprimée si l'objet est réenregistré dans le magasin de données.

Si une entité comporte une propriété dont le type de valeur diffère de celui du champ correspondant dans l'objet, JDO tente de convertir le type de la valeur en fonction du type du champ. Si le type de la valeur est impossible à convertir vers le type du champ, JDO lève une exception ClassCastException. Dans le cas des valeurs numériques (entiers longs et nombres à virgule flottante en double précision), la valeur fait l'objet d'une conversion simple, et non d'une conversion de type. Si la valeur de propriété numérique est plus importante que celle acceptée par le type du champ, la conversion entraîne un dépassement sans lever d'exception.

Vous pouvez déclarer une propriété non indexée en ajoutant la ligne

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

au-dessus de la propriété dans la définition de classe. Pour plus d'informations sur les implications des propriétés non indexées, consultez la section Propriétés non indexées de la documentation principale.

Héritage

La création de classes de données utilisant des stratégies d'héritage est une opération courante. JDO prend donc en charge cette fonctionnalité. Avant d'aborder le fonctionnement de l'héritage JDO sur App Engine, nous vous recommandons de consulter la documentation DataNucleus sur ce sujet, puis de reprendre votre lecture de cette documentation. C'est fait ? D'accord. L'héritage JDO dans App Engine fonctionne tel que décrit dans la documentation DataNucleus, mais fait l'objet de quelques restrictions supplémentaires. Cette section spécifie ces restrictions, puis en fournit quelques exemples concrets.

La stratégie d'héritage par "nouvelle table" ("new-table") vous permet de fractionner les données d'un même objet de données dans différentes "tables". Toutefois, du fait que le magasin de données d'App Engine n'est pas compatible avec les jointures, l'utilisation d'un objet de données avec cette stratégie d'héritage nécessite un appel de procédure à distance pour chaque niveau d'héritage. C'est un fonctionnement potentiellement très inefficace. Par conséquent, la stratégie d'héritage par nouvelle table n'est pas compatible avec les classes de données qui ne sont pas à la racine de leurs hiérarchies d'héritage.

Par ailleurs, la stratégie d'héritage par "table de super-classe" ("superclass-table") vous permet de stocker les données d'un objet de données dans la "table" de sa super-classe. Bien que cette stratégie ne présente pas de risque d'inefficacité, elle n'est pas prise en charge à ce jour. Cet aspect sera éventuellement modifié dans les versions ultérieures.

Maintenant, voici la bonne nouvelle : les stratégies par "table de sous-classe" ("subclass-table") et par "table complète" ("complete-table") fonctionnent comme décrit dans la documentation de DataNucleus. Vous pouvez également utiliser une stratégie par nouvelle table pour tout objet de données situé à la racine de sa hiérarchie d'héritage. Voyons un exemple :

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;
}

Dans cet exemple, nous avons ajouté une annotation @Inheritance à la déclaration de classe Worker en définissant son attribut strategy> sur InheritanceStrategy.SUBCLASS_TABLE. Cette opération indique à JDO de stocker tous les champs persistants de la classe Worker dans les entités de datastore de ses sous-classes. L'entité du datastore créée à la suite de l'appel de makePersistent() sur une instance Employee possède deux propriétés nommées "department" et "salary". L'entité du datastore créée à la suite de l'appel de makePersistent() sur une instance Intern possède deux propriétés nommées "department" et "internshipEndDate". Le magasin de données ne contient aucune entité de genre "Worker".

Passons maintenant à un aspect un peu plus intéressant. Supposons qu'en plus des instances Employee et Intern, nous souhaitions également disposer d'une spécialisation de l'instance Employee décrivant les employés qui ont quitté l'entreprise :

FormerEmployee.java

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

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

Dans cet exemple, nous avons ajouté une annotation @Inheritance à la déclaration de classe FormerEmployee, avec son attribut custom-strategy> sur "complete-table". Cette opération indique à JPO de stocker tous les champs persistants de la classe FormerEmployee et de ses super-classes dans les entités du datastore correspondant aux instances FormerEmployee. L'entité du datastore créée à la suite de l'appel de makePersistent() avec une instance FormerEmployee possède trois propriétés nommées "department", "salary" et "lastDay". Aucune entité de genre "Employee" ne correspond à un FormerEmployee. Toutefois, si vous appelez makePersistent() avec un objet dont le type d'exécution est Employee, vous créez une entité de genre "Employee".

L'utilisation combinée de relations et de stratégies d'héritage fonctionne aussi longtemps que les types déclarés de vos champs de relation correspondent aux types d'exécution des objets que vous attribuez à ces champs. Pour plus d'informations, veuillez vous reporter à la section sur les relations polymorphes.