Relations entre entités dans JDO

Vous pouvez modéliser les relations entre objets persistants à l'aide de champs des types d'objets. Une relation entre objets persistants peut être soit de type appartenance, dans le cas où l'un des objets ne peut exister sans l'autre, soit de type indépendant, lorsque les deux objets peuvent exister indépendamment de la relation qui les lie l'un à l'autre. La mise en œuvre de l'interface JDO pour App Engine peut modéliser les relations un à un de type appartenance ou indépendante, ainsi que les relations un à plusieurs, aussi bien unidirectionnelles que bidirectionnelles.

Les relations de type indépendant ne sont pas compatibles avec la version 1.0 du plug-in DataNucleus pour App Engine, mais vous pouvez les gérer vous-même en stockant directement les clés de datastore dans des champs. App Engine regroupe automatiquement les entités associées pour prendre en charge la mise à jour d'objets associés, mais l'application doit savoir quand utiliser les transactions du datastore.

La version 2.x du plug-in DataNucleus pour App Engine accepte les relations du type indépendant avec une syntaxe naturelle. La section Relations indépendantes explique comment créer des relations indépendantes dans chaque version du plug-in. Pour passer à la version 2.x du plug-in DataNucleus pour App Engine, consultez Migrer vers la version 2.x du plug-in DataNucleus pour App Engine.

Relations d'appartenance un-à-un

Pour créer une relation d'appartenance un à un unidirectionnelle entre deux objets persistants, vous devez utiliser un champ dont le type correspond à la classe de la classe associée.

L'exemple qui suit définit une classe de données ContactInfo et une classe de données Employee, en établissant une relation un à un entre l'objet Employee et l'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;

    // ...
}

Employee.java

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

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

    @Persistent
    private ContactInfo contactInfo;

    ContactInfo getContactInfo() {
        return contactInfo;
    }
    void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    // ...
}

Les objets persistants sont représentés sous la forme de deux entités distinctes dans le datastore, avec deux genres différents. Leur relation est représentée à l'aide d'une relation de groupe d'entités : la clé de l'enfant utilise la clé du parent en guise de parent du groupe d'entités. Lorsque l'application accède à l'objet enfant à l'aide du champ de l'objet parent, l'implémentation JDO exécute une requête de parent du groupe d'entités pour récupérer l'enfant.

La classe enfant doit comporter un champ de clé dont le type peut contenir les informations de clé parente, c'est-à-dire soit une instance Key, soit une valeur Key encodée sous forme de chaîne. Pour en savoir plus sur les types de champs de clés, consultez la page Créer des données : clés.

Pour créer une relation un à un bidirectionnelle à l'aide de champs des deux classes, vous devez ajouter une annotation au champ de la classe enfant pour déclarer que les champs représentent une relation bidirectionnelle. Le champ de la classe enfant doit avoir une annotation @Persistent avec l'argument mappedBy = "...", où la valeur est le nom du champ de la classe parente. Si le champ d'un objet est renseigné, le champ de référence correspondant de l'autre objet l'est également automatiquement.

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

La première fois que vous accédez aux objets enfants, ces derniers sont chargés à partir du datastore. Si vous n'accédez pas à l'objet enfant d'un objet parent, l'entité de l'objet enfant n'est jamais chargée. Si vous voulez charger l'enfant, vous pouvez le "toucher" avant de fermer PersistenceManager (par exemple, en appelant getContactInfo() dans l'exemple ci-dessus) ou ajouter explicitement le champ enfant au groupe d'extraction par défaut afin qu'il soit récupéré et chargé avec le parent :

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

Relations d'appartenance un-à-plusieurs

Pour créer une relation un à plusieurs entre des objets spécifiques d'une classe et plusieurs objets d'une autre classe, utilisez une collection de la classe associée :

Employee.java

import java.util.List;

// ...
    @Persistent
    private List<ContactInfo> contactInfoSets;

Les relations bidirectionnelles un à plusieurs sont semblables aux relations un à un, avec un champ de la classe parente utilisant l'annotation @Persistent(mappedBy = "..."), où la valeur est le nom du champ sur la classe enfant :

Employee.java

import java.util.List;

// ...
    @Persistent(mappedBy = "employee")
    private List<ContactInfo> contactInfoSets;

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

Les types de collection répertoriés dans Définir des classes de données : collections sont pris en charge pour les relations un à plusieurs. Toutefois, les tableaux ne sont pas utilisables dans le cadre de relations de ce type.

