2 - JPA Avancé et Optimisation
Séance 2 (4h)
Contexte : fil rouge e-commerce
Objectifs pédagogiques
- Maîtriser les associations bidirectionnelles et leurs pièges
- Comprendre et résoudre les problèmes N+1
- Utiliser l'héritage JPA à bon escient
- Optimiser les requêtes avec fetch strategies et projections
Partie 1 : Associations JPA (1h30)
1.1 Implémentation des associations manquantes
À réaliser :
- CompléterOrder↔OrderItem(bidirectionnel)
- ImplémenterOrder→User(unidirectionnel)
- GérerUser↔Category(preferences, Many-to-Many)
- Ajouter@JsonIgnore/@JsonManagedReferencepour éviter les boucles
Points d'attention :
- Choix du côté propriétaire (mappedBy)
- Cascade types appropriés
- Orphan removal
- Lazy vs Eager loading
1.2 Exercice pratique : Orders & OrderItems
// Contraintes métier à implémenter - Un Order doit toujours avoir au moins 1 OrderItem - Suppression d'un Order → suppression des OrderItems - totalAmount calculé automatiquement - Gestion du stock produit lors de la création
Tests attendus :
- Création d'une commande avec items
- Calcul automatique du total
- Mise à jour du stock
- Suppression en cascade
Chargement minimaliste
Pour recréer à moindre coût une relation (sans charger complètement l'instance depuis le repository)
val user = entityManager.getReference(User::class.java, userId);
Partie 2 : Problèmes de performance (1h30)
2.1 Diagnostic du problème N+1
Scénario :
GET /users/{id}/orders
// Retourne les commandes avec leurs items et produits
Mission :
- Activer les logs SQL (spring.jpa.show-sql=true)
- Identifier le problème N+1
- Compter le nombre de requêtes générées
2.2 Solutions d'optimisation
À implémenter et comparer :
| Solution | Cas d'usage | Avantages | Inconvénients | 
|---|---|---|---|
| @EntityGraph | Requêtes standards | Simple | Moins flexible | 
| JOIN FETCH | Requêtes complexes | Contrôle total | Code JPQL | 
| @BatchSize | Lazy loading | Transparent | Moins optimal | 
| DTO Projection | Lecture seule | Performances max | Plus de code | 
Exercices :
- Optimiser/users/{id}/ordersavec JOIN FETCH
- Créer une projection pour/products(liste)
- Comparer les performances avant/après
Partie 3 : Héritage JPA (1h)
3.1 Cas d'usage : Typologie de produits
Nouveau besoin métier :
Différencier 3 types de produits :
- PhysicalProduct : poids, dimensions, frais de port
- DigitalProduct : taille fichier, URL download, format
- ServiceProduct : durée, date prestation
Tous partagent : id, name, price, stock, category
3.2 Implémentation avec stratégies d'héritage
À explorer (au choix ou comparaison) :
// Option 1 : SINGLE_TABLE (par défaut) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "product_type") // Option 2 : JOINED @Inheritance(strategy = InheritanceType.JOINED) // Option 3 : TABLE_PER_CLASS @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
Exercice comparatif :
- Schéma base de données généré
- Requêtes SQL produites
- Avantages/inconvénients de chaque stratégie
3.3 Requêtes polymorphiques
// Repository List<Product> findAll(); // Tous types confondus List<PhysicalProduct> findPhysicalProducts(); // Nouveaux endpoints GET /products?type=PHYSICAL GET /products?type=DIGITAL
Partie 4 : Hypersistence Utils - Outils avancés (30min-1h)
4.1 Introduction à Hypersistence Utils
Hypersistence Utils est une bibliothèque créée par Vlad Mihalcea qui apporte :
- Des types personnalisés (JSON, Array, etc.)
- Des utilitaires de diagnostic de performance
- Des listeners pour optimiser les opérations
- Des identifiants optimisés (Tsid)
Dépendance Maven
<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-63</artifactId>
    <version>3.7.0</version>
</dependency>
4.2 Détection automatique des problèmes N+1
Objectif : Détecter automatiquement les problèmes de performance sans analyse manuelle des logs
Configuration
# application.properties - Ajout pour Hypersistence # Détection des problèmes N+1 logging.level.io.hypersistence.utils=DEBUG # Limites d'alerte (optionnel) hypersistence.query.fail.on.pagination.over.collection.fetch=false
Utilisation du QueryStackTraceLogger
// Configuration globale (classe @Configuration)
@Configuration
public class HypersistenceConfiguration {
    
    @Bean
    public QueryStackTraceLogger queryStackTraceLogger() {
        return new QueryStackTraceLogger();
    }
    
    @EventListener
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // Active la détection des problèmes N+1
        QueryStackTraceLogger.INSTANCE.setThreshold(10); // Alerte si > 10 requêtes
    }
}
Exercice :
- Activer le logger sur l'endpoint/users/{id}/orders
- Observer les alertes automatiques
- Corriger les problèmes détectés
4.3 Types JSON natifs
Cas d'usage : Stocker des métadonnées flexibles sur les produits
Exemple : Attributs dynamiques produit
@Entity
@Table(name = "products")
public class Product {
    // ... attributs existants
    
