JEE Back-end

Entity Graphs avec JPA

Dans cette séance

Les entités sont souvent liées entre elles (OneToMany, ManyToOne, etc.).

La problématique de ce cours est de contrôler quelles données de l'entité sont chargées quand on lit une classe depuis la base de donnée.

Après avoir rappeler la problématique, nous verons une solution versatile introduite dans la JPA 2.1 pour définir à la voléles parties de l’entité à charger ou à ignorer.

Le problème EAGER

@Entity
public class Student {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<Remark> remarks = new ArrayList<>();

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<Grade> grades = new ArrayList<>();

    ...
}

Quand un Student est chargé, les notes et les commentaires sont toujours chargés même si ce n'est pas nécessaire.

Le problème LAZY (1/2)

@Entity
public class Student {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Remark> remarks = new ArrayList<>();

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Grade> grades = new ArrayList<>();

    ...
}

Quand un Student est chargé, les notes et les commentaires ne sont chargées que si ils sont utilisés mais au prix d'un nouveau SELECT.

Le problème LAZY (2/2)

inTransaction(em -> {
	List<Student> students = em.createQuery("SELECT s FROM Student s", Student.class).getResultList();
    // les notes des étudiants ne sont pas chargées à ce momement
     var allGrades = new ArrayList<Grades>();  
    for (var student : students) {
        allGrades.addAll(students.getGrades());
        // student.getGrades() -> déclenche une requête par étudiant
    }
});

Il y a un SELECT par étudiant ! C'est un problème classique avec les relations LAZY.

Solution à base LEFT JOIN FETCH

inTransaction(em -> {
	List<Student> students = em.createQuery("SELECT s FROM Student s JOIN FETCH s.grades", Student.class).getResultList();
    // les notes des étudiants sont chargées à ce momement
    var allGrades = new ArrayList<Grades>();  
    for (var student : students) {
        allGrades.addAll(students.getGrades());
        // student.getGrades() -> ne déclenche pas de requête
    }
});

Il n'y a qu'un SELECT avec une jointure.

Vers une solution plus propre

La solution à base de JOIN FETCH marche mais elle vient mélanger la logique des requêtes et la logique du chargement des données.

Le Entity Graph permettent de spécifier en dehors de la requête, les informations que l'on souhaite charger quand on charge un étudiant.

Entity Graph example

@Entity
@NamedEntityGraph(
    name = "Student.withGrades",
    attributeNodes = {
        @NamedAttributeNode("grades")
    }
)
public Student findStudentWithGrades(EntityManager em, Long studentId) {
    EntityGraph entityGraph = em.getEntityGraph("Student.withGrades");
    Map<String, Object> hints = new HashMap<>();
    hints.put("jakarta.persistence.fetchgraph", entityGraph); 
    return em.find(Student.class, studentId, hints);
}

Solution à base de Graph Query

@Entity
@NamedEntityGraph(
    name = "Student.withGrades",
    attributeNodes = {
        @NamedAttributeNode("grades")
    }
)

inTransaction(em -> {
	EntityGraph entityGraph = em.getEntityGraph("Student.withGrades");
	List<Student> students = em.createQuery("SELECT s FROM Student s JOIN FETCH s.grades", Student.class).
	setHint("jakarta.persistence.fetchgraph", entityGraph)
	.getResultList();
    // les notes des étudiants sont chargées à ce momement
    var allGrades = new ArrayList<Grades>();  
    for (var student : students) {
        allGrades.addAll(students.getGrades());
        // student.getGrades() -> ne déclenche pas de requête
    }
});

Il n'y a qu'un SELECT avec une jointure.

Entity Graph (2/2)

@Entity
@NamedEntityGraph(
    name = "Student.withGradesAndRemarks",
    attributeNodes = {
        @NamedAttributeNode("grades"),
        @NamedAttributeNode("remarks")
    }
)

Entity Graph avec sous-graphes (1/2)

@Entity
class Teacher {
  @Id @GeneratedValue
  Long id;
  String name;
  @OneToMany(mappedBy = "teacher")
  Set<Student> students = new HashSet<>();
}

@Entity
class Student {
  @Id @GeneratedValue
  Long id;
  String name;
  @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
  List<Grade> grades = new ArrayList<>();
}

@Entity
class Grade {
  @Id @GeneratedValue
  Long id;
  double value;         // or BigDecimal
  String subject;       // optional
  LocalDate date;       // optional
}

EntityGraph avec sous-graphes (2/2)

@Entity
@NamedEntityGraph(
  name = "Teacher.withStudentsAndGrades",
  attributeNodes = @NamedAttributeNode(value = "students", subgraph = "studentsSubgraph"),
  subgraphs = @NamedSubgraph(
    name = "studentsSubgraph",
    attributeNodes = @NamedAttributeNode("grades")
  )
)