Outils pour utilisateurs

Outils du site


lang:java:jpa

Il faut commencer par créer un projet JPA et ensuite configurer le fichier persistence.xml.

Cycle de vie

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.

persistence.xml
<?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.

Hibernate

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.xml
<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.
hibernate.cfg.xml
<?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>

Les annotations

Les classes

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.

@Stateful

Les beans de type Stateful sont capables de conserver leur état.

@Stateless

Les beans de type stateless n'ont par leur état conservé.

@MessageDriven

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.

@Entity

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.

@Embeddable

Pour qu'une classe puisse être embarquée dans une entité, il faut que la classe :

  • soit déclarée @Embeddable,
  • n'est pas d'attribut @Id.

@AttributeOverrides et @AttributeOverride

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

@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"})
       })

@Access

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).

@Inheritance

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 :

Video.java
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 :

FilmV.java
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;
  }
}
Reportage.java
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

Les attributs

@Id

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 :

  • Solution 1 :
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();
}

@GeneratedValue

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)

@Transient

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.

@Column

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 :

OptionDescriptionValeur par défaut
name = "XXXX"Le nom de la colonneNom de la propriété ou de l'attribut.
unique = trueSi la colonne est composée de clés uniques.false
nullable = trueSi les valeurs null sont acceptées.true
length = 32Nombre 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

@Embedded, @EmbeddedId

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

PersonnePK.java
// 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;
  }
}
Personne.java
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;
  }
}

@IdClass

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.

Personne.java
// 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

@Temporal

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).

@Version

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/mapping

@OneToOne

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 ??

@OneToMany

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.

@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.

@ManyToMany

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")

Commun aux ''@OneTo''/''@ManyTo''

  • 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>();

@JoinColumn

@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.

Bases de données réparties

Une entité dans plusieurs tables

Personne
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;
  }
}

Exemples

Les Bean

Pays.java
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;
  }
}
Film.java
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;
  }
}

EntityManager

Initialisation de la connexion

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">.

Insertion

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.

Gestion des flux

  • 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.

Lecture

Tous
  • API Criteria :

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);
  • HQL / JPQL

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);
  • Hibernate

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.

Requête SQL

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.cfg.xml

hibernate.connection.isolation =

  • NONE ou 0,
  • READ_COMMITTED ou 2,
  • READ_UNCOMMITTED ou 1,
  • REPEATABLE_READ ou 4,
  • SERIALIZABLE ou 8,

Ressources

lang/java/jpa.txt · Dernière modification : 2020/05/11 00:31 de root