    @Type(JsonType.class)
    @Column(columnDefinition = "json")
    private Map<String, Object> attributes;
    
    // Pour PhysicalProduct : {"weight": 2.5, "dimensions": "30x20x10"}
    // Pour DigitalProduct : {"fileSize": "1.2GB", "format": "PDF"}
}
Données exemple
{
  "id": "550e8400-e29b-41d4-a716-446655440020",
  "name": "iPhone 15 Pro",
  "price": 1199.99,
  "stock": 25,
  "categoryId": "550e8400-e29b-41d4-a716-446655440010",
  "attributes": {
    "color": "Titanium Blue",
    "storage": "256GB",
    "warranty": "2 years"
  }
}
Exercice :
- Ajouter le champattributesàProduct
- Créer un endpointGET /products/{id}/attributes
- Filtrer les produits par attribut :GET /products?attr.color=Blue
4.4 Optimisation des identifiants avec Tsid
Tsid (Time-Sorted Identifiers) :
- Alternative performante aux UUID
- Triables chronologiquement
- Plus compacts (Long au lieu de UUID)
- Meilleure performance en base
Comparaison UUID vs Tsid
// Avant (UUID) @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; // Après (Tsid) - Pour nouvelles entités @Id @TsidGenerator private Long id;
Exercice optionnel :
- Créer une nouvelle entitéReviewavec Tsid
- Comparer les performances d'insertion (benchmark)
4.5 Monitoring des requêtes en temps réel
DataSourceProxyBeanPostProcessor
@Configuration
public class DataSourceProxyConfiguration {
    
    @Bean
    public DataSourceProxyBeanPostProcessor dataSourceProxyBeanPostProcessor() {
        return new DataSourceProxyBeanPostProcessor() {
            @Override
            protected DataSourceProxy createDataSourceProxy(DataSource dataSource) {
                return new DataSourceProxy(dataSource, new QueryCountHolder());
            }
        };
    }
}
Exercice :
- Mettre en place le monitoring
- Créer un test d'intégration qui vérifie le nombre exact de requêtes
- Exemple :assertQueryCount(3)après un appel API
4.6 Exercice intégratif
Mission : Améliorer l'endpoint recommendations
GET /users/{id}/recommendations
Avec Hypersistence :
- Détecter automatiquement les problèmes N+1
- Limiter à 5 requêtes maximum (assertion en test)
- Stocker les préférences utilisateur en JSON
- Logger les performances de la recommandation
Structure JSON recommandée :
// User.preferences (JSON)
{
  "priceRange": {"min": 50, "max": 500},
  "brands": ["Apple", "Samsung"],
  "excludeCategories": ["550e8400-..."]
}
Configuration complète
# application.properties - Configuration complète Séance 2 # H2 Database spring.datasource.url=jdbc:h2:file:./data/ecommerce spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # H2 Console spring.h2.console.enabled=true spring.h2.console.path=/h2-console # JPA/Hibernate spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true spring.jpa.properties.hibernate.generate_statistics=true # Logging SQL et statistiques logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE logging.level.org.hibernate.stat=DEBUG logging.level.org.hibernate.orm.jdbc.bind=TRACE # Hypersistence Utils logging.level.io.hypersistence.utils=DEBUG
Livrables attendus
Priorités (4h)
Must have :
- ✅ Associations Order/OrderItem/User complètes avec tests
- ✅ Résolution problème N+1 sur au moins 2 endpoints
- ✅ Implémentation héritage produits (1 stratégie au choix)
- ✅ Hypersistence : détection automatique N+1 activée
- ✅ Tests d'intégration validant les performances
Nice to have :
- Comparaison des 3 stratégies d'héritage
- Type JSON pour attributs dynamiques produits
- Tsid sur une nouvelle entité (Review, Wishlist…)
- Benchmark avant/après optimisations avec query count assertions
- Documentation des choix architecturaux
Ressources
Conseils :
- Commencer par les associations avant l'optimisation
- Toujours mesurer avant d'optimiser (logs SQL)
- L'héritage n'est pas toujours la meilleure solution (composition > héritage)
- Privilégier@ManyToOneLAZY par défaut
 jcheron
 jcheron