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 des 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 indiquer qu'une classe Java peut être stockée et extraite du magasin de données à l'aide de JDO, associez à cette classe 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 (il n'est pas stocké dans le magasin de données et n'est pas restauré lors de l'extraction de l'objet), attribuez-lui une annotation @NotPersistent.

Conseil : Selon la spécification JDO, certains types de champs sont par défaut persistants en l'absence d'annotation @Persistent ou @NotPersistent, tandis que tous 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 du datastore App Engine ne sont pas persistants par défaut, conformément à la spécification JDO, il est recommandé d'annoter explicitement les champs sous la forme @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 (telle qu'un objet java.util.List<...>) ou un tableau de valeurs de l'un des types de datastore 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 flexible est un objet Key qui est automatiquement renseigné par JDO avec une valeur unique pour toutes les autres instances de la classe lorsque l'objet est enregistré pour la première fois dans le magasin de données. Les clés primaires de type Key nécessitent une annotation @PrimaryKey et une annotation @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) :

Conseil : Faites en sorte que tous vos champs persistants soient privés (private) ou protégés (protected) (ou protégés par un package), et n'autorisez l'accès public qu'à travers des méthodes d'accès. L'accès direct à un champ persistant à partir d'une autre classe peut contourner l'enrichissement du code des classes JDO. Sinon, vous pouvez rendre d'autres classes @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 d'un type de base, déclarez un champ du type Java correspondant et 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 en tant que valeur Blob dans une propriété, déclarez un champ dont le type est la classe et 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 qui est 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 de classe Employee, renseigne son champ myContactInfo avec une nouvelle instance de classe ContactInfo, puis enregistre l'instance Employee avec pm.makePersistent(...), le magasin de données 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 possède la clé de l'entité Employee comme parent du 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 peut être utilisée en tant qu'objet encapsulé dans une autre classe de données. Les champs @Persistent de la classe sont encapsulés dans l'objet. Si vous attribuez à la classe à encapsuler l'annotation @EmbeddedOnly, cette classe ne peut être utilisée que comme classe incorporé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 champs à l'aide 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é. Cela fonctionne, que les noms de propriété des champs encapsulés aient été ou non 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 dont le type est List<String> est stocké sous forme de valeurs de type chaîne (zéro ou plusieurs) pour la propriété, une pour chaque valeur figurant dans la List.

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

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

Une collection d'objets enfants (de classes @PersistenceCapable) crée plusieurs entités avec une relation de type un à plusieurs. Pour plus d'informations, consultez la page sur les 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é

Le magasin de données d'App Engine fait la distinction entre une entité dans laquelle une propriété donnée est absente et une entité possédant une valeur null pour cette propriété. JDO n'accepte pas cette distinction : chaque champ d'un objet possède une valeur, éventuellement null. Si un champ possédant un type de valeur pouvant être vide (autre qu'un type intégré tel que int ou boolean) est défini à la valeur null, à l'enregistrement de l'objet, l'entité résultante possédera la propriété définie à une valeur null.

Si une entité du magasin de données est chargée dans un objet sans posséder de propriété pour l'un des champs de cet objet, et que ce champ est par ailleurs d'un type à valeur unique pouvant être vide, le champ est alors défini à la valeur null. Lorsque l'objet est sauvegardé dans le magasin de données, la propriété null est alors définie dans le magasin de données à la valeur null. 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 la classe Worker, avec un attribut strategy> défini sur InheritanceStrategy.SUBCLASS_TABLE. Cela indique à JDO de stocker tous les champs persistants de la classe Worker dans les entités de datastore correspondant à ses sous-classes. L'entité du magasin de données 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 magasin de données 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 classes Employee et Intern, nous souhaitons également disposer d'une spécialisation de la classe Employee représentant les employés qui ont quitté la société :

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 la classe FormerEmployee, avec un attribut custom-strategy> défini à "complete-table". Cela indique à JDO de stocker tous les champs persistants de la classe FormerEmployee et de ses super-classes dans les entités du magasin de données correspondant aux instances FormerEmployee. L'entité du magasin de données créée à la suite de l'appel de makePersistent() sur 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.

Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java