Il faut commencer par créer un projet JPA et ensuite configurer le fichier persistence.xml
.
Hibernate
semble ne pas supporter si les champs persistence
sont dans un namespace
. EclipseLink
a l'air de le supporter mais c'est Eclipse
qui alors semble mal le prendre.
exclude-unlisted-classes
: exclut toutes les classes non explicitement @Entity
.
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"> <persistence-unit name="Base"> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/database" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="javax.persistence.jdbc.password" value="aui" /> <!-- EclipseLink should create the database schema automatically --> <property name="eclipselink.ddl-generation" value="create-tables" /> <property name="eclipselink.logging.level" value="SEVERE" /> <property name="eclipselink.logging.level.sql" value="FINE" /> <property name="eclipselink.logging.parameters" value="true" /> </properties> </persistence-unit> </persistence>
eclipselink.ddl-generation
:
create-tables
: créera les tables si elles n'existent pas. Si elles existent et qu'elles ne correspondent pas au profil de l'entité, une exception sera générée.create-or-extend-tables
: créera les tables sur elles n'existent pas et ajoutera les colonnes manquantes.drop-and-create-tables
: supprimer et recrée les tables à chaque lancement de l'application.
Pour hibernate, il est possible d'utiliser soit le fichier persistence.xml
(automatiquement généré dans le dossier WEB-INF
et accessible depuis Project Explorer
dans la rubrique Java Resources
), soit hibernate.cfg.xml
(à mettre dans le dossier WEB-INF/classes
, à créer si nécessaire). Il est possible de générer automatiquement le fichier xml hibernate.cfg.xml
avec la clé DTD
: -//Hibernate/Hibernate Configuration DTD 3.0//EN
.
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd "> <persistence-unit name="demojpa-pu"> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/demojpa" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="javax.persistence.jdbc.password" value="aui" /> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" /> <property name="hibernate.hbm2ddl.auto" value="create" /> <property name="hibernate.archive.autodetection" value="class" /> <property name="hibernate.format_sql" value="true" /> </properties> </persistence-unit> </persistence>
hibernate.hbm2ddl.auto
:
validate
: validate the schema, makes no changes to the database.update
: update the schema.create
: creates the schema, destroying previous data.create-drop
: drop the schema at the end of the session.<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd" > <hibernate-configuration> <session-factory> <!-- local connection properties --> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/demojpa</property> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.username">orm</property> <property name="hibernate.connection.password">orm</property> <property name="hibernate.connection.pool_size">10</property> <!-- dialect for MySQL --> <property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property> <property name="hibernate.show_sql">true</property> <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property> <property name="cache.use_query_cache">false</property> </session-factory> </hibernate-configuration>
Certaines classes ont besoin d'implémenter l'interface Serializable
.
If an entity instance is to be passed by value as a detached object (e.g., through a remote interface), the entity class must implement the
Serializable
interface.JSR 338: Java TM Persistence API, Version 2.1, Archive
La classe ne doit pas être finale et doit avoir un constructeur sans argument explicitement écrit avec une visibilité publique ou protégée.
L'une des quatre annotation est nécessaire @Stateful
, @Stateless
, @MessageDriven
ou @Entity
.
Les beans de type Stateful
sont capables de conserver leur état.
Les beans de type stateless
n'ont par leur état conservé.
Les beans de type MessageDriven
permettent de réaliser des traitements asynchrones exécutés à la réception d'un message dans une queue JMS.
Les beans de type Entity
utilisent la persistance (JPA
). Par opposition aux composants (@Embeddable
) qui sont des objets sans identifiant qui nécessitent d'être attachés à une entité pour être sauvegardés.
Pour qu'une classe puisse être embarquée dans une entité, il faut que la classe :
@Embeddable
,@Id
.
Si on souhaite utiliser deux fois la même classe @Embeddable
, il va y avoir un problème puisque les attributs seront stockées dans la même colonne. Il faut donc utiliser @AttributeOverride
qui dire qu'on souhaite remplacer un attribut précédemment défini par un autre.
@Embedded @AttributeOverrides( { // adresse est le nom de l'attribut de la classe ''@Embeddable''. @AttributeOverride(name="adresse", column = @Column(name="adresse_pro") ), @AttributeOverride(name="codePostal", column = @Column(name="code_postal_pro") ), @AttributeOverride(name="ville", column = @Column(name="ville_pro") ) } )
@Table(name="tbl_sky")
Défini le nom de la table si elle est (ou doit être) différente du nom de la classe.
Il est possible d'y ajouter des contraintes d'unicité à l'intérieur de la table :
@Table(name="EMPLOYE", uniqueConstraints={ @UniqueConstraint(columnNames={"nom", "prenom"}) })
Indique la manière dont l'entity manager
va lire et modifier les attributs : directement avec l'attribut (AccessType.FIELD
) ou en passant par les getter/setter (AccessType.PROPERTY
).
Dans le cas d'une entité qui est héritée, il est possible de dire si les données de l'entité parent doit être dans la même table que l'entité enfant.
@Inheritance(strategy=InheritanceType.JOINED)
TABLE_PER_CLASS
: une table par entité parents et enfants. Pas de jointure mais redondance des informations puisque les données de chaque enfant sont enregistrées dans,JOINED
: une table pour l'entité parent et une pour chaque entité enfant. Une jointure sera effectuée pour avoir la classe entière,SINGLE_TABLE
: une table pour toutes les entités (parent et enfants réunis). Aucune jointure n'est effectuée. On lit la bonne table directement sans jointure.
@DiscriminatorColumn
ou @PrimaryKeyJoinColumn
est la colonne utilisée comme discriminant pour un héritage de type SINGLE_TABLE
et JOINED
. Si les deux sont absents, la valeur par défaut est :
@DiscriminatorColumn(name="DTYPE", discriminatorType=DiscriminatorType.STRING)
@DiscriminatorValue
permet à chaque entité concrète implémentant une entité possédant une @DiscriminatorColumn
de choisir le nom du discriminant. S'il est absent, la valeur par défaut est le nom de la classe si la colonne est de type STRING
et un nombre généré sinon.
Exemple :
Soit la classe classe parent :
import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; @Entity @Inheritance(strategy = InheritanceType.JOINED) @Table(name="Videos") public class Video implements Serializable { @Id @GeneratedValue private Long idVideo; private String titre; private Integer annee; private static final long serialVersionUID = 1L; public Video() { super(); } public Long getIdVideo() { return this.idVideo; } public void setIdVideo(Long idVideo) { this.idVideo = idVideo; } public String getTitre() { return this.titre; } public void setTitre(String titre) { this.titre = titre; } public Integer getAnnee() { return this.annee; } public void setAnnee(Integer annee) { this.annee = annee; } }
Deux classes enfants :
import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.PrimaryKeyJoinColumn; import modeles.webscope.Artiste; import modeles.webscope.Video; @Entity @PrimaryKeyJoinColumn(name = "id_video") public class FilmV extends Video implements Serializable { private static final long serialVersionUID = 1L; @ManyToOne @JoinColumn(name = "id_realisateur") private Artiste realisateur; @ManyToOne private Genre genre; @ManyToOne private Pays pays; public FilmV() { super(); } public Artiste getRealisateur() { return this.realisateur; } public void setRealisateur(Artiste realisateur) { this.realisateur = realisateur; } public Genre getGenre() { return genre; } public void setGenre(Genre genre) { this.genre = genre; } public Pays getPays() { return pays; } public void setPays(Pays pays) { this.pays = pays; } }
import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.PrimaryKeyJoinColumn; import modeles.webscope.Video; @Entity @PrimaryKeyJoinColumn(name = "id_video") public class Reportage extends Video implements Serializable { @Column(length = 40) private String lieu; private static final long serialVersionUID = 1L; public Reportage() { super(); } public String getLieu() { return this.lieu; } public void setLieu(String lieu) { this.lieu = lieu; } }
Trois possibilités :
InheritanceType.JOINED Video CREATE TABLE `Video` ( `idVideo` BIGINT(20) NOT NULL AUTO_INCREMENT, `annee` INT(11) DEFAULT NULL, `titre` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`idVideo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci FilmV CREATE TABLE `FilmV` ( `id_video` BIGINT(20) NOT NULL, `genre_code` VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `pays_code` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `id_realisateur` INT(11) DEFAULT NULL, PRIMARY KEY (`id_video`), KEY `FK_oimyaomgm9k6d10g06cxbp0yx` (`id_realisateur`), CONSTRAINT `FK_oimyaomgm9k6d10g06cxbp0yx` FOREIGN KEY (`id_realisateur`) REFERENCES `Artiste` (`id`), CONSTRAINT `FK_olwd2n0y4pblyxrqm26ylknpq` FOREIGN KEY (`id_video`) REFERENCES `Video` (`idVideo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci Reportage CREATE TABLE `Reportage` ( `lieu` VARCHAR(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `id_video` BIGINT(20) NOT NULL, PRIMARY KEY (`id_video`), CONSTRAINT `FK_q6ri2c7u6yysm923kor5hbith` FOREIGN KEY (`id_video`) REFERENCES `Video` (`idVideo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
InheritanceType.SINGLE_TABLE Video CREATE TABLE `Video` ( `DTYPE` VARCHAR(31) COLLATE utf8mb4_unicode_ci NOT NULL, `idVideo` BIGINT(20) NOT NULL AUTO_INCREMENT, `annee` INT(11) DEFAULT NULL, `titre` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `lieu` VARCHAR(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `genre_code` VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `pays_code` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `id_realisateur` INT(11) DEFAULT NULL, PRIMARY KEY (`idVideo`), KEY `FK_a12jw76dvr55b0p8013hr6dli` (`id_realisateur`), CONSTRAINT `FK_a12jw76dvr55b0p8013hr6dli` FOREIGN KEY (`id_realisateur`) REFERENCES `Artiste` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
InheritanceType.TABLE_PER_CLASS Video CREATE TABLE `Video` ( `idVideo` BIGINT(20) NOT NULL AUTO_INCREMENT, `annee` INT(11) DEFAULT NULL, `titre` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`idVideo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci FilmV CREATE TABLE `FilmV` ( `idVideo` BIGINT(20) NOT NULL, `annee` INT(11) DEFAULT NULL, `titre` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `genre_code` VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `pays_code` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `id_realisateur` INT(11) DEFAULT NULL, PRIMARY KEY (`idVideo`), KEY `FK_oimyaomgm9k6d10g06cxbp0yx` (`id_realisateur`), CONSTRAINT `FK_oimyaomgm9k6d10g06cxbp0yx` FOREIGN KEY (`id_realisateur`) REFERENCES `Artiste` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci Reportage CREATE TABLE `Reportage` ( `idVideo` BIGINT(20) NOT NULL, `annee` INT(11) DEFAULT NULL, `titre` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `lieu` VARCHAR(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`idVideo`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
Indique la clé primaire de la classe. Si elle est mise au dessus d'un attribut, la lecture/écriture de tous les attributs de la classe se fera directement sur les attributs. Si elle est mise au dessus d'un getter/setter, la lecture/écriture de tous les attributs de la classe se fera par l'intermédiaire des getter/setter. Je n'ai pas trouvé où c'est indiqué précisément dans la PJA et apparemment, je ne suis pas le seul : Issue with JPA mapping for two nested @Embeddable Archive du 26/08/2013 le 26/04/2020.
Il est conseillé de mettre le setter de @Id
en private
.
Par défaut, la propriété equals
(ou opérateur ==
) se détermine en regardant l'égalité de la référence.
L'Id
n'est pas suffisant pour déterminer que deux objets sont identique. Si on prend deux objets ayant un Id
autogénérés, leurs id
seront identiques à null
avant l'insertion dans la base de données via save/commit
. Il faut donc, soit utiliser l'ensemble de la clé primaire (si attribut multiple et non auto-généré), soit utiliser les autres champs en prenant le risque d'une fausse égalité.
Ci-dessous deux exemples d'implémentation de la méthode equals
et hashCode
:
public class Adresse { private String adresse; private String ville; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((adresse == null) ? 0 : adresse.hashCode()); result = prime * result + ((ville == null) ? 0 : ville.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Adresse other = (Adresse) obj; if (adresse == null) { if (other.adresse != null) return false; } else if (!adresse.equals(other.adresse)) return false; if (ville == null) { if (other.ville != null) return false; } else if (!ville.equals(other.ville)) return false; return true; } }
@Override public int hashCode() { HashCodeBuilder hcb = new HashCodeBuilder(5381, 33); hcb.append(adresse); hcb.append(ville); return hcb.toHashCode(); } @Override public boolean equals(Object obj) { // Partie générée automatiquement par Eclipse. if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Adresse other = (Adresse) obj; EqualsBuilder eb = new EqualsBuilder(); // append utilise la méthode equals. eb.append(adresse, other.adresse); eb.append(ville, other.ville); return eb.isEquals(); }
En complément à @Id
, il est possible d'indiquer que la génération de l'Id doit être automatique.
strategy = GenerationType.AUTO
: le type de génération est laissé à la discrétion de l'implémentation de PJA. Valeur par défaut.strategy = GenerationType.IDENTITY
: auto-incrémentation d'un numéro de façon traditionnelle.strategy = GenerationType.SEQUENCE
: si j'ai bien compris, l'intérêt est pour un accès concurrent à la base de données. Dès qu'une application génère un objet, il ne prend pas qu'une seule valeur mais en réserve une certaine quantité. Ainsi, l'application suivante pourra utiliser la plage de données suivante. L'intérêt est de permettre au SGBD d'optimiser les requêtes et ainsi permettre l'ajout différé des objets. Cela ne semble être compatible que Oracle et PostgreSQL How to generate primary keys with JPA and Hibernate Archive du 2016 le 26/04/2020.strategy = GenerationType.TABLE
: Dans les autres cas, cela passe par l'utilisation d'une table supplémentaire hibernate_sequence
et l'ajout d'un élément nécessite alors une opération de lecture préalable dans cette table.@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_generator") @SequenceGenerator(name="book_generator", sequenceName = "book_seq", initialValue=100, allocationSize=50)
Par défaut, tous les attributs sont sauvegardés dans la base de données. Pour qu'un attribut ne soit pas sauvegardé, il faut lui ajouter l'annotation @Transient
.
Permet de spécifier les caractéristiques de la colonne de la base de données, uniquement pour des colonnes n'étant pas des jointures (pour @ManyToOne
il faut utiliser @JoinColumn
).
Seul, il ne sert à rien puisque de toute façon, tous les attributs / getter/setter sont automatiquement persistants. L'objectif est de pouvoir définir :
Option | Description | Valeur par défaut |
---|---|---|
name = "XXXX" | Le nom de la colonne | Nom de la propriété ou de l'attribut. |
unique = true | Si la colonne est composée de clés uniques. | false |
nullable = true | Si les valeurs null sont acceptées. | true |
length = 32 | Nombre de caractères d'une colonne de type String uniquement. | 255 |
insertable = true updatable = true | Autorise ou non les requêtes INSERT et UPDATE. | true |
Lorsqu'un attribut fait référence à un objet, @Embedded
permet de dire que l'objet doit être intégré dans la classe en cours et ne pas faire l'objet d'un table à part. @EmbeddedId
fait que tous les attributs de l'objet intégré composent la clé primaire de la classe en cours.
L'objet à embarquer doit être déclaré @Embeddable
// PK pour Primary Key import java.io.Serializable; import javax.persistence.Embeddable; // Pour que la classe puisse être intégré dans Personne // sans passer par une autre table @Embeddable // La classe doit implémenter Serializable public class PersonnePK implements Serializable { // Les champs privés qui serviront d'Id multiple. private String nom; private String prenom; // Les getter et les setter public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getPrenom() { return prenom; } public void setPrenom(String prenom) { this.prenom = prenom; } // hashCode et equals : généré automatiquement depuis Eclipse. @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((nom == null) ? 0 : nom.hashCode()); result = prime * result + ((prenom == null) ? 0 : prenom.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PersonnePK other = (PersonnePK) obj; if (nom == null) { if (other.nom != null) return false; } else if (!nom.equals(other.nom)) return false; if (prenom == null) { if (other.prenom != null) return false; } else if (!prenom.equals(other.prenom)) return false; return true; } }
import javax.persistence.EmbeddedId; import javax.persistence.Entity; @Entity public class Personne { @EmbeddedId private PersonnePK pk; private Long numSecu; private int compteEnBanque; public PersonnePK getPk() { return pk; } public void setPk(PersonnePK pk) { this.pk = pk; } public Long getNumSecu() { return numSecu; } public void setNumSecu(Long numSecu) { this.numSecu = numSecu; } public int getCompteEnBanque() { return compteEnBanque; } public void setCompteEnBanque(int compteEnBanque) { this.compteEnBanque = compteEnBanque; } }
Si plusieurs attributs composent la clé primaire, il est nécessaire de passer par une classe qui ne contient que les attributs id. @IdClass
permet de définir laquelle est-ce.
// Ici, on réutilise la classe PersonnePK sauf qu'il n'y a pas besoin de ''@Embeddable'' import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass; @Entity @IdClass(PersonnePK.class) public class Personne2 { @Id private String nom; @Id private String prenom; private Long numSecu; private int compteEnBanque; public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getPrenom() { return prenom; } public void setPrenom(String prenom) { this.prenom = prenom; } public Long getNumSecu() { return numSecu; } public void setNumSecu(Long numSecu) { this.numSecu = numSecu; } public int getCompteEnBanque() { return compteEnBanque; } public void setCompteEnBanque(int compteEnBanque) { this.compteEnBanque = compteEnBanque; } }
Configuring a JPA Entity Primary Key Archive du 07/2019 le 26/04/2020
Nécessaire pour les formats java.util.Date
et java.util.Calendar
. Il faut préciser la façon dont sera stocké l'information (TemporalType.DATE
, TemporalType.TIME
, TemporalType.TIMESTAMP
).
Si plusieurs applications concurrentes accèdent à la même base de données, il peut être nécessaire de s'assurer que les données de l'application sont bien celles de la base de données. Il suffit d'ajouter un champ avec l'annotation @Version
@Version private Long version;
Association entre deux instances lorsque l'instance A ne peut appartenir qu'à l'instance B et que l'instance B ne peut appartenir qu'à l'instance A. Par exemple une personne et sa carte d'identité ou encore une personne et son adresse principale.
Paramètres :
mappedBy
: est utilisé dans une relation bidirectionnelle.
Soit une personne et sa carte d'identité. La personne possède une carte d'identité via un attribut de type CarteDIdentite
et un mapping OneToOne
.
Si on souhaite connaître la personne depuis la carte d'identité, il faut ajouter un champ Personne
dans CarteDIdentite
et un mapping OneToOne
avec comme paramètre mappedBy=XXX
avec XXX
le nom de l'attribut de type CarteDIdentite
dans la classe Personne
.
?? mappedBy=
vaut le nom de la colonne ou le nom de l'attribut ??
Une instance de la classe fait référence à plusieurs instances de l'autre classe.
Par exemple, un client peut avoir plusieurs comptes bancaires mais un compte bancaire ne peut appartenir qu'à un seul client.
@Entity public class Client implements Serializable { @OneToMany(mappedBy="client") private List<Compte> comptes; }
Dans le cas d'une relation bidirectionnelle, c'est dans @OneToMany
qu'il faut utiliser le paramètre @mappedBy
, pas dans @ManyToOne
.
Plusieurs instances de la classe font référence à une seule autre classe (mais pas forcément la même instance). Association uni-directionnelle. Il faut consulter la première classe pour connaître la deuxième.
Plusieurs marins peuvent travailler dans un même bateau. Il faut connaître le marin pour savoir dans quel bateau il travaille.
@Entity public class Marin implements Serializable { @ManyToOne private Bateau bateau ; }
Plusieurs films peuvent être tourné dans un même pays. Il faut connaître la fiche du film pour savoir dans quel pays il a été tourné.
@Entity public class Film implements Serializable { @ManyToOne private Pays pays ; }
Dans le cas d'une relation bidirectionnelle, c'est dans @ManyToOne
qu'il faut utiliser @JoinColumn
(qui contiendra le nom de la colonne) et dans @OneToMany
qu'il faut utiliser mappedBy
(qui contiendra le nom de l'attribut).
Il est théoriquement possible d'utiliser le même @JoinColumn
avec les mêmes paramètres du coté @OneToMany
mais il va en résulter une requête SQL de type update
qui serait inutile.
Une instance de la classe A peut appartenir à plusieurs instances de la classe B et une instance de la classe B peut appartenir à plusieurs instances de la classe A.
Sur un site d'e-commerce, un objet peut être dans le panier de plusieurs personnes et un panier peut posséder plusieurs objets.
Une relation bidirectionnelle de type @ManyToMany
passe toujours par une table de jointure.
Il est possible de la déclarer explicitement avec @JoinTable
par la syntaxe suivante :
name
est le nom de la table SQL,joinColumns
représente le nom de la colonne qui contient l'@id
de la classe en cours,inverseJoinColumns
représente le nom de la colonne qui contient l'@id
de la classe opposée,@ManyToMany @JoinTable(name = "Role", joinColumns = @JoinColumn(name = "id_film"), inverseJoinColumns = @JoinColumn(name = "id_acteur"))
Dans la classe opposé, on utilise juste mappedBy
qui contient l'attribut de la classe principale.
@ManyToMany(mappedBy = "acteur")
cascade=CascadeType.ALL
: active toutes les fonctionnalités possibles de la cascade en SQL. Quand une entité est persistée (EclipseLink
: persiste
, Hibernate
: save
), les attributs ayant une cascade à PERSIST
sont automatiquement persistés également.fetch=Fetchtype.LAZY
: indique si la liste d'entités doit être chargé lors de la création de la classe (EAGER
) ou uniquement lors de la lecture de la liste (LAZY
).
Les valeurs théoriques par défaut (Hibernate 5.0
pas avant) sont : @OneToMany
: LAZY
, @ManyToOne
: EAGER
, @ManyToMany
: LAZY
, @OneToOne
: EAGER
.
Dans le cas où plusieurs éléments d'une liste doit être chargée, il peut être plus efficace d'effectuer une requête permettant de charger automatiquement l'élément en cours et quelques suivants avec @BatchSize
. Par contre, durant mes essais, je n'ai réussi à faire une requête multique qu'en mode EAGER
et non LAZY
.
@OneToMany(fetch=FetchType.EAGER) @BatchSize(size=10) private Set<Notation> notations = new HashSet<Notation>();
@Column
permet de nommer le nom de la colonne d'un attribut, @JoinColumn
(attribut name
) permet de nommer le nom d'une colonne d'une association. C'est dans cette table que sera la clé étrangère.
Cette annotation s'utilise du coté de l'entité propriétaire de la relation. Pour faire une liaison bidirectionnelle, il faut utiliser le paramètre mappedBy
dans l'annotation de mapping de l'autre entité.
@JoinColumn(name = "code_pays")
referencedColumnName = "XX"
: définir de façon explicite l'attribut de l'autre classe qu'il faut utiliser dans la liaison avec XX
le nom de la colonne dans la base de données de l'attribut de l'autre classe.import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.PrimaryKeyJoinColumn; import javax.persistence.SecondaryTable; import javax.persistence.SecondaryTables; // Dans SecondaryTable, name correspond au nom de la table et // pkJoinColumns permet de définir les colonnes de jointure. // Si deux attributs composent la clé primaire, il est possible // de définir la valeur de pkJoinColumns sous forme d'un tableau. @Entity @SecondaryTables({ @SecondaryTable(name="PERSO_SECU", pkJoinColumns=@PrimaryKeyJoinColumn(referencedColumnName="ID")), @SecondaryTable(name="PERSO_BANQUE", pkJoinColumns=@PrimaryKeyJoinColumn(referencedColumnName="ID")) }) public class Personne { @Id private int id; private String nom; private String prenom; // Il faut définir explicitement dans quelle table est cet attribut et que // le nom de la table est dans une @SecondaryTable. @Column(table="PERSO_SECU") private Long numSecu; @Column(table="PERSO_BANQUE") private int compteEnBanque; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getPrenom() { return prenom; } public void setPrenom(String prenom) { this.prenom = prenom; } public Long getNumSecu() { return numSecu; } public void setNumSecu(Long numSecu) { this.numSecu = numSecu; } public int getCompteEnBanque() { return compteEnBanque; } public void setCompteEnBanque(int compteEnBanque) { this.compteEnBanque = compteEnBanque; } }
package modeles; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Pays implements Serializable { private static final long serialVersionUID = -2226639715151018263L; public Pays() { super(); } @Id private String code; public String getCode() { return code; } public void setCode(String code) { this.code = code; } @Column private String nom; public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } @Column private String langue; public String getLangue() { return langue; } public void setLangue(String langue) { this.langue = langue; } }
package modeles.webscope; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @Entity public class Film implements Serializable { @Id private Integer id; public void setId(Integer i) { id = i; } @Column private String titre; public void setTitre(String t) { titre = t; } public String getTitre() { return titre; } @Column private Integer annee; public void setAnnee(Integer a) { annee = a; } public Integer getAnnee() { return annee; } @ManyToOne @JoinColumn(name = "code_pays") private Pays pays; public void setPays(Pays p) { pays = p; } public Pays getPays() { return pays; } }
Hibernate 5 :
Session session = new Configuration().configure().buildSessionFactory().openSession();
EclipseLink :
EntityManager emf = Persistence.createEntityManagerFactory("XXX").createEntityManager();
avec “XXX” le même nom du projet défini dans persistence.xml
: <persistence-unit name="XXX">
.
Définition de l'objet à ajouter :
Pays monPays = new Pays(); monPays.setCode("IS"); monPays.setNom("Islande"); monPays.setLangue("Islandais");
Hibernate :
session.beginTransaction(); session.save(monPays); session.getTransaction().commit();
EclipseLink :
em.getTransaction().begin(); em.persist(entite); em.getTransaction().commit();
persist
enregistre entite
dans la base de données et fait que les données de entite
dans la base de données seront mises à jour si les attributs sont modifiés.remove
a pour effet de rendre une entité non persistante.refresh
a pour effet de synchroniser l’état de l’entité en mémoire avec ses valeurs présentes en base de données. Nécessaire pour synchroniser un objet après un merge
.detach
a pour effet de détacher l’entité considérée de l’entity manager qui la gère, et, dès lors, les valeurs qu’on pourra affecter à cette entité ne seront pas prises en compte au prochain commit. Une telle instance reste persistante, et pourra être ensuite, par exemple, attachée à un autre entity manager).merge
enregistre entite
dans la base de données mais les données de entite
dans la base de données ne seront PAS mises à jour si les attributs sont modifiés.close()
: exécute toutes les requêtes SQL en attente puis ferme la connexion avec la base de données.clear()
: annule toutes les requêtes SQL en attente.flush()
: exécute toutes les requêtes SQL en attente.Hibernate 5.2
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; CriteriaBuilder builder = session.getCriteriaBuilder(); CriteriaQuery<Pays> criteria = builder.createQuery(Pays.class); List<Pays> resultat = session.createQuery(criteria).getResultList();
List results = session.createCriteria(Item.class) .add(Expression.eq("item.seller", user)) .setFetchMode("bids", FetchMode.EAGER).list(); Iterator items = new HashSet(results).iterator(); while (items.hasNext()) { ... }
Trouve en fonction de la clé primaire.
Employe employe = em.find(Employe.class, 5233);
Le HQL
est la version spécifique du JPQL
pour Hibernate
.
Version simple
import org.hibernate.query.Query; Query<Pays> query = session.createQuery("from Pays", Pays.class); List<Pays> resultat = query.getResultList();
Avec un where
Query<Pays> query = session.createQuery("from Pays p where p.code = :code", Pays.class); query.setString("code", "FR"); List<Pays> resultat = query.getResultList();
On notera ici la présence de :code
et la définition de sa valeur par query.setString(“code”, FR)
.
Passer par des query
avec des select
permet d'éviter que le cache de premier niveau de Hibernate
ne soit remplit.
La syntaxe est proche de SQL
.
Cela marche aussi avec des références.
Artiste bergman = …; … Query<FilmV> query = session.createQuery("from FilmV f where f.realisateur = :rel", FilmV.class); query.setString("rel", bergman); List<Pays> resultat = query.getResultList();
Exemple plus complet :
SELECT employe FROM Employe employe WHERE prenom = 'Pierre'
On constate que from
est une classe, pas une table. select
renvoie des instances, pas des colonnes.
JPQL s'occupe de faire les jointures automatiques (deptAffectation
est une liaison ManyToOne
) :
SELECT employe.deptAffectation FROM Employe employe WHERE employe.prenom = 'Pierre'
est identique à
SELECT departement FROM Employe employe JOIN employe.deptAffectation departement WHERE employe.prenom = 'Pierre'
On peut aussi forcer le préchargement (fetch
) des données des tables jointes.
SELECT employe FROM Employe employe LEFT JOIN fetch employe.deptAffectation WHERE prenom = 'Pierre'
Ci-dessous, c'est le bordel…
Et des jointures plus ou moins implicites pour des relations @ManyToOne
et @OneToOne
.
SELECT titre FROM Film f WHERE f.realisateur.nom = 'Eastwood' SELECT film0_.titre AS col_0_0_ FROM Film film0_ CROSS JOIN Artiste artiste1_ WHERE film0_.id_realisateur=artiste1_.id AND artiste1_.nom='Eastwood' SELECT film.titre FROM Film AS film JOIN film.realisateur AS a WHERE a.nom='Eastwood' SELECT film0_.titre AS col_0_0_ FROM Film film0_ INNER JOIN Artiste artiste1_ ON film0_.id_realisateur=artiste1_.id WHERE artiste1_.nom='Eastwood' SELECT film.titre FROM Film AS film, Artiste AS a WHERE film.realisateur = a AND a.nom='Eastwood' SELECT film0_.titre AS col_0_0_ FROM Film film0_ CROSS JOIN Artiste artiste1_ WHERE film0_.id_realisateur=artiste1_.id AND artiste1_.nom='Eastwood'
Il n'est pas possible de faire une recherche directement dans une liaison @ManyToMany
ou @ManyToOne
. Dans ces cas, il faut passer par une jointure.
SELECT DISTINCT film.titre FROM Film AS film JOIN film.roles AS ROLE JOIN ROLE.pk.acteur AS acteur WHERE acteur.nom= 'Eastwood' SELECT DISTINCT film0_.titre AS col_0_0_ FROM Film film0_ INNER JOIN ROLE roles1_ ON film0_.id=roles1_.id_film INNER JOIN Artiste artiste2_ ON roles1_.id_acteur=artiste2_.id WHERE artiste2_.nom='Eastwood' SELECT DISTINCT film.titre FROM Film AS film JOIN film.roles AS ROLE WHERE ROLE.pk.acteur.nom= 'Eastwood' SELECT DISTINCT film0_.titre AS col_0_0_ FROM Film film0_ INNER JOIN ROLE roles1_ ON film0_.id=roles1_.id_film CROSS JOIN Artiste artiste2_ WHERE roles1_.id_acteur=artiste2_.id AND artiste2_.nom='Eastwood'
Pour mémo inner join
renvoie un croisement entre deux bases de données en fonction d'un critère d'égalité. Dans le cas d'une relation automatique, pas besoin de on
. Pour left outer join
, la requête va renvoyer aussi lorsque aucune correspondance n'existe en remplaçant le champ manquant par la valeur null
.
Les sous-requête HQL marchent de façon proche des SQL :
SELECT film.titrefrom Film AS film WHERE film IN (SELECT ROLE.pk.film FROM ROLE AS ROLE WHERE ROLE.nom='McClane') SELECT film.titre FROM Film AS film WHERE EXISTS (FROM Artiste AS a WHERE a = film.realisateur AND a.nom='Eastwood') FROM Film AS film WHERE 1940 > any (SELECT ROLE.pk.acteur.anneeNaissance FROM ROLE AS ROLE WHERE ROLE.pk.film=film)
Et group by
et having
sont proches à SQL :
SELECT film.titre, COUNT(*) FROM Film AS film JOIN film.roles AS ROLE GROUP BY film ORDER BY 2 ASC SELECT artiste.nom, COUNT(*) FROM Artiste AS artiste JOIN artiste.filmsRealises AS film GROUP BY artiste HAVING COUNT(*) > 3
Pour modifier le fetch
lors de la lecture d'un objet, faire :
SELECT film FROM Film AS film JOIN film.realisateur WHERE film.titre= :titre SELECT film0_.id AS id1_1_, film0_.annee AS annee2_1_, film0_.genre AS genre4_1_, film0_.code_pays AS code_pay5_1_, film0_.id_realisateur AS id_reali6_1_, film0_.titre AS titre3_1_ FROM Film film0_ LEFT OUTER JOIN Artiste artiste1_ ON film0_.id_realisateur=artiste1_.id WHERE film0_.titre='Vertigo'
n'est pas suffisant pour que le réalisateur soit chargé dans la classe Film
si réalisateur à un fetch LAZY
. Il faut alors utiliser les mots clé join fetch
.
SELECT film FROM Film AS film LEFT JOIN fetch film.realisateur WHERE film.titre= :titre SELECT film0_.id AS id1_1_0_, artiste1_.id AS id1_0_1_, film0_.annee AS annee2_1_0_, film0_.genre AS genre4_1_0_, film0_.code_pays AS code_pay5_1_0_, film0_.id_realisateur AS id_reali6_1_0_, film0_.titre AS titre3_1_0_, artiste1_.annee_naissance AS annee_na2_0_1_, artiste1_.nom AS nom3_0_1_, artiste1_.prenom AS prenom4_0_1_ FROM Film film0_ LEFT OUTER JOIN Artiste artiste1_ ON film0_.id_realisateur=artiste1_.id WHERE film0_.titre='Vertigo'
Mais ce n'est pas encore parfait car si on fait une recherche avec un critère dans une liste :
SELECT film FROM Film AS film LEFT JOIN fetch film.roles AS ROLE WHERE ROLE.nom= 'McClane' SELECT film0_.id AS id1_1_0_, roles1_.id_acteur AS id_acteu2_6_1_, roles1_.id_film AS id_film3_6_1_, film0_.annee AS annee2_1_0_, film0_.genre AS genre4_1_0_, film0_.code_pays AS code_pay5_1_0_, film0_.id_realisateur AS id_reali6_1_0_, film0_.titre AS titre3_1_0_, roles1_.nom_role AS nom_role1_6_1_, roles1_.id_film AS id_film3_6_0__, roles1_.id_acteur AS id_acteu2_6_0__ FROM Film film0_ LEFT OUTER JOIN ROLE roles1_ ON film0_.id=roles1_.id_film WHERE roles1_.nom_role='McClane'
il n'y a que le rôle qui correspond qui est chargé, pas tous les rôles. Pour avoir tous les rôles, il faut modifier la requête :
FROM Film AS film LEFT JOIN fetch film.roles AS ROLE WHERE film IN (SELECT r2.pk.film FROM ROLE AS r2 WHERE nom= 'McClane') SELECT film0_.id AS id1_1_0_, roles1_.id_acteur AS id_acteu2_6_1_, roles1_.id_film AS id_film3_6_1_, film0_.annee AS annee2_1_0_, film0_.genre AS genre4_1_0_, film0_.code_pays AS code_pay5_1_0_, film0_.id_realisateur AS id_reali6_1_0_, film0_.titre AS titre3_1_0_, roles1_.nom_role AS nom_role1_6_1_, roles1_.id_film AS id_film3_6_0__, roles1_.id_acteur AS id_acteu2_6_0__ FROM Film film0_ LEFT OUTER JOIN ROLE roles1_ ON film0_.id=roles1_.id_film WHERE film0_.id IN ( SELECT role2_.id_film FROM ROLE role2_, Film film3_ WHERE role2_.id_film=film3_.id AND role2_.nom_role='McClane' )
Dans le cas où une transaction va modifier une valeur, il peut être intéressant de mettre for update
dans la requête HQL pour imposer un verrou sur les éléments.
Dans le cas de la méthode get
, il faut ajouter le paramètre LockMode.UPGRADE
.
Réduire le nombre de résultats :
query.setFirstResult(10); query.setMaxResults(10); // Si un seul résultat (requête sur les attributs de la clé primaire / unique), il est possible de récupérer seulement l'instance. query.uniqueResult(10);
Ici, id
doit être sérializable impérativement.
session.get(Film.class, id); // null en cas d'échec. session.load(Film.class, id); // Exception en cas d'échec.
EclipseLink :
List<Pays> list = em.createQuery("select p from Pays p", Pays.class).getResultList();
ou
@Entity @NamedQueries(value = { @NamedQuery ( resultClass=Commande.class, name = "Pays.TousLesPays", query = "select p from Pays p" ) }) public class Pays implements Serializable { … } List<Pays> list = em.createNamedQuery("TousLesPays", Pays.class).getResultList();
Lors qu'une classe ne possèque qu'une seule requête nommée, @NamedQuery
est suffisant. Sinon, il faut les ranger dans le paramètre value
de @NamedQueries
sous forme d'un tableau.
Il n'est pas obligatoire que le nom soit NomClasse.nomQuery
mais cela permet de bien ranger les Query
.
hibernate.connection.isolation =
NONE
ou 0,READ_COMMITTED
ou 2,READ_UNCOMMITTED
ou 1,REPEATABLE_READ
ou 4,SERIALIZABLE
ou 8,