hibernate-orm/doc/reference/fr/modules/example_parentchild.xml

372 lines
15 KiB
XML

<?xml version="1.0" encoding="iso-8859-1"?>
<chapter id="example-parentchild">
<title>Exemple : Père/Fils</title>
<para>
L'une des premières choses que les nouveaux utilisateurs essaient de faire avec Hibernate est de modéliser
une relation père/fils. Il y a deux approches différentes pour cela. Pour un certain nombre de raisons, la méthode la
plus courante, en particulier pour les nouveaux utilisateurs, est de modéliser les deux relations <literal>Père</literal>
et <literal>Fils</literal> comme des classes entités liées par une association <literal>&lt;one-to-many&gt;</literal> du
<literal>Père</literal> vers le <literal>Fils</literal> (l'autre approche est de déclarer le <literal>Fils</literal>
comme un <literal>&lt;composite-element&gt;</literal>). Il est évident que le sens de l'association un vers plusieurs
(dans Hibernate) est bien moins proche du sens habituel d'une relation père/fils que ne l'est celui d'un
élément cmposite. Nous allons vous expliquer comment utiliser une association <emphasis>un vers plusieurs bidirectionnelle
avec cascade</emphasis> afin de modéliser efficacement et élégamment une relation père/fils, ce n'est vraiment
pas difficile !
</para>
<sect1 id="example-parentchild-collections">
<title>Une note à propos des collections</title>
<para>
Les collections Hibernate sont considérées comme étant une partie logique
de l'entité dans laquelle elles sont contenues ; jamais des entités qu'elle
contient. C'est une distinction crutiale ! Les conséquences sont les suivantes :
</para>
<itemizedlist>
<listitem>
<para>
Quand nous ajoutons / retirons un objet d'une collection, le numéro de version du
propriétaire de la collection est incrémenté.
</para>
</listitem>
<listitem>
<para>
Si un objet qui a été enlevé d'une collection est une instance de type valeur (ex :
élément composite), cet objet cessera d'être persistant et son état sera complètement effacé
de la base de données. Par ailleurs, ajouter une instance de type valeur dans une collection
aura pour conséquence que son état sera immédiatement persistant.
</para>
</listitem>
<listitem>
<para>
Si une entité est enlevée d'une collection (association un-vers-plusieurs
ou plusieurs-vers-plusieurs), par défaut, elle ne sera pas effacée. Ce comportement
est complètement logique - une modification de l'un des états internes d'une entité
ne doit pas causer la disparition de l'entité associée !
De même, l'ajout d'une entité dans une collection n'engendre pas,
par défaut, la persistance de cette entité.
</para>
</listitem>
</itemizedlist>
<para>
Le comportement par défaut est donc que l'ajout d'une entité dans une collection créé
simplement le lien entre les deux entités, et qu'effacer une entité supprime ce lien.
C'est le comportement le plus approprié dans la plupart des cas. Ce comportement n'est
cependant pas approprié lorsque la vie du fils est liée au cycle de vie du père.
</para>
</sect1>
<sect1 id="example-parentchild-bidir">
<title>un-vers-plusieurs bidirectionnel</title>
<para>
Supposons que nous ayons une simple association <literal>&lt;one-to-many&gt;</literal>
de <literal>Parent</literal> vers <literal>Child</literal>.
</para>
<programlisting><![CDATA[<set name="children">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
Si nous executions le code suivant
</para>
<programlisting><![CDATA[Parent p = .....;
Child c = new Child();
p.getChildren().add(c);
session.save(c);
session.flush();]]></programlisting>
<para>
Hibernate exécuterait deux ordres SQL:
</para>
<itemizedlist>
<listitem>
<para>un <literal>INSERT</literal> pour créer l'enregistrement pour <literal>c</literal></para>
</listitem>
<listitem>
<para>
un <literal>UPDATE</literal> pour créer le lien de <literal>p</literal> vers
<literal>c</literal>
</para>
</listitem>
</itemizedlist>
<para>
Ceci est non seuleument inefficace, mais viole aussi toute contrainte <literal>NOT NULL</literal> sur
la colonne <literal>parent_id</literal>. Nous pouvons réparer la contrainte de nullité
en spécifiant <literal>not-null="true"</literal> dans le mapping de la collection :
</para>
<programlisting><![CDATA[<set name="children">
<key column="parent_id" not-null="true"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
Cependant ce n'est pas la solution recommandée.
</para>
<para>
La cause sous jacente à ce comportement est que le lien (la clé étrangère <literal>parent_id</literal>) de
<literal>p</literal> vers <literal>c</literal> n'est pas considérée comme faisant partie de l'état
de l'objet <literal>Child</literal> et n'est donc pas créé par l'<literal>INSERT</literal>.
La solution est donc que ce lien fasse partie du mapping de <literal>Child</literal>.
</para>
<programlisting><![CDATA[<many-to-one name="parent" column="parent_id" not-null="true"/>]]></programlisting>
<para>
(Nous avons aussi besoin d'ajouter la propriété <literal>parent</literal> dans la classe <literal>Child</literal>).
</para>
<para>
Maintenant que l'état du lien est géré par l'entité <literal>Child</literal>, nous spécifions à la
collection de ne pas mettre à jour le lien. Nous utilisons l'attribut <literal>inverse</literal>.
</para>
<programlisting><![CDATA[<set name="children" inverse="true">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
Le code suivant serait utilisé pour ajouter un nouveau <literal>Child</literal>
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);
session.save(c);
session.flush();]]></programlisting>
<para>
Maintenant, seul un <literal>INSERT</literal> SQL est nécessaire !
</para>
<para>
Pour alléger encore un peu les choses, nous devrions créer une méthode <literal>addChild()</literal>
dans <literal>Parent</literal>.
</para>
<programlisting><![CDATA[public void addChild(Child c) {
c.setParent(this);
children.add(c);
}]]></programlisting>
<para>
Le code d'ajout d'un <literal>Child</literal> serait alors
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.save(c);
session.flush();]]></programlisting>
</sect1>
<sect1 id="example-parentchild-cascades">
<title>Cycle de vie en cascade</title>
<para>
L'appel explicite de <literal>save()</literal> est un peu fastidieux. Nous pouvons
simplifier cela en utilisant les cascades.
</para>
<programlisting><![CDATA[<set name="children" inverse="true" cascade="all">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
Simplifie le code précédent en
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.flush();]]></programlisting>
<para>
De la même manière, nous n'avons pas à itérer sur les fils lorsque nous sauvons
ou effacons un <literal>Parent</literal>. Le code suivant efface <literal>p</literal>
et tous ses fils de la base de données.
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
session.delete(p);
session.flush();]]></programlisting>
<para>
Par contre, ce code
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();]]></programlisting>
<para>
n'effacera pas <literal>c</literal> de la base de données, il enlèvera seulement
le lien vers <literal>p</literal> (et causera une violation de contrainte
<literal>NOT NULL</literal>, dans ce cas).
Vous devez explicitement utiliser <literal>delete()</literal> sur <literal>Child</literal>.
</para>
<programlisting><![CDATA[Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
session.delete(c);
session.flush();]]></programlisting>
<para>
Dans notre cas, un <literal>Child</literal> ne peut pas vraiment exister sans son père. Si nous
effacons un <literal>Child</literal> de la collection, nous voulons vraiment qu'il soit effacé.
Pour cela, nous devons utiliser <literal>cascade="all-delete-orphan"</literal>.
</para>
<programlisting><![CDATA[<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>]]></programlisting>
<para>
A noter : même si le mapping de la collection spécifie <literal>inverse="true"</literal>, les cascades
sont toujours assurées par l'itération sur les éléments de la collection. Donc, si vous avez besoin
qu'un objet soit enregistré, effacé ou mis à jour par cascade, vous devez l'ajouter dans la colleciton.
Il ne suffit pas d'appeler explicitement <literal>setParent()</literal>.
</para>
</sect1>
<sect1 id="example-parentchild-update">
<title>Cascades et <literal>unsaved-value</literal></title>
<para>
Supposons que nous ayons chargé un <literal>Parent</literal> dans une <literal>Session</literal>,
que nous l'ayons ensuite modifié et que voulions persiter ces modifications dans une nouvelle session
en appelant <literal>update()</literal>.
Le <literal>Parent</literal> contiendra une collection de fils et, puisque la cascade est activée,
Hibernate a besoin de savoir quels fils viennent d'être instanciés et quels fils proviennent de la base
de données. Supposons aussi que <literal>Parent</literal> et <literal>Child</literal> ont tous deux
des identifiants du type <literal>Long</literal>.
Hibernate utilisera la propriété de l'identifiant et la propriété de la version/horodatage pour déterminer quels fils sont nouveaux
(vous pouvez aussi utiliser la propriété version ou timestamp, voir
<xref linkend="manipulatingdata-updating-detached"/>).
<emphasis>Dans Hibernate3, il n'est plus nécessaire de spécifier
une <literal>unsaved-value</literal> explicitement.</emphasis>
</para>
<para>
Le code suivant mettra à jour <literal>parent</literal> et <literal>child</literal>
et insérera <literal>newChild</literal>.
</para>
<programlisting><![CDATA[//parent et child ont été chargés dans une session précédente
parent.addChild(child);
Child newChild = new Child();
parent.addChild(newChild);
session.update(parent);
session.flush();]]></programlisting>
<para>
Ceci est très bien pour des identifiants générés, mais qu'en est-il des identifiants assignés et des
identifiants composés ? C'est plus difficile,
puisqu'Hibernate ne peut pas utiliser la propriété de l'identifiant pour distinguer un objet
nouvellement instancié (avec un identifiant assigné par l'utilisateur) d'un objet chargé dans une session précédente.
Dans ce cas, Hibernate utilisera soit la propriété de version ou d'horodatage, soit effectuera vraiment une requête au cache
de second niveau, soit, dans le pire des cas, à la base de données, pour voir si la ligne existe.
</para>
<!-- undocumenting
<para>
There is one further possibility. The <literal>Interceptor</literal> method named
<literal>isUnsaved()</literal> lets the application implement its own strategy for distinguishing
newly instantiated objects. For example, you could define a base class for your persistent classes.
</para>
<programlisting><![CDATA[public class Persistent {
private boolean _saved = false;
public void onSave() {
_saved=true;
}
public void onLoad() {
_saved=true;
}
......
public boolean isSaved() {
return _saved;
}
}]]></programlisting>
<para>
(The <literal>saved</literal> property is non-persistent.)
Now implement <literal>isUnsaved()</literal>, along with <literal>onLoad()</literal>
and <literal>onSave()</literal> as follows.
</para>
<programlisting><![CDATA[public Boolean isUnsaved(Object entity) {
if (entity instanceof Persistent) {
return new Boolean( !( (Persistent) entity ).isSaved() );
}
else {
return null;
}
}
public boolean onLoad(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if (entity instanceof Persistent) ( (Persistent) entity ).onLoad();
return false;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if (entity instanceof Persistent) ( (Persistent) entity ).onSave();
return false;
}]]></programlisting>
<para>
Don't worry; in Hibernate3 you don't need to write any of this kind of code if you don't want to.
</para>
-->
</sect1>
<sect1 id="example-parentchild-conclusion">
<title>Conclusion</title>
<para>
Il y a quelques principes à maîtriser dans ce chapitre et tout cela peut paraître déroutant la première fois.
Cependant, dans la pratique, tout fonctionne parfaitement. La plupart des applications Hibernate utilisent
le pattern père / fils.
</para>
<para>
Nous avons évoqué une alternative dans le premier paragraphe. Aucun des points traités précédemment n'existe
dans le cas d'un mapping <literal>&lt;composite-element&gt;</literal> qui possède exactement la sémantique
d'une relation père / fils. Malheureusement, il y a deux grandes limitations pour les classes éléments
composites : les éléments composites ne peuvent contenir de collections, et ils ne peuvent être les fils
d'entités autres que l'unique parent.
</para>
</sect1>
</chapter>