Java EE - EPSI POE mars 2017 - David Gayerie Licence Creative Commons

Java Persistence API (JPA) 2/2

  1. Exécuter des requêtes avec JPA
  2. Les requêtes Natives
  3. Les requêtes JPQL
  4. Les requêtes par programmation
  5. Gestion des relations
  6. La relation 1:1 (one to one)
  7. La relation n:1 (many to one)
  8. La relation 1:n (one to many)
  9. La relation n:n (many to many)
  10. Les relations bi-directionnelles
  11. La propagation en cascade
  12. Fetch lazy ou fetch eager

Exécuter des requêtes avec JPA

Les méthodes find, persist, merge, detach, refresh et remove disponibles avec une instance d'EntityManager permettent de gérer simplement une entité mais ne permettent pas de réaliser des requêtes très élaborées.

Heureusement, un EntityManager fournit également différentes API pour exécuter des requêtes. Le principe est toujours le même :

  1. On crée un objet de type Query ou TypedQuery grâce à l'API
  2. Pour les requêtes paramétrées, on positionne la valeur des paramètres grâce aux méthodes setParameter(String name, XXX xxx) ou setParameter(int position, XXX xxx)
  3. On peut optionnellement positionner plusieurs autres informations pour la requête (par exemple, le nombre maximum de résultats pour une consultation grâce à la méthode setMaxResults(int))
  4. On exécute la requête grâce aux méthodes executeUpdate() (pour un update ou un delete), getSingleResult() (pour une requête SELECT ne retournant qu'un seul résultat) ou getResultList() (pour une requête SELECT retournant une liste de résultats).

Pour les différents exemples de requêtes qui suivent, nous nous baserons sur l'entité JPA suivante :


import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "individu")
public class Individu {
  @Id
  @Column(name = "individuId")
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Basic
  @Column(length = 30, nullable = false)
  private String nom;

  @Basic
  @Column(length = 30, nullable = false)
  private String prenom;

  @Column(length = 3, nullable = false)
  private Integer age;
  
  // les getters et setters sont omis ... 
}

Cette entité JPA correspond à la table MySQL :


CREATE TABLE `individu` (
  `individuId` int NOT NULL AUTO_INCREMENT,
  `nom` varchar(30) NOT NULL,
  `prenom` varchar(30) NOT NULL,
  `age` int(3) NOT NULL,
  PRIMARY KEY (`individu_id`)
);

Les requêtes Natives

Les requêtes natives en JPA désignent les requêtes SQL. On crée une requête native à partir des méthodes EntityManager.createNativeQuery(...).

Exemple : récupérer tous les individus

  List<Individu> individus = null;
  individus = entityManager.createNativeQuery("select * from individu", Individu.class)
                           .getResultList();

Exemple : récupérer tous les individus âgés au plus de ageMax

  int ageMax = 25;
  List<Individu> individus = null;
  individus = entityManager
                  .createNativeQuery("select * from individu where age <= ?", Individu.class)
                  .setParameter(1, ageMax)
                  .getResultList();

Exemple : connaître le nombre d'individus enregistrés

  long result = (long) entityManager
                           .createNativeQuery("select count(1) from individu", long.class)
                           .getSingleResult();

Exemple : Suppression d'un individu dont l'id est individuId

  long individuId = 1;
  // Cette requête nécessite une transaction active
  entityManager.createNativeQuery("delete from individu where individuId = ?")
               .setParameter(1, individuId)
               .executeUpdate();

Les requêtes JPQL

Avec JPA, il est possible d'utiliser une autre langage pour l'écriture des requêtes, il s'agit du JPA Query Language (JPQL). Ce langage est un langage de requête objet. L'objectif n'est plus d'écrire des requêtes basées sur le modèle relationnel des tables mais sur le modèle objet des classes Java.

On crée une requête JPQL à partir des méthodes EntityManager.createQuery(...).

Exemple : récupérer tous les individus

  List<Individu> individus = null;
  individus = entityManager.createQuery("select i from Individu i", Individu.class)
                           .getResultList();

Exemple : récupérer tous les individus âgés au plus de ageMax

  int ageMax = 25;
  List<Individu> individus = null;
  individus = entityManager
                .createQuery("select i from Individu i where i.age <= :ageMax", Individu.class)
                .setParameter("ageMax", ageMax)
                .getResultList();

Exemple : connaître le nombre d'individus enregistrés

  long result = entityManager.createQuery("select count(i) from Individu i", long.class)
                             .getSingleResult();

Exemple : Suppression d'un individu dont l'id est individuId

  long individuId = 1;
  // Cette requête nécessite une transaction active
  entityManager.createQuery("delete from Individu i where i.id = :id")
               .setParameter("id", individuId)
               .executeUpdate();

Sur des exemples aussi simples que les exemples précédents, le JPQL semble très proche du SQL. Cependant, avec le JPQL, on ne fait référence qu'aux objets et à leurs attributs, jamais au nom des tables et des colonnes.

Ensuite dans la requête JPQL suivante :

select individu from Individu individu

