Java Persistence API (la suite)

Quelques liens :

Rendre la DAO plus générique

Afin d'éviter la création de nombreuses méthodes (quatre pour chaque entité), nous pouvons maintenant doter notre classe Dao de nouvelles méthodes génériques :

public <T> T find(Class<T> clazz, Object id) {
    return em.find(clazz, id);
}

public <T> Collection<T> findAll(String query, Class<T> clazz) {
    TypedQuery<T> q = em.createQuery(query, clazz);
    return q.getResultList();
}

public <T> T add(T entity) {
    em.persist(entity);
    return entity;
}

public <T> T update(T entity) {
    return em.merge(entity);
}

public <T> void remove(Class<T> clazz, Object pk) {
    T entity = em.find(clazz, pk);
    if (entity != null) {
        em.remove(entity);
    }
}

Relation Un-Plusieurs

Pour étudier la gestion des relations, nous allons créer une nouvelle entité Voiture (classe Car) et une relation père-fils entre une personne et plusieurs voitures. Le code de la classe Car est le suivant :

package myapp.jpa.model;

import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "immatriculation")
public class Car {

    // properties
    @Id
    private String immatriculation;

    @Basic(optional = false)
    private String model;

    @ManyToOne(optional = true)
    @JoinColumn(name = "owner_id") // optionnelle
    @ToString.Exclude // afin d'éviter les boucles
    private Person owner;

    public Car(String immatriculation, String model) {
        this(immatriculation, model, null);
    }

}

Explications : L'annotation ManyToOne indique que la propriété code une relation fils-vers-le-père et l'annotation JoinColumn permet de donner un nom à la colonne de la table qui va jouer ce rôle. Attention : pour manipuler cette relation, vous devrez passer obligatoirement par la modification de cette propriété. C'est donc le fils qui abrite (owning) cette relation.

Travail à faire : Créez un test unitaire afin d'ajouter une voiture et son propriétaire (qui doit déjà exister).

Inverser la relation Un-Plusieurs

Nous allons ajouter à la classe Person, la liaison dans l'autre sens (père-vers-les-fils) avec une annotation OneToMany appliquée à une collection de voitures. L'attribut mappedBy est particulièrement important car il indique le nom de la propriété (côté fils) qui code la relation un-plusieurs.

public class Person {
   ...

    @OneToMany(//
        fetch = FetchType.LAZY, //
        mappedBy = "owner", //
        cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE }, //
        )
    @ToString.Exclude  // pour éviter les boucles
    private Set<Car> cars;

    public void addCar(Car c) {
        if (cars == null) {
            cars = new HashSet<>();
        }
        cars.add(c);
        c.setOwner(this);
    }

   ...
}

Remarque : Vous n'êtes pas obligé de préciser les attributs de @OneToMany.

Travail à faire :

  • Le lien père-vers-les-fils est géré en mode retardé (LAZY). Modifiez la méthode findPerson pour être sûr que les voitures soient chargées (utilisez simplement la méthode size() sur la collection de voitures pour forcer le chargement).
  • modifiez votre classe de test unitaire afin d'ajouter des voitures (en insertion de personne et en mise à jour de personne).
  • Maintenant que nous avons une relation, préparez une requête nommée (annotation NamedQuery) pour renvoyer les personnes qui possèdent un modèle de voiture. Prévoyez la méthode ci-dessous et un test unitaire :
    public List<Person> findPersonsByCarModel(String model) {
        ...
    }
    

Ordonner les relations

Il est souvent utile d'ordonner les collections récupérées par JPA. Vous pouvez le faire simplement en ajoutant l'annotation OrderBy comme le montre la nouvelle version de la classe Person ci-dessous :

   ...

   @OneToMany(...)
   @OrderBy("immatriculation ASC")
   private Set<Car> cars;

   ...

Travail à faire : Vérifiez l'ordre lors des récupérations de voitures.

Relation Plusieurs-Plusieurs

Nous allons mettre en place une relation plusieurs-plusieurs entre les personnes et des films. Pour ce faire, nous allons ajouter la classe Movie définie comme suit :

package myapp.jpa.model;

import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class Movie {

    @Id
    @GeneratedValue
    private Long id;

    @Basic(optional = false)
    private String name;

    public Movie(String name) {
        this.name = name;
    }

}

