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'objetsKey
. - 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 Key
qui 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 ListcontactInfos;
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, 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.