individu désigne la variable contenant l'instance courante de la classe Individu. Il ne s'agit absolument pas d'un alias de table comme en SQL. La déclaration d'une variable en JPQL est obligatoire ! Alors qu'un alias de table SQL est optionnel.

Enfin, le JPQL introduit une nouvelle façon de déclarer un paramètre dans une requête sous la forme :nom. Les paramètres disposent ainsi d'un nom explicite, rendant ainsi le code plus facile à lire et à maintenir.

Les requêtes par programmation

Lorsque l'on souhaite construire une requête JPQL dynamiquement, il n'est pas toujours très facile de construire la requête par simple concaténation de chaînes de caractères. Pour cela, JPA fournit une API permettant de définir entièrement une requête JPQL par programmation. On crée cette requête à travers un CriteriaBuilder que l'on peut récupérer grâce à la méthode EntityManager.getCriteriaBuilder().

Exemple : récupérer tous les individus

  CriteriaBuilder builder = entityManager.getCriteriaBuilder();

  CriteriaQuery<Individu> query = builder.createQuery(Individu.class);
  Root<Individu> i = query.from(Individu.class);
  query.select(i);

  List<Individu> individus = entityManager.createQuery(query).getResultList();

Exemple : récupérer tous les individus âgés au plus de ageMax

  int ageMax = 25;

  CriteriaBuilder builder = entityManager.getCriteriaBuilder();

  CriteriaQuery<Individu> query = builder.createQuery(Individu.class);
  Root<Individu> i = query.from(Individu.class);
  query.select(i);
  query.where(builder.lessThanOrEqualTo(i.get("age").as(int.class), ageMax));

  List<Individu> individus = entityManager.createQuery(query).getResultList();

Exemple : connaître le nombre d'individus enregistrés

  CriteriaBuilder builder = entityManager.getCriteriaBuilder();

  CriteriaQuery<Long> query = builder.createQuery(Long.class);
  Root<Individu> i = query.from(Individu.class);
  query.select(builder.count(i));

  long result = entityManager.createQuery(query).getSingleResult();

Gestion des relations

Une des fonctionnalités majeures des ORM est de gérer les relations entre objets comme des relations entre tables dans un modèle de base de données relationnelle. JPA définit des modèles de relation qui peuvent être déclarés par annotation.

Les relations sont spécifiées par les annotations : @OneToOne, @ManyToOne, @OneToMany, @ManyToMany.

La relation 1:1 (one to one)

L'annotation @OneToOne définit une relation 1:1 entre deux entités. Si cette relation n'est pas forcément très courante dans un modèle relationnel de base de données, elle se rencontre très souvent dans une modèle objet.

Exemple de relation OneToOne

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @OneToOne
  private Abonnement abonnement;

  // getters et setters omis ...
}


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Abonnement {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  // getters et setters omis ...
}

L'annotation @OneToOne implique que la table Individu contient une colonne qui est une clé étrangère contenant la clé d'un abonnement. Par défaut, JPA s'attend à ce que cette colonne se nomme ABONNEMENT_ID, mais il est possible de changer ce nom grâce à l'annotation @JoinColumn :

Déclaration de la clé étrangère

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @OneToOne
  @JoinColumn(name = "abonnement_fk")
  private Abonnement abonnement;

  // getters et setters omis ...
}

La relation n:1 (many to one)

L'annotation @ManyToOne définit une relation n:1 entre deux entités.

Exemple de relation ManyToOne

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne
  private Societe societe;

  // getters et setters omis ...
}


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Societe {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  // getters et setters omis ...
}

L'annotation @ManyToOne implique que la table Individu contient une colonne qui est une clé étrangère contenant la clé d'une société. Par défaut, JPA s'attend à ce que cette colonne se nomme SOCIETE_ID, mais il est possible de changer ce nom grâce à l'annotation @JoinColumn. Plutôt que par une colonne, il est également possible d'indiquer à JPA qu'il doit passer par une table d'association pour établir la relation entre les deux entités avec l'annotation @JoinTable :

Déclaration d'une table d'association

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne
  // déclaration d'une table d'association
  @JoinTable(name = "societe_individu", 
             joinColumns = @JoinColumn(name = "individu_id"), 
             inverseJoinColumns = @JoinColumn(name = "societe_id"))
  private Societe societe;

  // getters et setters omis ...
}

La relation 1:n (one to many)

L'annotation @OneToMany définit une relation 1:n entre deux entités. Cette annotation ne peut être utilisée qu'avec une collection d'éléments puisqu'elle implique qu'il peut y avoir plusieurs entités associées.

Exemple de relation OneToMany

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Societe {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @OneToMany
  private List<Individu> employes = new ArrayList<>();

  // getters et setters omis ...
}


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  // getters et setters omis ...
}

L'annotation @OneToMany implique que la table Individu contient une colonne qui est une clé étrangère contenant la clé d'une société. Par défaut, JPA s'attend à ce que cette colonne se nomme SOCIETE_ID, mais il est possible de changer ce nom grâce à l'annotation @JoinColumn. Il est également possible d'indiquer à JPA qu'il doit passer par une table d'association pour établir la relation entre les deux entités avec l'annotation @JoinTable. Ainsi, @ManyToOne fonctionne exactement comme @OneToMany sauf que cette annotation porte sur l'autre côté de la relation.

