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 comporter une annotation @Persistent avec l'argument mappedBy = "...", dont la valeur correspond au 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;

Une relation un à plusieurs bidirectionnelle est comparable à une relation un à un : un champ de la classe parente utilise l'annotation @Persistent(mappedBy = "..."), dont la valeur correspond au nom du champ de 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<...>, préservent l'ordre des objets lorsque l'objet parent est enregistré. 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 cette information en tant que propriété de l'entité correspondante en utilisant un nom de propriété identique au nom du champ 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 (utilisant l'extension list-ordering) spécifie l'ordre souhaité des éléments de la collection sous la forme d'une 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 des instances (ou collections d'instances) d'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 de 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 ne font pas partie du 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, plutôt que d'attribuer à Person une clé représentant le plat préféré de la personne, nous créons un membre privé de 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 de type Set<Food> pour représenter les mets favoris de la personne, nous affectons à Person un membre de type Set<Key>, où l'ensemble (Set) 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 de type Set<Food>, où l'ensemble représente les plats préférés de la personne.

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, Person gère un ensemble (Set) de valeurs Keyqui identifient de manière unique les objets Food qui constituent des favoris, tandis que Food gère un ensemble de valeurs Key qui identifient de manière unique les objets Person qui le considèrent comme leur mets favori.

Si vous modélisez une relation plusieurs à plusieurs à l'aide de valeurs Key, notez que la gestion des deux côtés de la relation doit être assurée par l'application :

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.

Examinons l'exemple suivant qui utilise une relation unidirectionnelle entre les classes Employee et ContactInfo définies précédemment :

    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. Puisque ces deux objets sont nouveaux, App Engine crée deux entités dans le même groupe d'entités, en utilisant l'entité Employee comme parent de l'entité ContactInfo. De la même façon, si l'objet Employee a déjà été enregistré et que l'objet ContactInfo associé est nouveau, App Engine crée l'entité ContactInfo en utilisant l'entité Employee existante en guise de parent.

Toutefois, notez que l'appel de la méthode 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. Pour déclarer une relation d'appartenance un à un comme dépendante, ajoutez la chaîne dependent="true" à l'annotation Persistent du champ de l'objet parent qui fait référence à l'enfant :

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

Pour déclarer une relation d'appartenance un à plusieurs comme dépendante, ajoutez 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 Google Cloud Console, 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 disposez d'une classe de base Recipe avec les spécialisations Appetizer, Entree et Dessert, et que vous souhaitez modéliser l'objet Recipe favori d'un objet Chef, vous pouvez effectuer cette modélisation de la façon suivante :

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 un objet Entree et que vous l'attribuez au champ Chef.favoriteRecipe, vous obtiendrez une exception UnsupportedOperationException si vous tentez de déclarer l'objet Chef comme persistant. Cette erreur est due au fait que le type d'exécution de l'objet, Entree, ne correspond pas au type déclaré du champ de relation, Recipe. La solution 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;
}

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