Java Persistence API (JPA) 2/2
- Exécuter des requêtes avec JPA
- Les requêtes Natives
- Les requêtes JPQL
- Les requêtes par programmation
- Gestion des relations
- La relation 1:1 (one to one)
- La relation n:1 (many to one)
- La relation 1:n (one to many)
- La relation n:n (many to many)
- Les relations bi-directionnelles
- La propagation en cascade
- 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 :
- On crée un objet de type
Query
ouTypedQuery
grâce à l'API - Pour les requêtes paramétrées, on positionne la valeur des paramètres grâce aux méthodes
setParameter(String name, XXX xxx)
ousetParameter(int position, XXX xxx)
- 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)
) - 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) ougetResultList()
(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 :
Cette entité JPA correspond à la table MySQL :
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(...)
.
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(...)
.
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 :
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()
.
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.
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
:
La relation n:1 (many to one)
L'annotation @ManyToOne
définit une relation n:1 entre deux entités.
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
:
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.
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.
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
:
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.
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
.
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
.
- eager signifie que l'information doit être chargée sytématiquement lorsque l'entité est chargée.
Cette stratégie est appliquée par défaut pour
@Basic
,@OneToOne
et@ManyToOne
. - lazy signifie que l'information ne sera chargée qu'à la demande (par exemple lorsque la méthode
get
de l'attribut sera appelée). Cette stratégie est appliquée par défaut pour@OneToMany
et@ManyToMany
.