Table des matières
Traducteur(s): Vincent Ricard
Hibernate, comme tous les autres outils de mapping objet/relationnel, nécessite des méta-données qui régissent la transformation des données d'une représentation vers l'autre (et vice versa). Dans Hibernate 2.x, les méta-données de mapping sont la plupart du temps déclarées dans des fichiers XML. Une autre option est XDoclet, qui utilise les annotations du code source Javadoc et un préprocesseur au moment de la compilation. Le même genre d'annotation est maintenant disponible avec le JDK standard, quoique plus puissant et mieux pris en charge par les outils. IntelliJ IDEA et Eclipse, par exemple, prennent en charge la complétion automatique et la coloration syntaxique des annotations du JDK 5.0. Les annotations sont compilées en bytecode et lues au moment de l'exécution (dans le cas d'Hibernate, au démarrage) en utilisant la réflexion, donc pas besoin de fichiers XML externes.
La spécification EJB3 reconnaît l'intérêt et le succès du paradigme du mapping objet/relationnel transparent. La spécification EJB3 standardise les APIs de base et les méta-données requises par n'importe quel mécanisme de persistance objet/relationnel. Hibernate EntityManager implémente les interfaces de programmation et les règles de cycle de vie telles que définies par la spécification de persistance EJB3. Avec Hibernate Annotations, ce wrapper implémente une solution de persistance EJB3 complète (et autonome) au-dessus du noyau mature d'Hibernate. Vous pouvez utiliser soit les trois ensembles, soit les annotations sans le cycle de vie et les interfaces de programmations EJB3, ou même Hibernate tout seul, selon les besoins techniques et fonctionnels de votre projet. Vous pouvez à tout moment recourir aux APIs natives d'Hibernate ou même, si besoin est, à celles de JDBC et au SQL.
Cette version est basée sur la dernière version de la spécification EJB 3.0 / JPA (alias JSP-220) et prend en charge toutes les fonctionnalités de la spécification (dont certaines optionnelles). La plupart des fonctionnalités d'Hibernate et des extensions sont aussi disponibles à travers des annotations spécifiques à Hibernate. Bien que la couverture d'Hibernate en termes de fonctionnalités soit maintenant très grande, certaines sont encore manquantes. Le but ultime est de tout couvrir. Voir la section JIRA "road map" pour plus d'informations.
Si vous utilisiez une version précédente d'Hibernate Annotations, veuillez regarder http://www.hibernate.org/371.html pour un guide de migration.
Téléchargez et installez la distribution Hibernate Annotations à partir du site web d'Hibernate.
Cette version requiert Hibernate 3.2.0.GA ou supérieur. N'utilisez pas cette version d'Hibernate Annotations avec une version plus ancienne d'Hibernate 3.x !
Cette version est connue pour fonctionner avec le noyau 3.2.0.CR5 et 3.2.0.GA d'Hibernate.
Assurez-vous que vous avez le JDK 5.0 d'installé. Vous pouvez bien sûr continuer à utiliser XDoclet et avoir certains des avantages des méta-données basées sur les annotations avec des versions plus anciennes du JDK. Notez que ce document décrit seulement les annotations du JDK 5.0 et que vous devez vous référer à la documentation de XDoclet pour plus d'informations.
Tout d'abord, paramétrez votre classpath (après avoir créer un nouveau projet dans votre IDE favori) :
Copiez toutes les bibliothèques du noyau Hibernate3 et toutes les bibliothèques tierces requises (voir lib/README.txt dans Hibernate).
Copiez aussi hibernate-annotations.jar et lib/ejb3-persistence.jar de la distribution Hibernate Annotations dans votre classpath.
Pour utiliser Chapitre 5, Intégration de Lucene avec Hibernate, ajouter le fichier jar de lucene.
Nous recommandons aussi un petit wrapper pour démarrer Hibernate dans un bloc statique d'initialisation, connu en tant que HibernateUtil. Vous pourriez avoir vu cette classe sous diverses formes dans d'autres parties de la documentation Hibernate. Pour prendre en charge Annotation vous devez modifier cette classe d'aide de la manière suivante :
package hello;
import org.hibernate.*;
import org.hibernate.cfg.*;
import test.*;
import test.animals.Dog;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
sessionFactory = new AnnotationConfiguration().buildSessionFactory();
} catch (Throwable ex) {
// Log exception!
throw new ExceptionInInitializerError(ex);
}
}
public static Session getSession()
throws HibernateException {
return sessionFactory.openSession();
}
}
La partie intéressante ici est l'utilisation de AnnotationConfiguration. Les packages et les classes annotées sont déclarés dans votre fichier de configuration XML habituel (généralement hibernate.cfg.xml). Voici un équivalent de la déclaration ci-dessus :
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping package="test.animals"/>
<mapping class="test.Flight"/>
<mapping class="test.Sky"/>
<mapping class="test.Person"/>
<mapping class="test.animals.Dog"/>
<mapping resource="test/animals/orm.xml"/>
</session-factory>
</hibernate-configuration>
Notez que vous pouvez mélanger l'utilisation du fichier hbm.xml et celle des annotations. L'élément de ressource peut être un fichier hbm ou un descripteur de déploiement XML EJB3. Cette distinction est transparente pour votre procédure de configuration.
Alternativement, vous pouvez définir les classes annotées et des packages en utilisant l'API :
sessionFactory = new AnnotationConfiguration() .addPackage("test.animals") // le nom complet du package .addAnnotatedClass(Flight.class) .addAnnotatedClass(Sky.class) .addAnnotatedClass(Person.class) .addAnnotatedClass(Dog.class) .addResource("test/animals/orm.xml") .buildSessionFactory();
Vous pouvez aussi utiliser Hibernate EntityManager qui a son propre mécanisme de configuration. Veullez vous référer à la documentation de ce projet pour plus de détails.
Il n'y a pas d'autres différences dans la façon d'utiliser les APIs d'Hibernate, excepté ce changement de routine de démarrage ou le fichier de configuration. Vous pouvez utiliser votre méthode de configuration favorite pour d'autres propriétés (hibernate.properties, hibernate.cfg.xml, utilisation des APIs, etc). Vous pouvez même mélanger les classes persistantes annotées et des déclarations hbm.cfg.xml classiques avec la même SessionFactory. Vous ne pouvez cependant pas déclarer une classe plusieurs fois (soit avec les annotations, soit avec un fichier hbm.xml). Vous ne pouvez pas non plus mélanger des stratégies de configuration (hbm vs annotations) dans une hiérarchie d'entités mappées.
Pour faciliter la procédure de migration de fichiers hbm vers les annotations, le mécanisme de configuration détecte la duplication de mappings entre les annotations et les fichiers hbm. Les classes décrites dans les fichiers hbm se voient alors affecter une priorité plus grande que les classes annotées. Vous pouvez changer cette priorité avec la propriété hibernate.mapping.precedence. La valeur par défaut est : hbm, class ; la changer en : class, hbm donne alors la priorité aux classes annotées lorsqu'un conflit survient.
Cette section couvre les annotations entity bean EJB 3.0 (alias JPA) et les extensions spécifiques à Hibernate.
Les entités EJB3 sont des POJOs ordinaires. En fait, ils représentent exactement le même concept que les entités de persistance Hibernate. Leur mapping est défini à travers les annotations du JDK 5.0 (une syntaxe de descripteur XML pour la surcharge est définie dans la spécification EJB3). Les annotations peuvent être divisées en deux catégories, les annotations de mapping logique (vous permettant de décrire le modèle objet, les associations de classe, etc) et les annotations de mapping physique (décrivant le schéma physique, les tables, les colonnes, les index, etc). Nous mélangerons les annotations des deux catégories dans les exemples de code.
Les annotations EJB3 sont dans le package javax.persistence.*. La plupart des IDE compatibles JDK 5 (comme Eclipse, IntelliJ IDEA et Netbeans) peuvent auto-compléter les interfaces et les attributes d'annotation pour vous (même sans module "EJB3" spécifique, puisque les annotations EJB3 sont des annotations ordinaires de JDK 5).
Pour plus d'exemples concrets, lisez le tutorial EJB 3.0 de JBoss ou parcourez la suite de tests d'Hibernate Annotations. La plupart des tests unitaires ont été conçus pour représenter un exemple concret et être une source d'inspiration.
Chaque classe POJO persistante liée est un entity bean et est déclarée en utilisant l'annotation @Entity (au niveau de la classe) :
@Entity
public class Flight implements Serializable {
Long id;
@Id
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
}
@Entity déclare la classe comme un entity bean (ie une classe POJO persistante), @Id déclare la propriété identifiante de cet entity bean. Les autres déclarations de mapping sont implicites. Ce concept de déclaration par exception est un composant essentiel de la nouvelle spécification EJB3 et une amélioration majeure. La classe Flight est mappée sur la table Flight, en utilisant la colonne id comme colonne de la clef primaire.
Selon que vous annotez des champs ou des méthodes, le type d'accès utilisé par Hibernate sera field ou property. La spécification EJB3 exige que vous déclariez les annotations sur le type d'élément qui sera accédé, c'est-à-dire le getter si vous utilisez l'accès property, le champ si vous utilisez l'accès field. Mélanger des EJB3 annotations dans les champs et les méthodes devrait être évité. Hibernate devinera le type d'accès de l'identifiant à partir de la position d'@Id ou d'@EmbeddedId.
@Table est positionnée au niveau de la classe ; cela vous permet de définir le nom de la table, du catalogue et du schéma pour le mapping de votre entity bean. Si aucune @Table n'est définie les valeurs par défaut sont utilisées : le nom de la classe de l'entité (sans le nom de package).
@Entity
@Table(name="tbl_sky")
public class Sky implements Serializable {
...
L'élément @Table contient aussi un attribut schema et un attribut catalog, si vous avez besoin de les définir. Vous pouvez aussi définir des contraintes d'unicité sur la table en utilisant l'annotation @UniqueConstraint en conjonction avec @Table (pour une contrainte d'unicité n'impliquant qu'une seule colonne, référez-vous à @Column).
@Table(name="tbl_sky",
uniqueConstraints = {@UniqueConstraint(columnNames={"month", "day"})}
)Une contrainte d'unicité est appliquée au tuple {month, day}. Notez que le tableau columnNames fait référence aux noms logiques des colonnes.
Le nom logique d'une colonne est défini par l'implémentation de NamingStrategy d'Hibernate. La stratégie de nommage EJB3 par défaut utilise le nom de colonne physique comme nom de colonne logique. Notez qu'il peut être différent du nom de la propriété (si le nom de colonne est explicite). A moins que vous surchargiez la stratégie de nommage, vous ne devriez pas vous soucier de ça.Vous pouvez ajouter un contrôle de concurrence optimiste à un entity bean en utilisant l'annotation @Version :
@Entity
public class Flight implements Serializable {
...
@Version
@Column(name="OPTLOCK")
public Integer getVersion() { ... }
} La propriété de version sera mappée sur la colonne OPTLOCK, et le gestionnaire d'entités l'utilisera pour détecter des conflits lors des mises à jour (prévenant des pertes de données lors de mises à jours que vous pourriez voir avec la stratégie du last-commit-wins).
La colonne de version peut être un numérique (solution recommandée) ou un timestamp comme pour la spécification EJB3. Hibernate prend en charge n'importe quel type fourni que vous définissez et implémentez avec la classe UserVersionType appropriée.
Chaque propriété (champ ou méthode) non statique non transient d'un entity bean est considérée persistante, à moins que vous l'annotiez comme @Transient. Ne pas avoir d'annotation pour votre propriété est équivalent à l'annotation @Basic. L'annotation @Basic vous permet de déclarer la stratégie de récupération pour une propriété :
public transient int counter; // propriété transient
private String firstname; // propriété persistante
@Transient
String getLengthInMeter() { ... } // propriété transient
String getName() {... } // propriété persistante
@Basic
int getLength() { ... } // propriété persistante
@Basic(fetch = FetchType.LAZY)
String getDetailedComment() { ... } // propriété persistante
@Temporal(TemporalType.TIME)
java.util.Date getDepartureTime() { ... } // propriété persistante
@Enumerated(EnumType.STRING)
Starred getNote() { ... } // enum persistée en tant que String dans la base de donnéescounter, un champ transient, et lengthInMeter, une méthode annotée comme @Transient, seront ignorés par le gestionnaire d'entités. Les propriétés name, length, et firstname sont mappées comme persistantes et à charger immédiatement (ce sont les valeurs par défaut pour les propriétés simples). La valeur de la propriété detailedComment sera chargée à partir de la base de données dès que la propriété de l'entité sera accédée pour la première fois. En général vous n'avez pas besoin de marquer de simples propriétés comme "à charger à la demande" (NdT: lazy) (à ne pas confondre avec la récupération d'association "lazy").
Pour activer la récupération à la demande au niveau de la propriété, vos classes doivent être instrumentées : du bytecode est ajouté au code original pour activer cette fonctionnalité, veuillez vous référer à la documentation de référence d'Hibernate. Si vos classes ne sont pas instrumentées, le chargement à la demande au niveau de la propriété est silencieusement ignoré.
L'alternative recommandée est d'utiliser la capacité de projection de JPA-QL ou des requêtes Criteria.
EJB3 prend en charge le mapping de propriété de tous les types élémentaires pris en charge par Hibernate (tous les types de base Java, leur wrapper respectif et les classes sérialisables). Hibernate Annotations prend en charge le mapping des types Enum soit vers une colonne ordinale (en stockant le numéro ordinal de l'enum), soit vers une colonne de type chaîne de caractères (en stockant la chaîne de caractères représentant l'enum) : la représentation de la persistance, par défaut ordinale, peut être surchargée grâce à l'annotation @Enumerated comme montré avec la propriété note de l'exemple.
Dans les APIs core de Java, la précision temporelle n'est pas définie. Lors du traitement de données temporelles vous pourriez vouloir décrire la précision attendue dans la base de données. Les données temporelles peuvent avoir une précision de type DATE, TIME, ou TIMESTAMP (c'est-à-dire seulement la date, seulement l'heure, ou les deux). Utilisez l'annotation @Temporal pour ajuster cela.
@Lob indique que la propriété devrait être persistée dans un Blob ou un Clob selon son type : java.sql.Clob, Character[], char[] et java.lang.String seront persistés dans un Clob. java.sql.Blob, Byte[], byte[] et les types sérialisables seront persistés dans un Blob.
@Lob
public String getFullText() {
return fullText;
}
@Lob
public byte[] getFullCode() {
return fullCode;
}
Si le type de la propriété implémente java.io.Serializable et n'est pas un type de base, et si la propriété n'est pas annotée avec @Lob, alors le type Hibernate serializable est utilisé.
La(les) colonne(s) utilisée(s) pour mapper une propriété peuvent être définies en utilisant l'annotation @Column. Utilisez-la pour surcharger les valeurs par défaut (voir la spécification EJB3 pour plus d'informations sur les valeurs par défaut). Vous pouvez utilisez cette annotation au niveau de la propriété pour celles qui sont :
pas du tout annotées
annotées avec @Basic
annotées avec @Version
annotées avec @Lob
annotées avec @Temporal
annotées avec @org.hibernate.annotations.CollectionOfElements (pour Hibernate uniquement)
@Entity
public class Flight implements Serializable {
...
@Column(updatable = false, name = "flight_name", nullable = false, length=50)
public String getName() { ... }
La propriété name est mappée sur la colonne flight_name, laquelle ne peut pas avoir de valeur nulle, a une longueur de 50 et ne peut pas être mise à jour (rendant la propriété immuable).
Cette annotation peut être appliquée aux propriétés habituelles ainsi qu'aux propriétés @Id ou @Version.
@Column(
name="columnName"; (1)
boolean unique() default false; (2)
boolean nullable() default true; (3)
boolean insertable() default true; (4)
boolean updatable() default true; (5)
String columnDefinition() default ""; (6)
String table() default ""; (7)
int length() default 255; (8)
int precision() default 0; // decimal precision (9)
int scale() default 0; // decimal scale| (1) | name (optionnel) : le nom de la colonne (par défaut le nom de la propriété) |
| (2) | unique (optionnel) : indique si la colonne fait partie d'une contrainte d'unicité ou non (par défaut false) |
| (3) | nullable (optionnel) : indique si la colonne peut avoir une valeur nulle (par défaut false). |
| (4) | insertable (optionnel) : indique si la colonne fera partie de la commande insert (par défaut true) |
| (5) | updatable (optionnel) : indique si la colonne fera partie de la commande update (par défaut true) |
| (6) | columnDefinition (optionnel) : surcharge le fragment DDL sql pour cette colonne en particulier (non portable) |
| (7) | table (optionnel) : définit la table cible (par défaut la table principale) |
| (8) | length (optionnel) : longueur de la colonne (par défaut 255) |
| (8) | precision (optionnel) : précision décimale de la colonne (par défaut 0) |
| (10) | scale (optionnel) : échelle décimale de la colonne si nécessaire (par défaut 0) |
Il est possible de déclarer un composant embarqué à l'intérieur d'une entité et même de surcharger le mapping de ses colonnes. Les classes de composant doivent être annotées au niveau de la classe avec l'annotation @Embeddable. Il est possible de surcharger le mapping de colonne d'un objet embarqué pour une entité particulière en utilisant les annotations @Embedded et @AttributeOverride sur la propriété associée :
@Entity
public class Person implements Serializable {
// Composant persistant utilisant les valeurs par défaut
Address homeAddress;
@Embedded
@AttributeOverrides( {
@AttributeOverride(name="iso2", column = @Column(name="bornIso2") ),
@AttributeOverride(name="name", column = @Column(name="bornCountryName") )
} )
Country bornIn;
...
}
@Embeddable
public class Address implements Serializable {
String city;
Country nationality; // par de surcharge ici
}
@Embeddable
public class Country implements Serializable {
private String iso2;
@Column(name="countryName") private String name;
public String getIso2() { return iso2; }
public void setIso2(String iso2) { this.iso2 = iso2; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
...
}
Un objet embarquable hérite du type d'accès de son entité d'appartenance (notez que vous pouvez surcharger cela en utilisant les annotations spécifiques à Hibernate @AccessType, voir Extensions d'Hibernate Annotation).
L'entity bean Person a deux propriétés composant, homeAddress et bornIn. La propriété homeAddress n'a pas été annotée, mais Hibernate devinera que c'est un composant persistant en cherchant l'annotation @Embeddable dans la classe Address. Nous surchargeons aussi le mapping d'un nom de colonne (pour bornCountryName) avec les annotations @Embedded et @AttributeOverride pour chaque attribut mappé de Country. Comme vous pouvez le voir, Country est aussi un composant imbriqué de Address, utilisant de nouveau la détection automatique d'Hibernate et les valeurs par défaut EJB3. Surcharger des colonnes d'objets embarqués d'objets (eux-mêmes) embarqués n'est actuellement pas pris en charge par la spécification EJB3, cependant, Hibernate Annotations le prend en charge à travers des expressions séparées par des points.
@Embedded
@AttributeOverrides( {
@AttributeOverride(name="city", column = @Column(name="fld_city") ),
@AttributeOverride(name="nationality.iso2", column = @Column(name="nat_Iso2") ),
@AttributeOverride(name="nationality.name", column = @Column(name="nat_CountryName") )
// les colonnes de nationality dans homeAddress sont surchargées
} )
Address homeAddress;Hibernate Annotations prend en charge une fonctionnalité de plus qui n'est pas explicitement prise en charge par la spécification EJB3. Vous pouvez annoter un objet embarqué avec l'annotation @MappedSuperclass pour rendre les propriétés de la classe parente persistantes (voir @MappedSuperclass pour plus d'informations).
Alors que ce n'est pas pris en charge par la spécification EJB3, Hibernate Annotations vous permet d'utiliser les annotations d'association dans un objet embarquable (ie @*ToOne ou @*ToMany). Pour surcharger les colonnes de l'association vous pouvez utiliser @AssociationOverride.
Si vous voulez avoir le même type d'objet embarquable deux fois dans la même entité, le nom de colonne par défaut ne fonctionnera pas : au moins une des colonnes devra être explicitée. Hibernate va au-delà de la spécification EJB3 et vous permet d'améliorer le mécanisme par défaut avec NamingStrategy. DefaultComponentSafeNamingStrategy est une petite amélioration par rapport à la stratégie par défaut EJB3NamingStrategy qui permet aux objets embarqués de fonctionner avec leur valeur par défaut même s'ils sont utilisés deux fois dans la même entité.
Si une propriété n'est pas annotée, les règles suivantes s'appliquent :
L'annotation @Id vous permet de définir quelle propriété identifie votre entity bean. Cette propriété peut être positionnée par l'application elle-même ou générée par Hibernate (préféré). Vous pouvez définir la stratégie de génération de l'identifiant grâce à l'annotation @GeneratedValue :
Hibernate fournit plus de générateurs d'identifiant que les simples générateurs EJB3. Vérifiez Extensions d'Hibernate Annotation pour plus d'informations.
L'exemple suivant montre un générateur par séquence utilisant la configuration SEQ_STORE (voir plus bas) :
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_STORE")
public Integer getId() { ... }
L'exemple suivant utilise le générateur identity :
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() { ... }
Le générateur AUTO est le type préféré pour les applications portables (vers différentes base de données). La configuration de la génération d'identifiant peut être partagée par différents mappings @Id avec l'attribut du générateur. Il y a différentes configurations disponibles avec @SequenceGenerator et @TableGenerator. La portée d'un générateur peut être l'application ou la classe. Les générateurs définis dans les classes ne sont pas visibles à l'extérieur de la classe et peuvent surcharger les générateurs de niveau applicatif. Les générateurs de niveau applicatif sont définis au niveau XML (voir Chapitre 3, Surcharger des méta-données à travers du XML) :
<table-generator name="EMP_GEN"
table="GENERATOR_TABLE"
pk-column-name="key"
value-column-name="hi"
pk-column-value="EMP"
allocation-size="20"/>
// et l'annotation équivalente
@javax.persistence.TableGenerator(
name="EMP_GEN",
table="GENERATOR_TABLE",
pkColumnName = "key",
valueColumnName = "hi"
pkColumnValue="EMP",
allocationSize=20
)
<sequence-generator name="SEQ_GEN"
sequence-name="my_sequence"
allocation-size="20"/>
// et l'annotation équivalente
@javax.persistence.SequenceGenerator(
name="SEQ_GEN",
sequenceName="my_sequence",
allocationSize=20
)
Si JPA XML (comme META-INF/orm.xml) est utilisé pour définir les générateurs, EMP_GEN et SEQ_GEN sont des générateurs de niveau applicatif. EMP_GEN définit un générateur d'identifiant basé sur une table utilisant l'algorithme hilo avec un max_lo de 20. La valeur haute est conservée dans une table "GENERATOR_TABLE". L'information est gardée dans une ligne où la colonne pkColumnName ("clef") est égale à pkColumnValue "EMP" et une colonne valueColumnName "hi" contient la prochaine valeur haute utilisée.
SEQ_GEN définit un générateur par séquence utilisant une séquence nommée my_sequence. La taille d'allocation utilisée pour cet algorithme hilo basé sur une séquence est 20. Notez que cette version d'Hibernate Annotations ne gère pas initialValue dans le générateur par séquence. La taille par défaut de l'allocation est 50, donc si vous voulez utiliser une séquence et récupérer la valeur chaque fois, vous devez positionner la taille de l'allocation à 1.
La définition au niveau package n'est plus prise en charge par la spécification EJB 3.0. Vous pouvez cependant utiliser @GenericGenerator au niveau du package (voir Section 2.4.2, « Identifiant »).
Le prochain exemple montre la définition d'un générateur par séquence dans la portée d'une classe :
@Entity
@javax.persistence.SequenceGenerator(
name="SEQ_STORE",
sequenceName="my_sequence"
)
public class Store implements Serializable {
private Long id;
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_STORE")
public Long getId() { return id; }
}
Cette classe utilisera une séquence nommée my_sequence et le générateur SEQ_STORE n'est pas visible dans les autres classes. Notez que vous pouvez regarder les tests unitaires d'Hibernate Annotations dans le package org.hibernate.test.metadata.id pour plus d'exemples.
Vous pouvez définir une clef primaire composée à travers différentes syntaxes :
Bien qu'assez commun pour le développeur EJB2, @IdClass est probablement nouveau pour les utilisateurs d'Hibernate. La classe de la clef primaire composée correspond aux multiples champs ou propriétés de l'entité ; de plus, les noms des champs ou propriétés de la clef primaire et ceux de l'entité doivent correspondre ; et enfin, leur type doit être le même. Regardons un exemple :
@Entity @IdClass(FootballerPk.class) public class Footballer { // partie de la clef @Id public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } // partie de la clef @Id public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } public String getClub() { return club; } public void setClub(String club) { this.club = club; } // implémentation appropriée de equals() et hashCode() } @Embeddable public class FootballerPk implements Serializable { // même nom et même type que dans Footballer public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } // même nom et même type que dans Footballer public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } // implémentation appropriée de equals() et hashCode() }
Comme vous pouvez le voir, @IdClass pointe vers la classe de la clef primaire correspondante.
Bien que ce ne soit pas pris en charge par la spécification EJB3, Hibernate vous permet de définir des associations à l'intérieur d'un identifiant composé. Pour cela, utilisez simplement les annotations habituelles.
@Entity
@AssociationOverride( name="id.channel", joinColumns = @JoinColumn(name="chan_id") )
public class TvMagazin {
@EmbeddedId public TvMagazinPk id;
@Temporal(TemporalType.TIME) Date time;
}
@Embeddable
public class TvMagazinPk implements Serializable {
@ManyToOne
public Channel channel;
public String name;
@ManyToOne
public Presenter presenter;
}
EJB3 prend en charge les trois types d'héritage :
La stratégie choisie est déclarée au niveau de la classe de l'entité la plus haute dans la hiérarhie en utilisant l'annotation @Inheritance.
Annoter des interfaces n'est pour le moment pas pris en charge.
Cette stratégie a beaucoup d'inconvénients (surtout avec les requêtes polymorphiques et les associations) expliqués dans la spécification EJB3, la documentation de référence d'Hibernate, Hibernate in Action, et plusieurs autres endroits. Hibernate en contourne la plupart en implémentant cette stratégie en utilisant des requêtes SQL UNION. Elle est habituellement utilisée pour le niveau le plus haut d'une hiérarchie d'héritage :
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Flight implements Serializable {
Cette stratégie prend en charge les associations de un vers plusieurs bidirectionnelles. Cette stratégie ne prend pas en charge la stratégie de générateur IDENTITY : l'identifiant doit être partagé par plusieurs tables. Par conséquent, lors de l'utilisation de cette stratégie, vous ne devriez pas utilisez AUTO ni IDENTITY.
Toutes les propriétés de toutes les classes parentes et classes filles sont mappées dans la même table, les instances sont différenciées par une colonne spéciale discriminante :
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
name="planetype",
discriminatorType=DiscriminatorType.STRING
)
@DiscriminatorValue("Plane")
public class Plane { ... }
@Entity
@DiscriminatorValue("A320")
public class A320 extends Plane { ... }
Plane est la classe parente, elle définit la stratégie d'héritage InheritanceType.SINGLE_TABLE. Elle définit aussi la colonne discriminante avec l'annotation @DiscriminatorColumn, une colonne discriminante peut aussi définir le type du discriminant. Finalement, l'annotation @DiscriminatorValue définit la valeur utilisée pour différencier une classe dans la hiérarchie. Tous ces attributs ont des valeurs par défaut sensées. Le nom par défaut de la colonne discriminante est DTYPE. La valeur discriminante par défaut est le nom de l'entité (comme défini dans @Entity.name) avec le type DiscriminatorType.STRING. A320 est une classe fille ; vous devez seulement définir la valeur discriminante si vous ne voulez pas utiliser la valeur par défaut. La stratégie et le type du discriminant sont implicites.
@Inheritance et @DiscriminatorColumn devraient seulement être définies sur l'entité la plus haute de la hiérarchie.
Les annotations @PrimaryKeyJoinColumn et @PrimaryKeyJoinColumns définissent la (les) clef(s) primaire(s) de la table de la classe fille jointe :
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class Boat implements Serializable { ... }
@Entity
public class Ferry extends Boat { ... }
@Entity
@PrimaryKeyJoinColumn(name="BOAT_ID")
public class AmericaCupClass extends Boat { ... }
Toutes les entités ci-dessus utilisent la stratégie JOINED, la table Ferry est jointe avec la table Boat en utilisant les mêmes noms de clef primaire. La table AmericaCupClass est jointe avec Boat en utilisant la condition de jointure Boat.id = AmericaCupClass.BOAT_ID.
Il est parfois utile de partager des propriétés communes à travers une classe technique ou métier sans l'inclure comme une entité habituelle (c'est-à-dire aucune table spécifique pour cette entité). Pour cela, vous pouvez les mapper comme @MappedSuperclass.
@MappedSuperclass
public class BaseEntity {
@Basic
@Temporal(TemporalType.TIMESTAMP)
public Date getLastUpdate() { ... }
public String getLastUpdater() { ... }
...
}
@Entity class Order extends BaseEntity {
@Id public Integer getId() { ... }
...
}En base de données, cette hiérarchie sera représentée comme une table Order ayant les colonnes id, lastUpdate et lastUpdater. Les mappings de propriété de la classe parente embarquée sont copiés dans les classes filles de l'entité. Souvenez-vous que la classe parente embarquable n'est cependant pas la racine de la hiérarchie.
Les propriétés des classes parentes non mappées comme @MappedSuperclass sont ignorées.
Le type d'accès (champ ou méthode) est hérité de l'entité racine, à moins que vous utilisiez l'annotation Hibernate @AccessType.
La même notion peut être appliquée aux objets @Embeddable pour persister des propriétés de leurs classes parentes. Vous avez aussi besoin d'utiliser @MappedSuperclass pour faire ça (cependant cela ne devrait pas être considéré comme une fonctionnalité EJB3 standard).
Il est permis de marquer une classe comme @MappedSuperclass dans le milieu d'une hiérarchie d'héritage mappée.
Toute classe de la hiérarchie non annotée avec @MappedSuperclass ou @Entity sera ignorée.
Vous pouvez surcharger des colonnes définies dans des entités parentes au niveau de l'entité racine en utilisant l'annotation @AttributeOverride.
@MappedSuperclass
public class FlyingObject implements Serializable {
public int getAltitude() {
return altitude;
}
@Transient
public int getMetricAltitude() {
return metricAltitude;
}
@ManyToOne
public PropulsionType getPropulsion() {
return metricAltitude;
}
...
}
@Entity
@AttributeOverride( name="altitude", column = @Column(name="fld_altitude") )
@AssociationOverride( name="propulsion", joinColumns = @JoinColumn(name="fld_propulsion_fk") )
public class Plane extends FlyingObject {
...
}La propriété altitude sera persistée dans la colonne fld_altitude de la table Plane et l'association propulsion sera matérialisée dans la colonne de clef étrangère fld_propulsion_fk.
Vous pouvez définir @AttributeOverride(s) et @AssociationOverride(s) sur des classes @Entity, des classes @MappedSuperclass et des propriétés pointant vers un objet @Embeddable.
Vous pouvez associer des entity beans avec une relation one-to-one en utilisant @OneToOne. Il y a trois cas pour les associations one-to-one : soit les entités associées partagent les mêmes valeurs de clef primaire, soit une clef étrangère est détenue par une des entités (notez que cette colonne de clef étrangère dans la base de données devrait être avoir une contrainte d'unicité pour simuler la cardinalité one-to-one), soit une table d'association est utilisée pour stocker le lien entre les 2 entités (une contrainte d'unicité doit être définie sur chaque clef étrangère pour assurer la cardinalité un à un).
Tout d'abord, nous mappons une véritable association one-to-one en utilisant des clefs primaires partagées :
@Entity
public class Body {
@Id
public Long getId() { return id; }
@OneToOne(cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
public Heart getHeart() {
return heart;
}
...
}
@Entity
public class Heart {
@Id
public Long getId() { ...}
}
L'association un à un est activée en utilisant l'annotation @PrimaryKeyJoinColumn.
Dans l'exemple suivant, les entités associées sont liées à travers une clef étrangère :
@Entity
public class Customer implements Serializable {
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name="passport_fk")
public Passport getPassport() {
...
}
@Entity
public class Passport implements Serializable {
@OneToOne(mappedBy = "passport")
public Customer getOwner() {
...
}
Un Customer est lié à un Passport, avec une colonne de clef étrangère nommée passport_fk dans la table Customer. La colonne de jointure est déclarée avec l'annotation @JoinColumn qui ressemble à l'annotation @Column. Elle a un paramètre de plus nommé referencedColumnName. Ce paramètre déclare la colonne dans l'entité cible qui sera utilisée pour la jointure. Notez que lors de l'utilisation de referencedColumnName vers une colonne qui ne fait pas partie de la clef primaire, la classe associée doit être Serializable. Notez aussi que referencedColumnName doit être mappé sur une propriété ayant une seule colonne lorsqu'elle pointe vers une colonne qui ne fait pas partie de la clef primaire (d'autres cas pourraient ne pas fonctionnner).
L'association peut être bidirectionnelle. Dans une relation bidirectionnelle, une des extrémités (et seulement une) doit être la propriétaire : la propriétaire est responsable de la mise à jour des colonnes de l'association. Pour déclarer une extrémité comme non responsable de la relation, l'attribut mappedBy est utilisé. mappedBy référence le nom de la propriété de l'association du côté du propriétaire. Dans notre cas, c'est passport. Comme vous pouvez le voir, vous ne devez (absolument) pas déclarer la colonne de jointure puisqu'elle a déjà été déclarée du côté du propriétaire.
Si aucune @JoinColumn n'est déclarée du côté du propriétaire, les valeurs par défaut s'appliquent. Une(des) colonne(s) de jointure sera(ont) créée(s) dans la table propriétaire, et son(leur) nom sera la concaténation du nom de la relation du côté propriétaire, _ (underscore), et le nom de la (des) colonne(s) de la clef primaire du propriétaire. Dans cet exemple passport_id parce que le nom de la propriété est passport et la colonne identifiante de Passport est id.
La troisième possibilité (utilisant une table d'association) est très exotique.
@Entity
public class Customer implements Serializable {
@OneToOne(cascade = CascadeType.ALL)
@JoinTable(name = "CustomerPassports",
joinColumns = @JoinColumn(name="customer_fk"),
inverseJoinColumns = @JoinColumn(name="passport_fk")
)
public Passport getPassport() {
...
}
@Entity
public class Passport implements Serializable {
@OneToOne(mappedBy = "passport")
public Customer getOwner() {
...
}
Un Customer est lié à un Passport à travers une table d'association nommée CustomerPassports ; cette table d'association a une colonne de clef étrangère nommée passport_fk pointant vers la table Passport (matérialisée par l'attribut inverseJoinColumn), et une colonne de clef étrangère nommée customer_fk pointant vers la table Customer (matérialisée par l'attribut joinColumns).
Vous devez déclarer le nom de la table de jointure et les colonnes de jointure explicitement dans un tel mapping.
Les associations Many-to-one sont déclarées au niveau de la propriété avec l'annotation @ManyToOne :
@Entity()
public class Flight implements Serializable {
@ManyToOne( cascade = {CascadeType.PERSIST, CascadeType.MERGE} )
@JoinColumn(name="COMP_ID")
public Company getCompany() {
return company;
}
...
}
L'attribut @JoinColumn est optionnel, la valeur par défaut est comme l'association un à un, la concaténation du nom de la relation du côté propriétaire, _ (underscore), et le nom de la colonne de la clef primaire du côté propriétaire. Dans cet exemple, company_id parce que le nom de la propriété est company et la colonne identifiante de Company est id.
@ManyToOne a un paramètre nommé targetEntity qui décrit le nom de l'entité cible. Généralement, vous ne devriez pas avoir besoin de ce paramètre puisque la valeur par défaut (le type de la propriété qui stocke l'association) est correcte dans la plupart des cas. Il est cependant utile lorsque vous souhaitez retourner une interface plutôt qu'une entité normale.
@Entity()
public class Flight implements Serializable {
@ManyToOne( cascade = {CascadeType.PERSIST, CascadeType.MERGE}, targetEntity=CompanyImpl.class )
@JoinColumn(name="COMP_ID")
public Company getCompany() {
return company;
}
...
}
public interface Company {
...
Vous pouvez sinon mapper une association plusieurs à un avec une table d'association. Cette association décrite par l'annotation @JoinTable contiendra une clef étrangère référençant la table de l'entité (avec @JoinTable.joinColumns) et une clef étrangère référençant la table de l'entité cible (avec @JoinTable.inverseJoinColumns).
@Entity()
public class Flight implements Serializable {
@ManyToOne( cascade = {CascadeType.PERSIST, CascadeType.MERGE} )
@JoinTable(name="Flight_Company",
joinColumns = @JoinColumn(name="FLIGHT_ID"),
inverseJoinColumns = @JoinColumn(name="COMP_ID")
)
public Company getCompany() {
return company;
}
...
}
Vous pouvez mapper des Collections, des Lists (ie des listes ordonnées, pas des listes indexées), des Maps et des Sets. La spécification EJB3 décrit comment mapper une liste ordonnée (ie une liste ordonnée au chargement) en utilisant l'annotation @javax.persistence.OrderBy : pour ordonner la collection, cette annotation prend en paramètre une liste de propriétés (de l'entité cible) séparées par des virgules (p. ex. firstname asc, age desc) ; si la chaîne de caractères est vide, la collection sera ordonnée par les identifiants. Pour le moment @OrderBy fonctionne seulement sur des collections n'ayant pas de table d'association. Pour les véritables collections indexées, veuillez vous référer à Extensions d'Hibernate Annotation. EJB3 vous permet de mapper des Maps en utilisant comme clef une des propriétés de l'entité cible avec @MapKey(name="myProperty") (myProperty est un nom de propriété de l'entité cible). Lorsque vous utilisez @MapKey sans nom de propriété, la clef primaire de l'entité cible est utilisée. La clef de la map utilise la même colonne que celle pointée par la propriété : il n'y a pas de colonne supplémentaire définie pour la clef de la map, et c'est normal puisque la clef de la map représente en fait un propriété de la cible. Faites attention qu'une fois chargée, la clef n'est plus synchronisée avec la propriété, en d'autres mots, si vous modifiez la valeur de la propriété, la clef ne sera pas changée automatiquement dans votre modèle Java (pour une véritable prise en charge des maps veuillez vous référer à Extensions d'Hibernate Annotation). Beaucoup de gens confondent les capacités de <map> et celles de @MapKey. Ce sont deux fonctionnalités différentes. @MapKey a encore quelques limitations, veuillez vous référer au forum ou au système de suivi de bogues JIRA pour plus d'informations.
Hibernate a plusieurs notions de collections.
Tableau 2.1. Sémantique des collections
| Sémantique | Représentation Java | Annotations |
|---|---|---|
| Sémantique de Bag | java.util.List, java.util.Collection | @org.hibernate.annotations.CollectionOfElements ou @OneToMany ou @ManyToMany |
| Sémantique de Bag avec une clef primaire (sans les limitations de la sémantique de Bag) | java.util.List, java.util.Collection | (@org.hibernate.annotations.CollectionOfElements ou @OneToMany ou @ManyToMany) et @CollectionId |
| Sémantique de List | java.util.List | (@org.hibernate.annotations.CollectionOfElements ou @OneToMany ou @ManyToMany) et @org.hibernate.annotations.IndexColumn |
| Sémantique de Set | java.util.Set | @org.hibernate.annotations.CollectionOfElements ou @OneToMany ou @ManyToMany |
| Sémantique de Map | java.util.Map | (@org.hibernate.annotations.CollectionOfElements ou @OneToMany ou @ManyToMany) et (rien ou @org.hibernate.annotations.MapKey/MapKeyManyToMany pour une véritable prise en charge des maps, ou @javax.persistence.MapKey |
Les collections de types primitifs, de types core ou d'objets embarqués ne sont pas prises en charge par la spécification EJB3. Cependant Hibernate Annotations les autorise (voir Extensions d'Hibernate Annotation).
@Entity public class City {
@OneToMany(mappedBy="city")
@OrderBy("streetName")
public List<Street> getStreets() {
return streets;
}
...
}
@Entity public class Street {
public String getStreetName() {
return streetName;
}
@ManyToOne
public City getCity() {
return city;
}
...
}
@Entity
public class Software {
@OneToMany(mappedBy="software")
@MapKey(name="codeName")
public Map<String, Version> getVersions() {
return versions;
}
...
}
@Entity
@Table(name="tbl_version")
public class Version {
public String getCodeName() {...}
@ManyToOne
public Software getSoftware() { ... }
...
}Donc City a une collection de Streets qui sont ordonnées par streetName (de Street) lorsque la collection est chargée. Software a une map de Versions dont la clef est codeName de Version.
A moins que la collection soit une "generic", vous devrez définir targetEntity. C'est un attribut de l'annotation qui prend comme valeur la classe de l'entité cible.
Les associations one-to-many sont déclarées au niveau propriété avec l'annotation @OneToMany. Les associations un à plusieurs peuvent être bidirectionnelles.
Puisque les associations plusieurs à un sont (presque) toujours l'extrémité propriétaire de la relation bidirectionnelle dans la spécification EJB3, l'association un à plusieurs est annotée par @OneToMany(mappedBy=...).
@Entity
public class Troop {
@OneToMany(mappedBy="troop")
public Set<Soldier> getSoldiers() {
...
}
@Entity
public class Soldier {
@ManyToOne
@JoinColumn(name="troop_fk")
public Troop getTroop() {
...
} Troop a une relation bidirectionnelle un à plusieurs avec Soldier à travers la propriété troop. Vous ne devez pas définir de mapping physique à l'extrémité de mappedBy.
Pour mapper une relation bidirectionnelle un à plusieurs, avec l'extrémité one-to-many comme extrémité propriétaire, vous devez enlever l'élément mappedBy et marquer l'annotation @JoinColumn de l'extrémité plusieurs à un comme ne pouvant pas être insérée et ni mise à jour. Cette solution n'est certainement pas optimisée et produira quelques commandes UPDATE supplémentaires.
@Entity
public class Troop {
@OneToMany
@JoinColumn(name="troop_fk") // nous avons besoin de dupliquer l'information physique
public Set<Soldier> getSoldiers() {
...
}
@Entity
public class Soldier {
@ManyToOne
@JoinColumn(name="troop_fk", insertable=false, updatable=false)
public Troop getTroop() {
...
}Une relation un à plusieurs unidirectionnelle utilisant une colonne de clef étrangère de l'entité propriétaire n'est pas si commune, réellement recommandée. Nous vous conseillons fortement d'utiliser une table de jointure pour cette sorte d'association (comme expliqué dans la prochaine section). Cette sorte d'association est décrite à travers @JoinColumn.
@Entity
public class Customer implements Serializable {
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="CUST_ID")
public Set<Ticket> getTickets() {
...
}
@Entity
public class Ticket implements Serializable {
... // pas de relation bidirectionnelle
}
Customer décrit une relation unidirectionnelle avec Ticket en utilisant la colonne de jointure CUST_ID.
Une relation unidirectionnelle un à plusieurs avec une table de jointure est largement préférée. Cette association est décrite à travers l'annotation @JoinTable.
@Entity
public class Trainer {
@OneToMany
@JoinTable(
name="TrainedMonkeys",
joinColumns = @JoinColumn( name="trainer_id"),
inverseJoinColumns = @JoinColumn( name="monkey_id")
)
public Set<Monkey> getTrainedMonkeys() {
...
}
@Entity
public class Monkey {
... // pas de relation bidirectionnelle
}
Trainer décrit une relation unidirectionelle avec Monkey en utilisant la table de jointure TrainedMonkeys, avec une clef étrangère trainer_id vers Trainer (joinColumns) et une clef étrangère monkey_id vers Monkey (inversejoinColumns).
Si aucun mapping physique n'est déclaré, une relation unidirectionnelle un vers plusieurs utilise une table de jointure. Le nom de la table est la concaténation du nom de la table propriétaire, _, et le nom de la table de l'autre extrémité. Le nom des colonnes de la clef étrangère référençant la table propriétaire est la concaténation de la table propriétaire, _, et le nom des colonnes de la clef primaire. Le nom des colonnes de la clef étrangère référençant l'autre extrémité est la concaténation du nom de la propriété du propriétaire, _, et le nom des colonnes de la clef primaire de l'autre extrémité. Une contrainte d'unicité est ajoutée sur la clef étrangère référençant la table de l'autre extrémité pour réfléter le un à plusieurs.
@Entity
public class Trainer {
@OneToMany
public Set<Tiger> getTrainedTigers() {
...
}
@Entity
public class Tiger {
... // non bidirectionnelle
}
Trainer décrit une relation unidirectionnelle avec Tiger utilisant la table de jointure Trainer_Tiger, avec une clef étrangère trainer_id vers Trainer (nom de la table, _, identifiant de trainer) et une clef étrangère trainedTigers_id vers Monkey (nom de la propriété, _, colonne de la clef primaire de Tiger).
Une association many-to-many est définie logiquement en utilisant l'annotation @ManyToMany. Vous devez aussi décrire la table d'association et les conditions de jointure en utilisant l'annotation @JoinTable. Si l'association est bidirectionnelle, une extrémité doit être la propriétaire et l'autre doit être marquée comme "inverse" (ie qu'elle sera ignorée lors de la mise à jour des valeurs de la relation dans la table d'association) :
@Entity
public class Employer implements Serializable {
@ManyToMany(
targetEntity=org.hibernate.test.metadata.manytomany.Employee.class,
cascade={CascadeType.PERSIST, CascadeType.MERGE}
)
@JoinTable(
name="EMPLOYER_EMPLOYEE",
joinColumns=@JoinColumn(name="EMPER_ID"),
inverseJoinColumns=@JoinColumn(name="EMPEE_ID")
)
public Collection getEmployees() {
return employees;
}
...
}
@Entity
public class Employee implements Serializable {
@ManyToMany(
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "employees",
targetEntity = Employer.class
)
public Collection getEmployers() {
return employers;
}
}
Nous avons déjà montré les déclarations des relations "many" et détaillé les attributs de ces associations. Allons plus en profondeur dans la description de @JoinTable ; elle définit un name, un tableau de colonnes de jointure (un tableau dans une annotation est défini par {A, B, C}), et un tableau de colonnes de jointure inverse. Ces dernières sont les colonnes de la table d'association qui référencent la clef primaire de Employee ("l'autre extrémité").
Comme vu précédemment, l'autre extrémité ne doit pas décrire le mapping physique : un simple argument mappedBy contenant le nom de la propriété de l'extrémité propriétaire suffit à relier les deux.
Comme d'autres annotations, la plupart des valeurs d'une relation plusieurs à plusieurs sont inférées. Si aucun mapping physique n'est décrit dans une relation plusieurs à plusieurs unidirectionnelle, alors les règles suivantes s'appliquent. Le nom de la table est la concaténation du nom de la table propriétaire, _ et le nom de la table de l'autre extrémité. Le nom des colonnes de la clef étrangère référençant la table propriétaire est la concaténation du nom de la table propriétaire, _ et le nom des colonnes de la clef primaire de cette table. Le nom des colonnes de la clef étrangère référençant l'autre extrémité est la concaténation du nom de la propriété du propriétaire, _ et le nom des colonnes de la clef primaire de l'autre extrémité. Ce sont les mêmes règles que celles utilisées pour une relation un à plusieurs unidirectionnelle.
@Entity
public class Store {
@ManyToMany(cascade = CascadeType.PERSIST)
public Set<City> getImplantedIn() {
...
}
}
@Entity
public class City {
... // pas de relation bidirectionnelle
}
La table Store_City est utilisée comme table de jointure. La colonne Store_id est une clef étrangère vers la table Store. La colonne implantedIn_id est une clef étrangère vers la table City.
Si aucun mapping physique n'est décrit dans une relation plusieurs à plusieurs bidirectionnelle, alors les règles suivantes s'appliquent. Le nom de la table est la concaténation du nom de la table propriétaire, _ et le nom de la table de l'autre extrémité. Le nom des colonnes de la clef étrangère référençant la table propriétaire est la concaténation du nom de la propriété de l'autre extrémité, _ et le nom des colonnes de la clef primaire du propriétaire. Le nom des colonnes de la clef étrangère référençant l'autre extrémité est la concaténation du nom de la propriété du propriétaire, _ et le nom des colonnes de la clef primaire de l'autre extrémité. Ce sont les mêmes règles que celles utilisées pour une relation un à plusieurs unidirectionnelle.
@Entity
public class Store {
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public Set<Customer> getCustomers() {
...
}
}
@Entity
public class Customer {
@ManyToMany(mappedBy="customers")
public Set<Store> getStores() {
...
}
}
La table Store_Customer est utilisée comme table de jointure. La colonne stores_id est une clef étrangère vers la table Store. La colonne customers_id est une clef étrangère vers la table Customer.
Vous avez probablement remarqué l'attribut cascade prenant comme valeur un tableau de CascadeTypes. Le concept de cascade dans EJB3 est similaire à la persistance transitive et les opérations en cascade dans Hibernate, mais avec une sémantique légèrement différente et les types de cascade suivants :
Veullez vous référer au chapitre 6.3 de la spécification EJB3 pour plus d'informations sur les opérations en cascade et la sémantique des opérations de création/fusion.
Vous avez la possibilité de récupérer les entités associées soit immédiatement ("eager"), soit à la demande ("lazy"). Le paramètre fetch peut être positionné à FetchType.LAZY ou à FetchType.EAGER. EAGER essaiera d'utiliser une jointure externe pour rappatrier l'objet associé, alors que LAZY est la valeur par défaut et rapportera les données lorsque l'objet associé sera accédé pour la première fois. JPA-QL a aussi un mot clef fetch qui vous permet de surcharger le type de récupération pour une requête particulière. C'est très utile pour améliorer les performances et décider au cas par cas.
Les clefs primaires composées utilisent une classe embarquée comme représentation de la clef primaire, donc vous devriez utiliser les annotations @Id et @Embeddable. Alternativement, vous pouvez utiliser l'annotation @EmbeddedId. Notez que la classe dépendante doit être sérialisable et implementer equals()/hashCode(). Vous pouvez aussi utiliser @IdClass comme décrit dans Mapper des propriétés identifiantes.
@Entity
public class RegionalArticle implements Serializable {
@Id
public RegionalArticlePk getPk() { ... }
}
@Embeddable
public class RegionalArticlePk implements Serializable { ... }
ou alternativement
@Entity
public class RegionalArticle implements Serializable {
@EmbeddedId
public RegionalArticlePk getPk() { ... }
}
public class RegionalArticlePk implements Serializable { ... }
@Embeddable hérite le type d'accès de son entité d'appartenance à moins que l'annotation spécifique Hibernate @AccessType soit utilisée. Les clefs étrangères composées (si les valeurs par défaut ne sont pas utilisées) sont définies sur les associations en utilisant l'élément @JoinColumns, lequel est simplement un tableau de @JoinColumns. Il est considéré comme une bonne pratique d'exprimer referencedColumnNames explicitement. Sinon, Hibernate supposera que vous utilisez le même ordre de colonnes que dans la déclaration de la clef primaire.
@Entity
public class Parent implements Serializable {
@Id
public ParentPk id;
public int age;
@OneToMany(cascade=CascadeType.ALL)
@JoinColumns ({
@JoinColumn(name="parentCivility", referencedColumnName = "isMale"),
@JoinColumn(name="parentLastName", referencedColumnName = "lastName"),
@JoinColumn(name="parentFirstName", referencedColumnName = "firstName")
})
public Set<Child> children; //unidirectionnelle
...
}
@Entity
public class Child implements Serializable {
@Id @GeneratedValue
public Integer id;
@ManyToOne
@JoinColumns ({
@JoinColumn(name="parentCivility", referencedColumnName = "isMale"),
@JoinColumn(name="parentLastName", referencedColumnName = "lastName"),
@JoinColumn(name="parentFirstName", referencedColumnName = "firstName")
})
public Parent parent; // unidirectionnelle
}
@Embeddable
public class ParentPk implements Serializable {
String firstName;
String lastName;
...
}
Notez l'usage explicite de referencedColumnName.
Vous pouvez mapper un simple entity bean vers plusieurs tables en utilisant les annotations de niveau classe @SecondaryTable ou @SecondaryTables. Pour dire qu'une colonne est dans une table particulière, utlisez le paramètre table de @Column ou @JoinColumn.
@Entity @Table(name="MainCat") @SecondaryTables({ @SecondaryTable(name="Cat1", pkJoinColumns={ @PrimaryKeyJoinColumn(name="cat_id", referencedColumnName="id") ), @SecondaryTable(name="Cat2", uniqueConstraints={@UniqueConstraint(columnNames={"storyPart2"})}) }) public class Cat implements Serializable { private Integer id; private String name; private String storyPart1; private String storyPart2; @Id @GeneratedValue public Integer getId() { return id; } public String getName() { return name; } @Column(table="Cat1") public String getStoryPart1() { return storyPart1; } @Column(table="Cat2") public String getStoryPart2() { return storyPart2; }
Dans cet exemple, name sera dans MainCat. storyPart1 sera dans Cat1 et storyPart2 sera dans Cat2. Cat1 sera joint à MainCat avec cat_id comme clef étrangère, et Cat2 avec id (ie le même nom de colonne que la colonne identifiante de MainCat). De plus, une contrainte d'unicité sur storyPart2 a été renseignée.
Regardez le tutoriel EJB3 de JBoss ou la suite de tests unitaires d'Hibernate Annotations pour plus d'exemples.
Vous pouvez mapper des requêtes JPA-QL/HQL en utilisant les annotations. @NamedQuery et @NamedQueries peuvent être définies au niveau de la classe ou dans un fichier JPA XML. Cependant, leurs définitions sont globales au scope de la session factory/entity manager factory. Une requête nommée est définie par son nom et la chaîne de caractères de la requête réelle.
<entity-mappings>
<named-query name="plane.getAll">
<query>select p from Plane p</query>
</named-query>
...
</entity-mappings>
...
@Entity
@NamedQuery(name="night.moreRecentThan", query="select n from Night n where n.date >= :date")
public class Night {
...
}
public class MyDao {
doStuff() {
Query q = s.getNamedQuery("night.moreRecentThan");
q.setDate( "date", aMonthAgo );
List results = q.list();
...
}
...
}
Vous pouvez aussi fournir des indications de fonctionnement à une requête à travers un tableau de QueryHints avec l'attribut hints.
Les indications de fonctionnement Hibernate disponibles sont :
Tableau 2.2. Indications de fonctionnement d'une requête
| Indication | description |
|---|---|
| org.hibernate.cacheable | Indique si la requête devrait interagir avec le cache de second niveau (par défaut à false) |
| org.hibernate.cacheRegion | Nom de la région du cache (si indéfinie, la valeur par défaut est utilisée) |
| org.hibernate.timeout | Timeout des requêtes |
| org.hibernate.fetchSize | Taille des result sets par fetch |
| org.hibernate.flushMode | Mode de flush utilisé pour cette requête |
| org.hibernate.cacheMode | Mode de cache utilisé pour cette requête |
| org.hibernate.readOnly | Indique si les entités chargées par cette requête devraient être en lecture seule ou pas (par défaut à false) |
| org.hibernate.comment | Commentaire de la requête, ajouté au SQL généré |
Vous pouvez aussi mapper une requête native (ie une requête SQL). Pour ce faire, vous devez décrire la structure de l'ensemble de résultat SQL en utilisant @SqlResultSetMapping (ou @SqlResultSetMappings si vous prévoyez de définir plusieurs mappings de résultats). Comme @NamedQuery, un @SqlResultSetMapping peut être défini au niveau de la classe ou dans un fichier XML JPA. Cependant sa portée est globale à l'application.
Comme vous le verrez, un paramètre de resultSetMapping est défini dans @NamedNativeQuery, il représente le nom du @SqlResultSetMapping défini. Le mapping de l'ensemble des résultats déclare les entités récupérées par cette requête native. Chaque champ de l'entité est lié à un alias SQL (nom de colonne). Tous les champs de l'entité (dont ceux des classes filles) et les colonnes des clefs étrangères relatives aux entités doivent être présents dans la requête SQL. Les définitions des champs sont optionnelles, si elles ne sont pas fournies, elles mappent le même nom de colonne que celui déclaré sur la propriété de la classe.
@NamedNativeQuery(name="night&area", query="select night.id nid, night.night_duration, "
+ " night.night_date, area.id aid, night.area_id, area.name "
+ "from Night night, Area area where night.area_id = area.id", resultSetMapping="joinMapping")
@SqlResultSetMapping(name="joinMapping", entities={
@EntityResult(entityClass=org.hibernate.test.annotations.query.Night.class, fields = {
@FieldResult(name="id", column="nid"),
@FieldResult(name="duration", column="night_duration"),
@FieldResult(name="date", column="night_date"),
@FieldResult(name="area", column="area_id"),
discriminatorColumn="disc"
}),
@EntityResult(entityClass=org.hibernate.test.annotations.query.Area.class, fields = {
@FieldResult(name="id", column="aid"),
@FieldResult(name="name", column="name")
})
}
)Dans l'exemple ci-dessus, la requête nommée night&area utilise le mapping de résultats joinMapping. Ce mapping retourne 2 entités, Night et Area, chaque propriété est déclarée et associée à un nom de colonne, en fait le nom de colonne récupéré par la requête. Voyons maintenant une déclaration implicite de mapping propriété/colonne.
@Entity
@SqlResultSetMapping(name="implicit", entities=@EntityResult(entityClass=org.hibernate.test.annotations.query.SpaceShip.class))
@NamedNativeQuery(name="implicitSample", query="select * from SpaceShip", resultSetMapping="implicit")
public class SpaceShip {
private String name;
private String model;
private double speed;
@Id
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column(name="model_txt")
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}Dans cet exemple, nous décrivons seulement le membre de l'entité du mapping de résultats. Le mapping de propriété/colonne est fait en utilisant les valeurs de mapping de l'entité. Dans ce cas, la propriété model est liée à la colonne model_txt. Si l'association à une entité concernée implique une clef primaire composée, un élément @FieldResult devrait être utilisé pour chaque colonne de la clef étrangère. Le nom de @FieldResult est composé du nom de la propriété pour la relation, suivi par un point ("."), suivi par le nom ou le champ ou la propriété de la clef primaire.
@Entity
@SqlResultSetMapping(name="compositekey",
entities=@EntityResult(entityClass=org.hibernate.test.annotations.query.SpaceShip.class,
fields = {
@FieldResult(name="name", column = "name"),
@FieldResult(name="model", column = "model"),
@FieldResult(name="speed", column = "speed"),
@FieldResult(name="captain.firstname", column = "firstn"),
@FieldResult(name="captain.lastname", column = "lastn"),
@FieldResult(name="dimensions.length", column = "length"),
@FieldResult(name="dimensions.width", column = "width")
}),
columns = { @ColumnResult(name = "surface"),
@ColumnResult(name = "volume") } )
@NamedNativeQuery(name="compositekey",
query="select name, model, speed, lname as lastn, fname as firstn, length, width, length * width as surface from SpaceShip",
resultSetMapping="compositekey")
} )
public class SpaceShip {
private String name;
private String model;
private double speed;
private Captain captain;
private Dimensions dimensions;
@Id
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumns( {
@JoinColumn(name="fname", referencedColumnName = "firstname"),
@JoinColumn(name="lname", referencedColumnName = "lastname")
} )
public Captain getCaptain() {
return captain;
}
public void setCaptain(Captain captain) {
this.captain = captain;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
public Dimensions getDimensions() {
return dimensions;
}
public void setDimensions(Dimensions dimensions) {
this.dimensions = dimensions;
}
}
@Entity
@IdClass(Identity.class)
public class Captain implements Serializable {
private String firstname;
private String lastname;
@Id
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
@Id
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
}
Si vous regardez la propriété dimension, vous verrez qu'Hibernate prend en charge la notation avec les points pour les objets embarqués (vous pouvez même avoir des objets embarqués imbriqués). Les implémentations EJB3 n'ont pas à prendre en charge cette fonctionnalité, mais nous le faisons :-)
Si vous récupérez une simple entité et si vous utilisez le mapping par défaut, vous pouvez utiliser l'attribut resultClass à la place de resultSetMapping :
@NamedNativeQuery(name="implicitSample", query="select * from SpaceShip",
resultClass=SpaceShip.class)
public class SpaceShip {Dans certaines de vos requêtes natives, vous devrez retourner des valeurs scalaires, par exemple lors de la construction de requêtes de rapport. Vous pouvez les mapper dans @SqlResultsetMapping avec @ColumnResult. En fait, vous pouvez même mélanger des retours d'entités et de valeurs scalaires dans la même requête native (ce n'est cependant probablement pas commun).
@SqlResultSetMapping(name="scalar", columns=@ColumnResult(name="dimension"))
@NamedNativeQuery(name="scalar", query="select length*width as dimension from SpaceShip", resultSetMapping="scalar")Une autre indication de fonctionnement spécifique aux requêtes natives a été présentée : org.hibernate.callable laquelle peut être à true ou à false fausse selon que la requête est une procédure stockée ou pas.
Hibernate 3.1 offre une variété d'annotations supplémentaires que vous pouvez mélanger/faire correspondre avec des entités EJB3. Elles ont été conçues comme une extension naturelle aux annotations EJB3.
Pour aller plus loin que les capacités d'EJB3, Hibernate fournit des annotations spécifiques qui correspondent aux fonctionnalités d'Hibernate. Le package org.hibernate.annotations contient toutes ces extensions d'annotations.
Vous pouvez finement paramétrer certaines des actions faites par Hibernate sur les entités au-delà de ce qu'offre la spécification EJB3.
@org.hibernate.annotations.Entity ajoute des méta-données supplémentaires qui peuvent être nécessaires au-delà de ce qui est défini dans l'annotation @Entity standard :
@javax.persistence.Entity est encore obligatoire, @org.hibernate.annotations.Entity ne la remplace pas.
Voici quelques extensions d'annotations Hibernate supplémentaires.
@org.hibernate.annotations.BatchSize vous permet de définir la taille du batch lors de la récupération d'instances de cette entité (p. ex. @BatchSize(size=4)). Lors du chargement d'une entité donnée, Hibernate chargera alors toutes les entités non initialisées du même type dans le contexte de la persistance jusqu'à la taille du batch.
@org.hibernate.annotations.Proxy définit les attributs de chargement de l'entité. lazy (valeur par défaut) définit si la classe est chargée à la demande ou non. proxyClassName est l'interface utilisée pour générer le proxy (par défaut, la classe elle-même).
@org.hibernate.annotations.Where définit une clause WHERE SQL optionnelle utilisée lorsque des instances de cette classe sont récupérées.
@org.hibernate.annotations.Check déclare une contrainte de vérification optionnelle définie dans l'expression DDL.
@OnDelete(action=OnDeleteAction.CASCADE) sur des classes filles jointes : utilise une commande SQL DELETE en cascade lors de la suppression plutôt que le mécanisme habituel d'Hibernate.
@Table(appliesTo="tableName", indexes = { @Index(name="index1", columnNames={"column1", "column2"} ) } ) crée les index définis sur les colonnes de la table tableName. Cela peut s'appliquer sur une table primaire ou une table secondaire. L'annotation @Tables vous permet d'avoir des index sur des tables différentes. Cette annotation est attendue là où @javax.persistence.Table ou @javax.persistence.SecondaryTable(s) sont déclarées.
@org.hibernate.annotations.Table est un complément, pas un remplacement de @javax.persistence.Table. Surtout, si vous souhaitez changer le nom par défaut d'une table, vous devez utiliser @javax.persistence.Table, pas @org.hibernate.annotations.Table.
@Entity
@BatchSize(size=5)
@org.hibernate.annotations.Entity(
selectBeforeUpdate = true,
dynamicInsert = true, dynamicUpdate = true,
optimisticLock = OptimisticLockType.ALL,
polymorphism = PolymorphismType.EXPLICIT)
@Where(clause="1=1")
@org.hibernate.annotations.Table(name="Forest", indexes = { @Index(name="idx", columnNames = { "name", "length" } ) } )
public class Forest { ... }@Entity
@Inheritance(
strategy=InheritanceType.JOINED
)
public class Vegetable { ... }
@Entity
@OnDelete(action=OnDeleteAction.CASCADE)
public class Carrot extends Vegetable { ... }@org.hibernate.annotations.GenericGenerator vous permet de définir un générateur d'identifiants Hibernate spécifique.
@Id @GeneratedValue(generator="system-uuid")
@GenericGenerator(name="system-uuid", strategy = "uuid")
public String getId() {
@Id @GeneratedValue(generator="hibseq")
@GenericGenerator(name="hibseq", strategy = "seqhilo",
parameters = {
@Parameter(name="max_lo", value = "5"),
@Parameter(name="sequence", value="heybabyhey")
}
)
public Integer getId() {strategy est le nom court de la stratégie du générateur Hibernate3 ou le nom pleinement qualifié de la classe d'une implémentation de IdentifierGenerator. Vous pouvez ajouter des paramètres avec l'attribut parameters.
Contrairement à son pendant standard, @GenericGenerator peut ête utilisée dans les annotations au niveau du package, en faisant ainsi un générateur de niveau applicatif (comme s'il était dans un fichier JPA XML).
@GenericGenerator(name="hibseq", strategy = "seqhilo",
parameters = {
@Parameter(name="max_lo", value = "5"),
@Parameter(name="sequence", value="heybabyhey")
}
)
package org.hibernate.test.modelLe type d'accès est déduit de la position de @Id ou de @EmbeddedId dans la hiérarchie de l'entité. Les entités filles, les objets embarqués et les entités parentes mappés héritent du type d'accès de l'entité racine.
Dans Hibernate, vous pouvez surcharger le type d'accès pour :
utiliser une stratégie d'accès personnalisée
paramétrer finement le type d'accès au niveau de la classe ou au niveau de la propriété
Une annocation @AccessType a été présentée pour prendre en charge ce comportement. Vous pouvez définir le type d'accès sur :
une enti