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.
@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.
@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.
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.
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.
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
@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);
}
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
@NamedEntityGraph(
name = "Student.withGradesAndRemarks",
attributeNodes = {
@NamedAttributeNode("grades"),
@NamedAttributeNode("remarks")
}
)
@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
}
@Entity
@NamedEntityGraph(
name = "Teacher.withStudentsAndGrades",
attributeNodes = @NamedAttributeNode(value = "students", subgraph = "studentsSubgraph"),
subgraphs = @NamedSubgraph(
name = "studentsSubgraph",
attributeNodes = @NamedAttributeNode("grades")
)
)