La relation n:n (many to many)

L'annotation @ManyToMany définit une relation n:n entre deux entités. Cette annotation ne peut être utilisée qu'avec une collection d'éléments puisqu'elle implique qu'il peut y avoir plusieurs entités associées.

Exemple de relation ManyToMany

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToMany
  private List<Individu> enfants = new ArrayList<>();

  // getters et setters omis ...
}

L'annotation @ManyToMany implique qu'il existe une table d'association Individu_Individu qui contient les clés étrangères INDIVIDU_ID et ENFANTS_ID permettant de modéliser la relation. Il est possible de décrire explicitement la relation grâce à l'annotation @JoinTable :

Déclaration de la table d'association

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToMany
  @JoinTable(name = "ParentEnfant", 
             joinColumns = @JoinColumn(name = "parent_id"), 
             inverseJoinColumns = @JoinColumn(name = "enfant_id"))
  private List<Individu> enfants = new ArrayList<>();

  // getters et setters omis ...
}

Les relations bi-directionnelles

Parfois, il est nécessaire de construire un lien entre deux objets qui soit navigables dans les deux sens. D'un point de vue objet, cela signifie que chaque objet dispose d'un attribut pointant sur l'autre instance. Mais d'un point de vue du schéma de base de données relationnelle, il s'agit du même lien. Avec JPA, il est possible de qualifier une relation entre deux objets comme étant une relation inverse grâce à l'attribut mappedBy de l'annotation indiquant le type de relation.

Exemple d'une relation bi-directionnelle avec mappedBy

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToMany
  private List<Individu> enfants = new ArrayList<>();

  @ManyToMany(mappedBy = "enfants")
  private List<Individu> parents = new ArrayList<>();

  // getters et setters omis ...
}

Dans l'exemple précédent, la liste des enfants représente bien une relation entre plusieurs instances de la classe Individu et la liste parents représente la relation inverse. Pour le spécifier à JPA, on utilise l'attribut mappedBy de l'annotation @ManyToMany pour indiquer sur l'attribut représentant la relation inverse, le nom de l'attribut modélisant la relation principale.

La propagation en cascade

Avec JPA, il est possible de propager des modifications à tout ou partie des entités liées. Les annotations permettant de spécifier une relation possèdent un attribut cascade permettant de spécifier les opérations concernées : ALL, DETACH, MERGE, PERSIST, REFRESH ou REMOVE.

Exemple d'opération en cascade

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
  private Societe societe;

  // getters et setters omis ...
}

Dans l'exemple précédent, on précise que l'instance de société doit être enregistrée en base si nécessaire au moment où l'instance d'Individu sera elle-même enregistrée. De plus, lors d'un appel à merge pour une instance d'Individu, un merge sera automatiquement realisé sur l'instance de l'attribut société.

Fetch lazy ou fetch eager

Lorsque JPA doit charger une entité depuis la base de données (qu'il s'agisse d'une appel à EntityManager.find(...) ou d'une requête), la question est de savoir quelles informations doivent être chargées. Doit-il charger tous les attributs d'une entité ? Parmi ces attributs, doit-il charger les entités qui sont en relation avec l'entité chargée ? Ces questions sont importantes, car la façon d'y répondre peut avoir un impact sur les performances de l'application.

Dans JPA, l'opération de chargement d'une entité depuis la base de données est appelée fetch. Un fetch peut avoir deux stratégies : eager ou lazy (que l'on peut traduire respectivement et approximativement par chargement immédiat et chargement différé). On peut décider de la stratégie pour chaque membre de la classe grâce à l'attribut fetch présent sur les annotations @Basic, @OneToOne, @ManyToOne, @OneToMany et @ManyToMany.

Exemple d'utilisation de fetch lazy

import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;

@Entity
public class Individu {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  private Societe societe;

  /*
   * Stocke la photo d'identité sous une forme binaire. Comme l'information peut être
   * volumineuse, on déclare un fetch lazy pour ne déclencher le chargement qu'à l'appel
   * de getPhoto(), c'est-à-dire quand l'application en a vraiment besoin.
   */
  @Lob
  @Basic(fetch = FetchType.LAZY)
  private byte[] photo;

  // getters et setters omis ...
}


  // Exécute une requête de la forme : SELECT i.id FROM Individu i WHERE i.id = ?
  // Un appel à la méthode find ne charge pas les attributs marqués avec un fetch lazy.
  Individu persistedIndividu = entityManager.find(Individu.class, individuId);

  // Exécute une requête de la forme : SELECT i.photo FROM Individu i WHERE i.id = ?
  byte[] photo = persistedIndividu.getPhoto();
  
  // Exécute une requête de la forme :  SELECT * FROM Societe WHERE id = ?
  Societe societe = persistedIndividu.getSociete();