Travail à faire : Modifiez la classe Person en ajoutant le code suivant :

   ...

   @ManyToMany(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
   // @JoinTable est optionnelle (afin de préciser les tables)
   @JoinTable(
      name = "Person_Movie",
      joinColumns = { @JoinColumn(name = "id_person") },
      inverseJoinColumns = { @JoinColumn(name = "id_movie") }
      )
   @ToString.Exclude
   Set<Movie> movies;

   public void addMovie(Movie movie) {
      if (movies == null) {
         movies = new HashSet<>();
      }
      movies.add(movie);
   }

   ...

Les annotations ManyToMany et JoinTable vont nous permettre de définir cette relation et de créer la table de correspondance. Dans notre exemple, la relation n'est accessible que via la classe Person. Nous aurions pu également la définir dans la classe Movie en inversant les attributs joinColumns et inverseJoinColumns.

Attention : c'est la classe contenant l'annotation JoinTable qui abrite (owning) la relation. Toute modification de cette relation doit donc passer par cette classe. En d'autres termes, si vous souhaitez ajouter un film à une personne, vous devez charger la personne, puis le film et ajouter le film à la personne.

Travail à faire : Ajoutez un test unitaire afin de vérifier l'ajout d'un film à une personne et sa suppression.

Remarque : Vous trouverez plus d'exemples sur ce site.

Relation Un-Un

La relation Un-Un (où l'un des côtés peut être optionnel) permet de lier l'existence de deux entités. Cette relation passe par l'annotation OneToOne. La JavaDoc présente deux alternatives que nous ne détaillerons pas dans cette séance.

Travail à faire : Vous pouvez, à titre d'exemple, associer une personne à un CV (et vice-versa) mais en autorisant les personnes sans CV (et non pas les CV sans personne). Pour ce faire, vous devez créer une entité CV et mettre en place les liaisons.

Les relations vers des types simples

Si vous souhaitez associer des types simples (chaines, entiers, décimaux) à une entité, vous pouvez le faire simplement avec ElementCollection. À titre d'exemple, nous allons ajouter une collection triée de prénoms à notre personne ainsi qu'une collection ordonnée de surnoms :

   ...

    @ElementCollection(fetch = FetchType.LAZY)
    // annotations optionnelles
    @CollectionTable(name = "FIRST_NAMES", //
            joinColumns = @JoinColumn(name = "person_id"))
    @Column(name = "first_name")
    Collection<String> firstNames;

    @ElementCollection(fetch = FetchType.LAZY)
    @OrderColumn(name = "position")
    List<String> nickNames;

   ...

Travail à faire : Vérifiez la structure relationnelle créée.

Remarque : Vous pouvez utiliser des classes embarquées à la place des types simples.

Traitement de la concurrence

Pose de verrou

Cette approche classique consiste à verrouiller les lignes (donc les objets) de la base après leur récupération pour pouvoir les modifier à sa guise sans craindre qu'une autre transaction ne les altère. Pour ce faire nous allons ajouter un paramètre au chargement LockModeType :

public void changeFirstName(long idPerson, String firstName) {
    Person p = em.find(Person.class, idPerson, LockModeType.PESSIMISTIC_WRITE);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    p.setFirstName(firstName);
}

Vérification : Vous pouvez tester ce code en l'exécutant plusieurs fois ou en essayant une modification via le client du SGBDR (seulement si vous utilisez MySQL).

Gestion optimiste

Dans la gestion optimiste, les données sont versionnées et JPA va vérifier que le numéro de version ne change pas entre la lecture et la modification. Ce mécanisme est utile quand l'objet reste longtemps en mémoire entre sa lecture et sa modification (par exemple pour un formulaire de saisie).

Nous avons déjà prévu une propriété version dans le JavaBean Person (annotation Version).

Vérification : Créez un test unitaire composé de trois parties :

  • lecture d'une personne dans p1,
  • lecture de la même personne dans p2,
  • changement de p2 et mise à jour,
  • changement de p1 et mise à jour,

Vous devriez obtenir une erreur d'exécution indiquant un problème dans la gestion optimiste du verrou : le numéro de version a changé lors de la sauvegarde de p1.