Ressource
Rappel
Dans la section dédiée au transaction JPA nous avons souligné deux cas
- si nous sommes dans un contexte Java SE (l’ensemble des TP précédents) nous devions gérer les transactions et l’injection de dépendances
- si nous utilisons un container EE, alors nous pouvons nous passer de la gestion des transactions et déléguer l’injection au CDI
Structure du projet
Le projet est structuré par domaine technique, les différents packages représentent nos différentes couches Controller, Service et Repository/DAO
De plus, nous avons un GenericDAO<T> qui va nous permettre d’éviter le code boilerplate. Et comme souligné ci-dessus, étant donné que nous sommes dans un contexte container EE nous n’avons pas besoin de gérer explicitement les transactions.
@Stateless
public class GenericDao<T extends BaseEntity> {
@PersistenceContext
private EntityManager em;
private Class<T> entityClass;
public GenericDao() {}
public GenericDao(Class<T> entityClass) {
this.entityClass = entityClass;
}
public T create(final T t) {}
public T update(final T t) {}
public void delete(final Object id) {}
public T find(final Object id) {
return em.find(entityClass ,id);
}
}@PersistenceContextvia TomEE nous bénéficions du CDI qui permet d’injecter la dépendanceEntityManagerentityClassnous permet de connaître la classe concrète- pas besoin de gérer les transactions
Partie 1
1. Coder le service StudentService
StudentServiceappelleStudentDAO
public class StudentService {
@Inject // Injection de dépendances
private StudentDao studentDao;
public Student getStudent(Long id) {
return studentDao.find(id);
}
}- Dans
studentDaonous n’avons pas besoin de recoder la méthodefind() - Ce sera celle de la super-classe qui sera appelée
2. Rajouter les soirées
- Nous avons donc une relation
OneToMany, on fera attention aux éléments suivants :- utiliser
mappedBypour avoir une relation bidirectionnelle - ne pas oublier les méthode
addSoiree(Soiree s)etremoveStudent(Soiree s)pour garantir la synchronisation des objets
- utiliser
Attention
La relation est en réalité n:m et pas 1:n, mais passons …
@Entity
public class Student extends BaseEntity {
private String name;
private int age;
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
private List<Soiree> soirees;
// méthodes addSoiree(Soiree e) et removeSoiree(Soiree e)
}
@Entity
public class Soiree extends BaseEntity {
private String name;
@ManyToOne
private Student student;
}3. Récupérer un étudiant avec ses soirées
Dans StudentService rajoutons la méthode
public class StudentService {
...
public Student findWithSoiree(Long id) {
Student student = studentDao.find(id);
student.getSoirees(); // Illegal LazyInitializationException
return student;
}
}- Si nous réalisons le code ci-dessus, alors une
LazyInitializationExceptionsera levée; en effet nous ne pouvons pas accéder à la liste des soirée si nous ne sommes pas dans la même transaction. (cf TP3) - Une solution consiste donc à utiliser une JOINTURE, en codant une nouvelle méthode dans
StudentDao
public class StudentDao {
...
public Student findWithSoirees(Long id) {
return em.createQuery(
"SELECT s FROM Student s LEFT JOIN FETCH s.soirees WHERE s.id = :id", Student.class)
.setParameter("id", id)
.getSingleResult();
}
}3b. Afficher un étudiant et ses soirées (JSON)
Nous avons un problème de référence circulaire car :
- un
studentà un attributsoirees - et une
soireesà un attributstudent
{
id: 1
name: Adrien
age: 24
soirees: [
{
id: 1
name: super soiree
student: {
id: 1
name: Adrien
age: 24
soirees: [
...
]
}
},
{
id: 2
...
}
]
}
Plusieurs options permettent d’éviter les références circulaires :
-
supprimer l’attribut
studentdans la classeSoirees, mais ceci casse la relation bidirectionnelle, et comme rappelé dans le paragraphe OneToMany relation-bidirectionnelle -
une autre option consiste à rajouter l’annotation
@JsonIgnoresur l’attributstudentdans la classeSoireespour ne pas afficher les étudiants au format JSON. Mais pour moi cette solution n’est pas la bonne car elle contourne le problème. -
En effet, le problème vient du faire que nous retournons à la Vue notre schéma relationnelle. Or, comme vu dans le premier cours nous n’avons pas à exposer notre architecture de base de données à notre Vue. En effet si dans les étudiants avions un champs
passwordsouhaitons-nous le retourner dans le JSON ?! Par conséquent, la solution est de créer unStudentDTO- Et dans la couche Service ou Controller (au choix) nous ferons la conversion entre
StudentetStudentDTO:public StudentDTO toDTO(Student student)
- Et dans la couche Service ou Controller (au choix) nous ferons la conversion entre
Partie 2 - pagination
Pour le moment, tous les étudiants sont remontés. Par soucis de performance, nous devons mettre en place de la pagination.
- Depuis le Controller on demandera la page et le nombre d’élément souhaités (size)
- Puis dans le DAO nous coderons la requête SQL permettant de limiter le nombre d’éléments
public class GenericDao<T extends BaseEntity> {
...
// Ajout de la méthode de pagination
public List<T> findAllPaginated(int page, int size) {
return em.createQuery("SELECT e FROM " + entityClass.getSimpleName() + " e", entityClass)
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
}
}- Pour coder la pagination SQL nous allons utiliser un
createQuerypuis deux méthodes- la première permettant d’aller au premier résultat à remonter
- la deuxième pour spécifier le nombre d’élément à remonter
⇒ Par conséquent, en 4 lignes, nous venons de coder la pagination pour l’ensemble de nos classes.
MAIS pour les jointures ?
Néanmoins, comment récupérer tous les étudiants avec leurs soirées ?
- car si nous appelons cette méthode nous rencontrerons une
LazyInitializationExceptioncomme dans la Partie 1 point 3.
Il faut comme dans le cas précédent créer une jointure. Néanmoins au lieu de créer une nouvelle méthode dans StudentDAO essayons de créer une méthode générique
- Pour ce faire, nous allons utiliser une liste pour représenter notre jointure, dans laquelle nous préciserons les attributs de jointure (ici
aetb)
SELECT s
FROM Student s
JOIN s.soirees
JOIN s.y
JOIN s.z
public List<T> findAllPaginatedAndJoin(int page, int size, List<String> fetchRelations) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(entityClass);
Root<T> root = cq.from(entityClass);
for (String relation : fetchRelations) {
root.fetch(relation, JoinType.LEFT);
}
return em.createQuery(cq)
.setFirstResult(page * size) // ajout de la pagination
.setMaxResults(size)
.getResultList();
}Et dans StudentDAO nous pouvons construire la méthode suivante
public List<Student> findAllWithPaginationAndJoin(int page, int size) {
// Permet le JOIN à soirée
return findByCriteria(criteria, List.of("soirees"));
}Partie 3 - recherche via criteria
Il nous reste un point à aborder. Comment rechercher des étudiants ?
- pour ce faire on va se baser sur l’API Criteria
SELECT s
FROM Student s
WHERE name = :name
AND age = :age
AND y = :y
Pour représenter nos critère nous allons utilise une Map<String, Object>, dans le GenericDAO nous codons la méthode suivante
public List<T> findByCriteria(Map<String, Object> criterias, List<String> fetchRelations) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(entityClass);
Root<T> root = cq.from(entityClass);
// Jointure pour éviter LazyInitializationException
for (String relation : fetchRelations) {
root.fetch(relation, JoinType.LEFT);
}
// Critère de recherche
List<Predicate> predicates = new ArrayList<>();
for (Map.Entry<String, Object> entry : criterias.entrySet()) {
predicates.add(cb.equal(root.get(entry.getKey()), entry.getValue()));
}
cq.where(cb.and(predicates.toArray(new Predicate[0])));
return em.createQuery(cq).getResultList();
}Et finalement dans notre classe StudentDAO
public List<Student> searchStudents(String name, Integer age) {
Map<String, Object> criteria = new HashMap<>();
if (name != null && !name.isEmpty()) {
criteria.put("name", name);
}
if (age != null) {
criteria.put("age", age);
}
// Join à soirée
return findByCriteria(criteria, List.of("soirees"));
}Attention, nous ne pouvons pas coder simplement
findByCriteria(criteria, Map.of("name", name, "age", age));Car si name ou age sont null, alors vous allez vous retrouver avec la requête suivante qui faussera les résultats
WHERE name = NULL
AND age = NULL