App Engine ne prend pas en charge les requêtes de jointure : vous ne pouvez pas exécuter une requête portant sur une entité parente à l'aide d'un attribut d'une entité enfant. (Vous pouvez exécuter une requête sur une propriété d'une classe intégrée, car ces classes stockent les propriétés dans l'entité parente. Consultez Définir des classes de données : classes intégrées.)

Préserver l'ordre des collections ordonnées

Les collections ordonnées, telles que List<...>, conservent l'ordre des objets lors de l'enregistrement de l'objet parent. JDO exige que les bases de données préservent cet ordre en stockant la position de chaque objet sous la forme d'une propriété de l'objet. App Engine stocke ces informations en tant que propriété de l'entité correspondante, en utilisant un nom de propriété égal au nom du champ du parent suivi de _INTEGER_IDX. Les propriétés de position sont inefficaces. En cas d'ajout, de suppression ou de déplacement d'un élément dans la collection, toutes les entités situées derrière cet élément dans la collection doivent être mises à jour. Si elle n'entre pas dans le cadre d'une transaction, cette procédure peut se révéler lente et source d'erreurs.

Si vous n'avez pas besoin de préserver un ordre arbitraire dans une collection, mais que vous devez utiliser un type de collection ordonné, vous pouvez spécifier un ordonnancement reposant sur les propriétés des éléments à l'aide d'une annotation, une extension de JDO fournie par DataNucleus :

import java.util.List;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc"))
    private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();

L'annotation @Order (avec l'extension list-ordering) spécifie l'ordre souhaité des éléments de la collection en tant que clause d'ordonnancement JDOQL. Cet ordonnancement utilise les valeurs des propriétés des éléments. Comme dans le cas des requêtes, tous les éléments d'une collection doivent comporter une valeur correspondant aux propriétés utilisées dans la clause d'ordonnancement.

L'accès à une collection exécute une requête. Si la clause d'ordonnancement d'un champ utilise plusieurs ordres de tri, la requête doit comporter un index Datastore. Pour en savoir plus, consultez la page Index Datastore.

Pour procéder de manière efficace, utilisez systématiquement une clause d'ordonnancement explicite pour les relations un à plusieurs des types de collections ordonnés, chaque fois que cela vous est possible.

Relations indépendantes

