Les classes persistantes sont les classes d'une application qui implémentent les entités d'un problème métier (ex. Client et Commande dans une application de commerce électronique). Les classes persistantes ont, comme leur nom l'indique, des instances transiantes mais aussi persistantes c'est-à-dire stockées en base de données.
Hibernate fonctionne de manière optimale lorsque ces classes suivent quelques règles simples, aussi connues comme le modèle de programmation Plain Old Java Object (POJO).
Toute bonne application Java nécessite une classe persistante représentant les félins.
package eg; import java.util.Set; import java.util.Date; public class Cat { private Long id; // identifiant private String name; private Date birthdate; private Cat mate; private Set kittens private Color color; private char sex; private float weight; private void setId(Long id) { this.id=id; } public Long getId() { return id; } void setName(String name) { this.name = name; } public String getName() { return name; } void setMate(Cat mate) { this.mate = mate; } public Cat getMate() { return mate; } void setBirthdate(Date date) { birthdate = date; } public Date getBirthdate() { return birthdate; } void setWeight(float weight) { this.weight = weight; } public float getWeight() { return weight; } public Color getColor() { return color; } void setColor(Color color) { this.color = color; } void setKittens(Set kittens) { this.kittens = kittens; } public Set getKittens() { return kittens; } // addKitten n'est pas nécessaire pour Hibernate public void addKitten(Cat kitten) { kittens.add(kitten); } void setSex(char sex) { this.sex=sex; } public char getSex() { return sex; } }
Il y a quatre règles à suivre ici :
Cat déclare des méthodes d'accès pour tous ses attributs persistants. Beaucoup d'autres solutions de mapping Objet/relationnel persistent directement les instances des attributs. Nous pensons qu'il est bien mieux de découpler ce détail d'implémentation du mécanisme de persistence. Hibernate persiste les propriétés suivant le style JavaBeans et reconnait les noms de méthodes de la forme getFoo, isFoo et setFoo.
Les propriétés n'ont pas à être déclarées publiques - Hibernate peut persister une propriété avec un paire de getter/setter de visibilité par défault, protected ou private
Cat possède un constructeur par défaut (sans argument) implicite. Toute classe persistante doit avoir un constructeur par défaut (qui peut être non-publique) pour permettre à Hibernate de l'instancier en utilisant Constructor.newInstance().
Cat possède une propriété appelée id. Cette propriété conserve la valeur de la colonne de clé primaire de la table d'une base de données. La propriété aurait pu s'appeler complètement autrement, et son type aurait pu être n'importe quel type primitif, n'importe quel "encapsuleur" de type primitif, java.lang.String ou java.util.Date. (Si votre base de données héritée possède des clés composites, elles peuvent être mappées en utilisant une classe définie par l'utilisateur et possédant les propriétés associées aux types de la clé composite - voir la section concernant les identifiants composites plus bas).
La propriété d'identifiant est optionnelle. Vous pouver l'oublier et laisser Hibernate s'occuper des identifiants de l'objet en interne. Cependant, pour beaucoup d'applications, avoir un identifiant reste un design bon (et très populaire).
De plus, quelques fonctionnalités ne sont disponibles que pour les classes déclarant un identifiant de propriété :
Mises à jour en cascade (Voir "Cycle de vie des objets")
Session.saveOrUpdate()
Nous recommandons que vous déclariez les propriétés d'identifiant de manière uniforme. Nous recommandons également que vous utilisiez un type nullable (ie. non primitif).
Une fonctionalité clée d'Hibernate, les proxies, nécessitent que la classe persistente soit non finale ou qu'elle soit l'implémentation d'une interface qui déclare toutes les méthodes publiques.
Vous pouvez persister, grâce à Hibernate, les classes final qui n'implémentent pas d'interface, mais vous ne pourrez pas utiliser les proxies - ce qui limitera vos possibilités d'ajustement des performances.
Une sous-classe doit également suivre la première et la seconde règle. Elle hérite sa propriété d'identifiant de Cat.
package eg; public class DomesticCat extends Cat { private String name; public String getName() { return name; } protected void setName(String name) { this.name=name; } }
Vous devez surcharger les méthodes equals() et hashCode() si vous avez l'intention de "mélanger" des objets de classes persistantes (ex dans un Set).
Cette règle ne s'applique que si ces objets sont chargés à partir de deux Sessions différentes, dans la mesure où Hibernate ne garantit l'identité de niveau JVM ( a == b , l'implémentation par défaut d'equals() en Java) qu'au sein d'une seule Session !
Même si deux objets a et b représentent la même ligne dans la base de données (ils ont la même valeur de clé primaire comme identifiant), nous ne pouvons garantir qu'ils seront la même instance Java hors du contexte d'une Session donnée.
La manière la plus évidente est d'implémenter equals()/hashCode() en comparant la valeur de l'identifiant des deux objets. Si cette valeur est identique, les deux doivent représenter la même ligne de base de données, ils sont donc égaux (si les deux sont ajoutés à un Set, nous n'auront qu'un seul élément dans le Set). Malheureusement, nous ne pouvons pas utiliser cette approche. Hibernate n'assignera de valeur d'identifiant qu'aux objets qui sont persistant, une instance nouvellement créée n'aura donc pas de valeur d'identifiant ! Nous recommandons donc d'implémenter equals() et hashCode() en utilisant l'égalité par clé métier.
L'égalité par clé métier signifie que la méthode equals() compare uniquement les propriétés qui forment une clé métier, une clé qui identifierait notre instance dans le monde réel (une clé candidate naturelle) :
public class Cat { ... public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof Cat)) return false; final Cat cat = (Cat) other; if (!getName().equals(cat.getName())) return false; if (!getBirthday().equals(cat.getBirthday())) return false; return true; } public int hashCode() { int result; result = getName().hashCode(); result = 29 * result + getBirthday().hashCode(); return result; } }
Garder à l'esprit que notre clé candidate (dans ce cas, une clé composée du nom et de la date de naissance) n'a à être valide et pertinente que pour une opération de comparaison particulière (peut-être même pour un seul cas d'utilisation). Nous n'avons pas besoin du même critère de stabilité que celui nécessaire à la clé primaire réelle !
Une classe persistence peut, de manière facultative, implémenter l'interface Lifecycle qui fournit des callbacks permettant aux objets persistants d'effectuer des opérations d'initialisation ou de nettoyage après une sauvegarde ou un chargement et avant une suppression ou une mise à jour.
L'Interceptor d'Hibernate offre cependant une alternative moins intrusive.
public interface Lifecycle { public boolean onSave(Session s) throws CallbackException; (1) public boolean onUpdate(Session s) throws CallbackException; (2) public boolean onDelete(Session s) throws CallbackException; (3) public void onLoad(Session s, Serializable id); (4) }
(1) | onSave - appelé juste avant que l'objet soit sauvé ou inséré |
(2) | onUpdate - appelé juste avant qu'un objet soit mis à jour (quand l'objet est passé à Session.update()) |
(3) | onDelete - appelé juste avant que l'objet soit supprimé |
(4) | onLoad - appelé juste après que l'objet soit chargé |
onSave(), onDelete() et onUpdate() peuvent être utilisés pour sauver ou supprimer en cascade de objets dépendants. onLoad() peut être utilisé pour initialiser des propriétés transiantes de l'objet à partir de son état persistant. Il ne doit pas être utilisé pour charger des objets dépendants parce que l'interface Session ne doit pas être appelée au sein de cette méthode. Un autre usage possible de onLoad(), onSave() et onUpdate() est de garder une référence à la Session courante pour un usage ultérieur.
Notez que onUpdate() n'est pas appelé à chaque fois que l'état persistant d'un objet est mis à jour. Elle n'est appelée que lorsqu'un objet transiant est passé à Session.update().
Si onSave(), onUpdate() ou onDelete() retourne true, l'opération n'est pas effectuée et ceci de manière silencieuse. Si une CallbackException est levée, l'opération n'est pas effectuée et l'exception est retournée à l'application.
Notez que onSave() est appelé après que l'identifiant ait été assigné à l'objet sauf si la génération native de clés est utilisée.
Si la classe persistante a besoin de vérifier des invariants avant que son état soit persisté, elle peut implémenter l'interface suivante :
public interface Validatable { public void validate() throws ValidationFailure; }
L'objet doit lever une ValidationFailure si un invariant a été violé. Une instance de Validatable ne doit pas changer son état au sein de la méthode validate().
Contrairement aux méthodes de callback de l'interface Lifecycle, validate() peut être appelé à n'importe quel moment. L'application ne doit pas s'appuyer sur les appels à validate() pour des fonctionalités métier.
Dans le chapitre suivant, nous allons voir comment les mappings Hibernate sont exprimés dans un format XML simple et lisible. Beaucoup d'utilisateurs d'Hibernate préfèrent embarquer les informations de mapping directement dans le code source en utilisant les tags XDoclet @hibernate.tags. Nous ne couvrirons pas cette approche dans ce document parce que considérée comme une part de XDoclet. Cepdendant, nous avons inclus l'exemple suivant utilisant la classe Cat et le mapping XDoclet.
package eg; import java.util.Set; import java.util.Date; /** * @hibernate.class * table="CATS" */ public class Cat { private Long id; // identifiant private Date birthdate; private Cat mate; private Set kittens private Color color; private char sex; private float weight; /** * @hibernate.id * generator-class="native" * column="CAT_ID" */ public Long getId() { return id; } private void setId(Long id) { this.id=id; } /** * @hibernate.many-to-one * column="MATE_ID" */ public Cat getMate() { return mate; } void setMate(Cat mate) { this.mate = mate; } /** * @hibernate.property * column="BIRTH_DATE" */ public Date getBirthdate() { return birthdate; } void setBirthdate(Date date) { birthdate = date; } /** * @hibernate.property * column="WEIGHT" */ public float getWeight() { return weight; } void setWeight(float weight) { this.weight = weight; } /** * @hibernate.property * column="COLOR" * not-null="true" */ public Color getColor() { return color; } void setColor(Color color) { this.color = color; } /** * @hibernate.set * lazy="true" * order-by="BIRTH_DATE" * @hibernate.collection-key * column="PARENT_ID" * @hibernate.collection-one-to-many */ public Set getKittens() { return kittens; } void setKittens(Set kittens) { this.kittens = kittens; } // addKitten n'est pas nécesaire à Hibernate public void addKitten(Cat kitten) { kittens.add(kitten); } /** * @hibernate.property * column="SEX" * not-null="true" * update="false" */ public char getSex() { return sex; } void setSex(char sex) { this.sex=sex; } }