eadl:bloc3:dev_av:td2

2 - JPA Avancé et Optimisation

Séance 2 (4h)

Contexte : fil rouge e-commerce

  • 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

À réaliser :

  • Compléter OrderOrderItem (bidirectionnel)
  • Implémenter OrderUser (unidirectionnel)
  • Gérer UserCategory (preferences, Many-to-Many)
  • Ajouter @JsonIgnore / @JsonManagedReference pour éviter les boucles

Points d'attention :

  • Choix du côté propriétaire (mappedBy)
  • Cascade types appropriés
  • Orphan removal
  • Lazy vs Eager loading

// 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

Scénario :

GET /users/{id}/orders
// Retourne les commandes avec leurs items et produits

Mission :

  1. Activer les logs SQL (spring.jpa.show-sql=true)
  2. Identifier le problème N+1
  3. Compter le nombre de requêtes générées

À 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 :

  1. Optimiser /users/{id}/orders avec JOIN FETCH
  2. Créer une projection pour /products (liste)
  3. Comparer les performances avant/après

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

À 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

// Repository
List<Product> findAll(); // Tous types confondus
List<PhysicalProduct> findPhysicalProducts();

// Nouveaux endpoints
GET /products?type=PHYSICAL
GET /products?type=DIGITAL

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>

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

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 champ attributes à Product
  • Créer un endpoint GET /products/{id}/attributes
  • Filtrer les produits par attribut : GET /products?attr.color=Blue

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é Review avec Tsid
  • Comparer les performances d'insertion (benchmark)

Reviewid : Longrating : Integertitle : Stringcomment : Stringverified : BooleanhelpfulCount : IntegercreatedAt : LocalDateTimeupdatedAt : LocalDateTimeProductid : UUIDname : Stringprice : BigDecimalstock : IntegerUserid : UUIDusername : Stringemail : StringContraintes métier :• rating ∈ [1..5]• 1 review max par (user, product)• verified = true si achat confirmé• helpfulCount >= 0 Tsid Generator pour l'id(performance + tri chronologique)product10..*author10..*

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

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-..."]
}

# 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

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

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 @ManyToOne LAZY par défaut
  • eadl/bloc3/dev_av/td2.txt
  • Dernière modification : il y a 15 heures
  • de jcheron