Outre les relations d'appartenance, l'API JDO offre une fonction de gestion des relations indépendantes. Cette fonctionnalité agit différemment selon la version du plug-in DataNucleus pour App Engine que vous utilisez :

  • La version 1 du plug-in DataNucleus ne met pas en œuvre les relations indépendantes avec une syntaxe naturelle, mais vous pouvez quand même gérer ces relations à l'aide de valeurs Key au lieu d'instances (ou de collections d'instances) des objets de votre modèle. Vous pouvez considérer que le stockage d'objets Key équivaut à la modélisation d'une "clé étrangère" arbitraire entre deux objets. Le datastore ne garantit pas l'intégrité référentielle avec ces références Key, mais l'utilisation d'objets Key facilite grandement la modélisation (puis la récupération) des relations entre deux objets.

    Cependant, si vous choisissez cette procédure, vous devez vous assurer que les clés sont du genre approprié. JDO et le compilateur ne vérifient pas les types d'objets Key.
  • La version 2.x du plug-in DataNucleus met en œuvre des relations indépendantes avec une syntaxe naturelle.

Conseil : Il peut parfois s'avérer nécessaire de modéliser une relation d'appartenance comme s'il s'agissait d'une relation indépendante. Cette nécessité découle du fait que tous les objets impliqués dans une relation d'appartenance sont automatiquement placés dans le même groupe d'entités, et qu'un groupe d'entités ne prend en charge qu'une à dix écritures par secondes. Ainsi, si un objet parent et un objet enfant reçoivent tous deux 0,75 écriture par seconde, il peut être judicieux de modéliser cette relation comme indépendante afin que le parent et l'enfant résident chacun dans leur propre groupe d'entités indépendant.

Relations indépendantes un-à-un

Supposons que vous souhaitiez modéliser une personne (objet Person) et un plat (objet Food), une personne ne pouvant avoir qu'un seul plat favori, mais le plat favori n'appartenant pas à la personne, puisqu'il peut constituer le plat favori d'un nombre de personnes indéfini. Cette section montre la procédure à suivre dans ce cas.

Dans JDO 2.3

Dans cet exemple, nous attribuons à Person un membre du type Key, où Key est l'identifiant unique d'un objet Food. Si une instance de Person et l'instance de Food référencée par Person.favoriteFood n'appartiennent pas au même groupe d'entités, vous ne pouvez mettre à jour la personne et son plat favori en une seule transaction que si votre configuration JDO est définie de sorte à activer les transactions entre groupes (XG).

Person.java

// ... imports ...

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

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

Dans JDO 3.0

Dans cet exemple, au lieu d'attribuer à Person une clé représentant son plat préféré, nous créons un membre privé du type Food :

Person.java

// ... imports ...

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

    @Persistent
    @Unowned
    private Food favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

Relations indépendantes un-à-plusieurs

Supposons à présent que vous souhaitiez autoriser une personne à avoir plusieurs plats favoris. Là encore, un plat favori n'appartient pas à la personne, puisqu'il peut constituer le plat favori d'un nombre de personnes indéfini.

Dans JDO 2.3

Dans cet exemple, au lieu d'attribuer à Person un membre du type Set<Food> pour représenter son plat préféré, nous lui attribuons un membre du type Set<Key>, où l'ensemble contient les identifiants uniques des objets Food. Notez que si une instance de Person et une instance de Food contenues dans Person.favoriteFoods ne font pas partie du même groupe d'entités, vous devez définir votre configuration JDO de sorte à activer les transactions entre groupes (XG) pour pouvoir les mettre à jour dans la même transaction.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

Dans JDO 3.0

Dans cet exemple, nous attribuons à Person un membre du type Set<Food>, où l'ensemble représente ses plats préférés.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relations plusieurs-à-plusieurs

Nous pouvons modéliser une relation plusieurs à plusieurs en gérant des collections de clés des deux côtés de la relation. Modifions notre exemple pour permettre à Food de garder la trace des personnes pour lesquelles il s'agit d'un plat favori :

Person.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> favoriteFoods;

Food.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> foodFans;

Dans cet exemple, l'objet Person gère un ensemble de valeurs Key identifiant de manière unique les objets Food qui sont favoris, tandis que l’objet Food gère un ensemble de valeurs Key identifiant de manière unique les objets Person pour lesquels il s'agit d'un plat favori.

Lorsque vous modélisez des relations plusieurs à plusieurs à l'aide de valeurs Key, n'oubliez pas que l'application a la responsabilité de gérer les deux côtés de la relation :

Album.java


// ...
public void addFavoriteFood(Food food) {
    favoriteFoods.add(food.getKey());
    food.getFoodFans().add(getKey());
}

public void removeFavoriteFood(Food food) {
    favoriteFoods.remove(food.getKey());
    food.getFoodFans().remove(getKey());
}

Si une instance de Person et une instance de Food contenues dans Person.favoriteFoods ne font pas partie du même groupe d'entités et que vous souhaitez les mettre à jour en une seule transaction, vous devez définir votre configuration JDO de sorte à activer les transactions entre groupes (XG).

Relations, groupes d'entités et transactions

Lorsque votre application enregistre un objet avec des relations d'appartenance dans le datastore, tous les autres objets qui sont accessibles par l'intermédiaire de relations et qui doivent être enregistrés (parce qu'ils sont nouveaux ou qu'ils ont été modifiés depuis leur dernier chargement) le sont automatiquement. Cette règle a d'importantes répercussions sur les transactions et les groupes d'entités.

Prenons l'exemple suivant basé sur une relation unidirectionnelle entre les classes Employee et ContactInfo ci-dessus :

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    pm.makePersistent(e);

Lorsque le nouvel objet Employee est enregistré à l'aide de la méthode pm.makePersistent(), le nouvel objet ContactInfo associé est enregistré automatiquement. Comme les deux objets sont nouveaux, App Engine crée deux nouvelles entités dans le même groupe d'entités, où l'entité Employee est parente de l'entité ContactInfo. De même, si l'objet Employee a déjà été enregistré et que l'objet ContactInfo est nouveau, App Engine crée l'entité ContactInfo en utilisant l'entité Employee existante en tant que parent.

Notez, cependant, que l'appel à pm.makePersistent() dans cet exemple n'utilise pas de transaction. En l'absence d'une transaction explicite, les deux entités sont créées à l'aide d'actions atomiques distinctes. Dans ce cas, il est possible que la création de l'entité Employee aboutisse, mais que la création de l'entité ContactInfo échoue. Pour vous assurer que les entités seront créées toutes les deux ou ne le seront ni l'une ni l'autre, vous devez utiliser une transaction :

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    try {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        pm.makePersistent(e);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

Si les deux objets ont été enregistrés avant l'établissement de la relation, App Engine ne peut pas "déplacer" l'entité ContactInfo existante vers le groupe d'entités Employee, car les groupes d'entités ne peuvent être attribués qu'après la création des entités. App Engine peut établir la relation avec une référence, mais les entités associées n'appartiendront pas au même groupe. Dans ce cas, les deux entités peuvent être mises à jour ou supprimées dans la même transaction si vous définissez votre configuration JDO de sorte à activer les transactions entre groupes (XG). Si vous n'utilisez pas les transactions XG, la tentative de mise à jour ou de suppression d'entités de différents groupes dans la même transaction déclenchera une exception JDOFatalUserException.

L'enregistrement d'un objet parent dont les objets enfants ont été modifiés entraînera l'enregistrement des modifications apportées à ces derniers. Il est recommandé d'autoriser les objets parents à préserver de cette façon la persistance de tous les objets enfants associés et d'utiliser des transactions lors de l'enregistrement des modifications.

Enfants dépendants et suppressions en cascade

Une relation d'appartenance peut être "dépendante", ce qui signifie que l'enfant ne peut pas exister sans son parent. Si une relation est dépendante et qu'un objet parent est supprimé, tous les objets enfants sont également supprimés. La rupture d'une relation d'appartenance dépendante par l'attribution d'une nouvelle valeur au champ dépendant du parent supprime également l'ancien enfant. Vous pouvez déclarer qu'une relation d'appartenance un à un est dépendante en ajoutant dependent="true" à l'annotation Persistent du champ de l'objet parent qui fait référence à l'enfant :

// ...
    @Persistent(dependent = "true")
    private ContactInfo contactInfo;

Vous pouvez déclarer qu'une relation d'appartenance un à plusieurs est dépendante en ajoutant une annotation @Element(dependent = "true") au champ de l'objet parent qui fait référence à la collection enfant :

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

Comme pour la création et la mise à jour d'objets, si vous souhaitez que chaque suppression d'une opération de suppression en cascade soit effectuée dans une même action atomique, vous devez exécuter la suppression dans une transaction.

Remarque : C'est la mise en œuvre de JDO qui se charge de supprimer les objets enfants dépendants, et non le datastore. Si vous supprimez une entité parente à l'aide de l'API de bas niveau ou de la console Google Cloud Platform, les objets enfants associés ne seront pas supprimés.

Relations polymorphes

Même si la spécification JDO inclut la prise en charge des relations polymorphes, celles-ci ne sont pas encore compatibles avec l'implémentation JDO pour App Engine. Nous espérons éliminer cette limitation dans les versions ultérieures du produit. Si vous devez faire référence à plusieurs types d'objets par l'intermédiaire d'une classe de base commune, nous vous recommandons d'utiliser la même stratégie que pour l'implémentation des relations indépendantes, c'est-à-dire en stockant une référence Key. Par exemple, si vous avez une classe de base Recipe avec les spécialisations Appetizer, Entree et Dessert, et que vous voulez modéliser la recette Recipe favorite d'un Chef, vous pouvez procéder comme suit :

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

    @Persistent
    private int prepTime;
}

Appetizer.java

// ... imports ...

@PersistenceCapable
public class Appetizer extends Recipe {
// ... appetizer-specific fields
}

Entree.java

// ... imports ...

@PersistenceCapable
public class Entree extends Recipe {
// ... entree-specific fields
}

Dessert.java

// ... imports ...

@PersistenceCapable
public class Dessert extends Recipe {
// ... dessert-specific fields
}

Chef.java

// ... imports ...

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

    @Persistent(dependent = "true")
    private Recipe favoriteRecipe;
}

Malheureusement, si vous instanciez une classe Entree et l'attribuez à Chef.favoriteRecipe, vous obtiendrez une exception UnsupportedOperationException lorsque vous essaierez de rendre l'objet Chef persistant. En effet, le type d'environnement d'exécution de l'objet Entree ne correspond pas au type de champ de relation déclaré, Recipe. La solution permettant de contourner ce problème consiste à remplacer Recipe par Key pour le type de Chef.favoriteRecipe :

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

Chef.favoriteRecipe n'étant plus un champ de relation, il peut faire référence à un objet de n'importe quel type. Mais ceci présente un inconvénient, car comme dans le cas d'une relation indépendante, vous devez gérer cette relation manuellement.

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

Envoyer des commentaires concernant…

Environnement standard App Engine pour Java