Chapitre 1: Questions d'Entrevue — JPA & Base de Données
Ce chapitre couvre les questions les plus fréquentes sur JPA, Hibernate, les relations entre entités, et les problèmes de performance en base de données. Chaque question inclut une réponse vulgarisée et une phrase clé à dire en entrevue.
Q1. Qu'est-ce que le problème N+1 en JPA ?
L'analogie du restaurant
Imagine que tu demandes à un serveur au restaurant :
"Apporte-moi les 10 commandes"
Puis, pour chaque commande, tu redemandes :
"Et c'est qui le client de cette commande ?"
Tu ne fais pas 1 seule grosse demande. Tu fais :
- 1 demande pour les commandes
- puis 10 petites demandes pour les clients
Ça fait beaucoup d'allers-retours inutiles.
C'est quoi N+1 concrètement ?
1 requête pour la liste principale
+ N requêtes pour les données liées
────────────────────────────────
= N + 1 requêtes au total
Si N = 10 commandes :
SELECT * FROM orders; -- 1 requête
SELECT * FROM customers WHERE id = 1; -- requête 2
SELECT * FROM customers WHERE id = 2; -- requête 3
SELECT * FROM customers WHERE id = 3; -- requête 4
...
SELECT * FROM customers WHERE id = 10; -- requête 11
Total = 11 requêtes, alors qu'une seule aurait suffi.
Pourquoi ça arrive ?
Parce que souvent la relation order.customer est chargée en lazy loading. JPA ne charge pas le client au début — il attend que tu fasses order.getCustomer().
Le code qui provoque le N+1
List<Order> orders = orderRepository.findAll();
for (Order o : orders) {
System.out.println(o.getCustomer().getName());
// ↑ CHAQUE appel déclenche une requête SQL !
}
Ici :
findAll()charge les orders → 1 requêtegetCustomer()déclenche 1 requête par order → N requêtes
La solution : JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomers();
JPA fait une seule requête qui ramène à la fois les commandes et leurs clients.
Alternative : @EntityGraph
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();
Alternative : @BatchSize
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 50)
private Customer customer;
}
Au lieu de faire N requêtes, JPA en fait N/50 (charge les clients par lots de 50).
Image mentale simple
| Approche | Voyages |
|---|---|
| Sans correction (N+1) | 11 voyages pour 10 commandes |
| Avec JOIN FETCH | 1 seul voyage pour tout |
EN : "The N+1 problem happens when JPA fires one query per related entity. I fix it with JOIN FETCH or @EntityGraph to load everything in a single query."
FR : "Le problème N+1 arrive quand JPA exécute une requête initiale, puis une requête supplémentaire pour chaque entité liée. Je le corrige avec JOIN FETCH ou @EntityGraph pour tout charger en une seule requête."
Q2. Quelle est la différence entre Lazy Loading et Eager Loading ?
| Stratégie | Comportement | Défaut pour |
|---|---|---|
| LAZY | Charge les données liées seulement quand on y accède | @OneToMany, @ManyToMany |
| EAGER | Charge les données liées immédiatement avec l'entité | @ManyToOne, @OneToOne |
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
EAGER partout = catastrophe de performance. Si chaque entité charge ses 5 relations en EAGER, une simple requête peut déclencher des centaines de requêtes en cascade.
Bonne pratique : Tout en LAZY + JOIN FETCH quand nécessaire.
"I default everything to LAZY loading and use JOIN FETCH or EntityGraph when I need to eagerly load specific associations for a use case."
Q3. Quelle est la différence entre JpaRepository, CrudRepository et PagingAndSortingRepository ?
JpaRepository (le plus complet)
↑ hérite de
PagingAndSortingRepository (pagination + tri)
↑ hérite de
CrudRepository (CRUD de base)
↑ hérite de
Repository (interface marqueur)
| Interface | Méthodes ajoutées |
|---|---|
| CrudRepository | save(), findById(), findAll(), deleteById(), count() |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) |
| JpaRepository | flush(), saveAndFlush(), deleteInBatch(), retourne List<T> au lieu de Iterable<T> |
En pratique, on utilise presque toujours JpaRepository.
Q4. Expliquez les stratégies de propagation de @Transactional
| Propagation | Comportement |
|---|---|
REQUIRED (défaut) | Rejoint la transaction existante ou en crée une nouvelle |
REQUIRES_NEW | Crée toujours une nouvelle transaction (suspend l'existante) |
SUPPORTS | Rejoint si existante, sinon exécute sans transaction |
NOT_SUPPORTED | Exécute toujours sans transaction |
MANDATORY | Transaction existante requise, sinon exception |
NEVER | Pas de transaction, sinon exception |
NESTED | Transaction imbriquée (savepoint) |
Cas d'usage concret de REQUIRES_NEW
@Transactional
public void processPayment(Order order) {
paymentGateway.charge(order); // Transaction principale
auditService.logPayment(order); // Si ça échoue, le paiement doit quand même persister
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPayment(Order order) {
// Nouvelle transaction indépendante
// Même si cette transaction échoue, le paiement est sauvé
}
}
"I use REQUIRED by default. I switch to REQUIRES_NEW when I need an independent transaction, like audit logging that must persist regardless of the main transaction's outcome."
Q5. Différence entre @Transactional(readOnly = true) et @Transactional
@Transactional(readOnly = true) // Lecture : Hibernate désactive le dirty checking
public List<User> findAll() { ... }
@Transactional // Écriture : commit si succès, rollback si exception
public User create(UserDto dto) { ... }
Bonne pratique : Mettre @Transactional(readOnly = true) au niveau de la classe, et @Transactional sur les méthodes d'écriture :
@Service
@Transactional(readOnly = true)
public class UserService {
public List<User> findAll() { ... } // Hérite de readOnly = true
public User findById(Long id) { ... } // Hérite de readOnly = true
@Transactional // Override : écriture
public User create(UserDto dto) { ... }
@Transactional // Override : écriture
public void delete(Long id) { ... }
}
Q6. Comment implémenter la pagination dans Spring Data JPA ?
// Repository — rien à faire, JpaRepository supporte Pageable nativement
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByRole(Role role, Pageable pageable);
}
// Controller
@GetMapping("/users")
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userRepository.findAll(pageable);
}
Le Page<T> retourné contient : contenu, numéro de page, taille, total d'éléments, nombre de pages.
Q7. Qu'est-ce que les Query Methods dans Spring Data JPA ?
Spring Data génère automatiquement les requêtes SQL à partir du nom de la méthode :
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
// → SELECT * FROM users WHERE name = ?
List<User> findByNameAndEmail(String name, String email);
// → SELECT * FROM users WHERE name = ? AND email = ?
List<User> findByAgeBetween(int min, int max);
// → SELECT * FROM users WHERE age BETWEEN ? AND ?
List<User> findByNameContainingIgnoreCase(String keyword);
// → SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%keyword%')
Optional<User> findByEmail(String email);
// → SELECT * FROM users WHERE email = ? LIMIT 1
long countByRole(Role role);
// → SELECT COUNT(*) FROM users WHERE role = ?
boolean existsByEmail(String email);
// → SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM users WHERE email = ?
}
"Spring Data JPA derives queries from method names. For complex queries, I use @Query with JPQL or native SQL."
Q8. Quand utiliser @Query vs les Query Methods ?
| Approche | Quand l'utiliser |
|---|---|
| Query Methods | Requêtes simples (1-2 conditions) |
| @Query (JPQL) | Requêtes complexes, JOIN, agrégations |
| @Query (native) | Requêtes SQL spécifiques au SGBD, performances critiques |
// JPQL
@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
List<User> findActiveByDepartment(@Param("dept") String dept);
// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<User> findRecentUsers();
Q9. Comment gérer les relations entre entités ?
@ManyToOne / @OneToMany (la plus fréquente)
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Entity
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
}
@ManyToMany
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
Toujours utiliser Set pour @ManyToMany, jamais List — sinon Hibernate peut générer des requêtes inefficaces (supprime tout puis réinsère).
⬅️ Retour au Module 18 | Chapitre 2: Exercices de Refactoring →