jeudi 6 avril 2023

Multiple fetch dans une requête avec jpa

Hibernate et fort probablement les autres ORM sont limité dans la capacité de ramener tout une structure d'object imbriqué.

@Entity
private class Student{
 
    @Id
    private Long studentId; 
    private String firstname;
    private String lastname;
 
    @OneToMany(mappedBy = "student")
    private List<Course> courses

    @OneToMany(mappedBy = "student")
    private List<Book> books

}


MultipleBagFetchException

Si vous tentez de lancer cette requête provenant d'un repository
 
@Query("""
  select s 
  from Student s 
  join fetch s.courses
  join fetch s.books
""")
 List<Student> findStudentWithCoursesBooks();

vous obtiendrez une errreur de type MultipleBagFetchException. Il n'est pas possible de fetcher plus qu'une entité qui va généré un produit cartésien.

Il pourrait être possible d'éviter cette erreur en changeant les list pour des set dans l'entité Student, cependant le produit cartésien se produira toujours.


Solution avec transaction

@Query("""
  select s 
  from Student s 
  join fetch s.courses
""")
 List<Student> findStudentWithCoursesBooks(); 


Dans un service

@Transactional
public List<Student> getStudent(){
     List<Student>  students = studentRepository.findStudentWithCoursesBooks();
     for(Student student: students){
          student.getBooks().size();
     } 
}

Il y aura chargement des livres, puisque l'annotation Transactional a été utilisé l'erreur LazyInitializationException ne survientdra pas. Cependant pour chaque étudiant, une requête sql pour aller chercher les Book. C'est le problème n+1 souvent mentionné dans le domaine des orm.

S'il y a que très peu de student, et que la méthode getStudent() est très peu utilisé. Cela pourrais être une solution possible. Il y a toujours possibilité d'ajouter du cache dans l'application afin de limiter les dégats

Solution avec multiple requêtes

Il est possible de combiner de multiple requete, une pour chaque fetch que vous désirez.
Le problème du n+1 est ainsi évite.
Il faut cependant que les deux retournes les même Students dans notre cas. Il faut donc ajouter une condition

@Query("""
  select distinct(s)
  from Student s 
  join fetch s.courses
  where s.studentid  < 10
""")
 List<Student> findStudentWithCourses();
 
@Query("""
  select distinct(s)
  from Student s 
  join fetch s.books
  where s.studentid  < 10
""")
 List<Student> findStudentWithBooks(List<Student> students); 
 
 
 
Dans une classe au niveau du service
 
@Service
public StudentService{

    private StudentRepository studentRepository;

    @Transactional
    public List<Students> getStudentWithCoursesBooks(){

        List<Student> students = studentRepository.findStudentWithCourses();

        return !students.isEmpty() ?
            studentRepository.findStudentWithBooks(
               minId,
               maxId
             ) :
           students;

        }

}
 
Au niveau des requêtes seul deux requetes sont exécutés .
  

Aucun commentaire:

Enregistrer un commentaire