eadl:bloc3:dev_av:td2

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentes Révision précédente
Prochaine révision
Révision précédente
eadl:bloc3:dev_av:td2 [2025/10/07 23:35] jcheroneadl:bloc3:dev_av:td2 [2025/11/09 16:30] (Version actuelle) jcheron
Ligne 32: Ligne 32:
 ==== 1.2 Exercice pratique : Orders & OrderItems ==== ==== 1.2 Exercice pratique : Orders & OrderItems ====
  
-<sxh java;gutter:false>+<sxh kotlin>
 // Contraintes métier à implémenter // Contraintes métier à implémenter
-- Un Order doit toujours avoir au moins 1 OrderItem +// - Un Order doit toujours avoir au moins 1 OrderItem 
-- Suppression d'un Order → suppression des OrderItems +// - Suppression d'un Order → suppression des OrderItems 
-- totalAmount calculé automatiquement +// - totalAmount calculé automatiquement 
-- Gestion du stock produit lors de la création+// - Gestion du stock produit lors de la création 
 + 
 +@Entity 
 +@Table(name = "orders"
 +class Order( 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "user_id", nullable = false) 
 +    val user: User, 
 + 
 +    @Enumerated(EnumType.STRING) 
 +    @Column(nullable = false) 
 +    var status: OrderStatus = OrderStatus.PENDING, 
 + 
 +    @Column(nullable = false) 
 +    var totalAmount: BigDecimal = BigDecimal.ZERO, 
 + 
 +    @Column(nullable = false) 
 +    val createdAt: Instant = Instant.now() 
 +) { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null 
 + 
 +    @OneToMany( 
 +        mappedBy = "order", 
 +        cascade = [CascadeType.ALL], 
 +        orphanRemoval = true, 
 +        fetch = FetchType.LAZY 
 +    ) 
 +    @JsonManagedReference 
 +    private val _items: MutableList<OrderItem> = mutableListOf() 
 + 
 +    val items: List<OrderItem> 
 +        get() = _items.toList() 
 + 
 +    fun addItem(item: OrderItem) { 
 +        require(_items.isEmpty() || _items.size < 100) { 
 +            "Cannot add more than 100 items to an order" 
 +        } 
 +        _items.add(item) 
 +        item.order = this 
 +        recalculateTotal() 
 +    } 
 + 
 +    fun removeItem(item: OrderItem) { 
 +        _items.remove(item) 
 +        item.order = null 
 +        recalculateTotal() 
 +    } 
 + 
 +    private fun recalculateTotal() { 
 +        totalAmount = _items.sumOf { it.unitPrice * it.quantity.toBigDecimal() } 
 +    } 
 + 
 +    init { 
 +        require(user.id != null) { "User must be persisted before creating an order" } 
 +    } 
 +
 + 
 +@Entity 
 +@Table(name = "order_items"
 +class OrderItem( 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "product_id", nullable = false) 
 +    val product: Product, 
 + 
 +    @Column(nullable = false) 
 +    val quantity: Int, 
 + 
 +    @Column(nullable = false, precision = 10, scale = 2) 
 +    val unitPrice: BigDecimal 
 +) { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null 
 + 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "order_id", nullable = false) 
 +    @JsonBackReference 
 +    var order: Order? = null 
 + 
 +    init { 
 +        require(quantity > 0) { "Quantity must be positive"
 +        require(unitPrice > BigDecimal.ZERO) { "Unit price must be positive"
 +    } 
 +
 + 
 +enum class OrderStatus { 
 +    PENDING, 
 +    CONFIRMED, 
 +    SHIPPED, 
 +    DELIVERED, 
 +    CANCELLED 
 +}
 </sxh> </sxh>
  
Ligne 45: Ligne 138:
   * Mise à jour du stock   * Mise à jour du stock
   * Suppression en cascade   * Suppression en cascade
 +
 +<sxh kotlin>
 +@SpringBootTest
 +@Transactional
 +class OrderServiceTest {
 +
 +    @Autowired
 +    private lateinit var orderService: OrderService
 +
 +    @Autowired
 +    private lateinit var userRepository: UserRepository
 +
 +    @Autowired
 +    private lateinit var productRepository: ProductRepository
 +
 +    @Test
 +    fun `should create order with items and calculate total`() {
 +        // Given
 +        val user = userRepository.save(User("John Doe", "john@example.com"))
 +        val product1 = productRepository.save(
 +            Product("iPhone", BigDecimal("999.99"), 10, category)
 +        )
 +        val product2 = productRepository.save(
 +            Product("MacBook", BigDecimal("1999.99"), 5, category)
 +        )
 +
 +        val dto = CreateOrderDto(
 +            userId = user.id!!,
 +            items = listOf(
 +                OrderItemDto(product1.id!!, 2),
 +                OrderItemDto(product2.id!!, 1)
 +            )
 +        )
 +
 +        // When
 +        val order = orderService.createOrder(dto)
 +
 +        // Then
 +        assertThat(order.items).hasSize(2)
 +        assertThat(order.totalAmount).isEqualByComparingTo("3999.97") // 2*999.99 + 1999.99
 +        assertThat(product1.stock).isEqualTo(8) // 10 - 2
 +        assertThat(product2.stock).isEqualTo(4) // 5 - 1
 +    }
 +
 +    @Test
 +    fun `should fail when insufficient stock`() {
 +        // Given
 +        val user = userRepository.save(User("Jane", "jane@example.com"))
 +        val product = productRepository.save(
 +            Product("Limited Item", BigDecimal("50.00"), 2, category)
 +        )
 +
 +        val dto = CreateOrderDto(
 +            userId = user.id!!,
 +            items = listOf(OrderItemDto(product.id!!, 5))
 +        )
 +
 +        // When & Then
 +        assertThatThrownBy { orderService.createOrder(dto) }
 +            .isInstanceOf(InsufficientStockException::class.java)
 +    }
 +}
 +</sxh>
 +
 +==== Chargement minimaliste ====
 +Pour recréer à moindre coût une relation (sans charger complètement l'instance depuis le repository)
 +
 +<sxh kotlin>
 +val user = entityManager.getReference(User::class.java, userId)
 +</sxh>
  
 ===== Partie 2 : Problèmes de performance (1h30) ===== ===== Partie 2 : Problèmes de performance (1h30) =====
Ligne 52: Ligne 215:
 <WRAP round bloc important> <WRAP round bloc important>
 **Scénario :** **Scénario :**
-<sxh;gutter:false+<sxh kotlin
-GET /users/{id}/orders+// GET /users/{id}/orders
 // Retourne les commandes avec leurs items et produits // Retourne les commandes avec leurs items et produits
 </sxh> </sxh>
Ligne 77: Ligne 240:
   - Créer une projection pour ''/products'' (liste)   - Créer une projection pour ''/products'' (liste)
   - Comparer les performances avant/après   - Comparer les performances avant/après
 +
 +<sxh kotlin>
 +// ❌ PROBLÈME N+1 : Sans optimisation
 +interface OrderRepository : JpaRepository<Order, UUID> {
 +    fun findByUserId(userId: UUID): List<Order>
 +    // 1 requête pour les orders
 +    // N requêtes pour charger les items de chaque order
 +    // M requêtes pour charger les produits de chaque item
 +}
 +
 +// ✅ SOLUTION 1 : JOIN FETCH
 +interface OrderRepository : JpaRepository<Order, UUID> {
 +    @Query("""
 +        SELECT DISTINCT o FROM Order o
 +        JOIN FETCH o.items i
 +        JOIN FETCH i.product
 +        WHERE o.user.id = :userId
 +    """)
 +    fun findByUserIdWithItems(userId: UUID): List<Order>
 +}
 +
 +// ✅ SOLUTION 2 : @EntityGraph
 +interface OrderRepository : JpaRepository<Order, UUID> {
 +    @EntityGraph(attributePaths = ["items", "items.product"])
 +    fun findByUserId(userId: UUID): List<Order>
 +}
 +
 +// ✅ SOLUTION 3 : DTO Projection
 +data class OrderSummaryDto(
 +    val id: UUID,
 +    val totalAmount: BigDecimal,
 +    val status: OrderStatus,
 +    val itemCount: Long,
 +    val createdAt: Instant
 +)
 +
 +interface OrderRepository : JpaRepository<Order, UUID> {
 +    @Query("""
 +        SELECT new com.ecommerce.order.dto.OrderSummaryDto(
 +            o.id, o.totalAmount, o.status, COUNT(i), o.createdAt
 +        )
 +        FROM Order o
 +        LEFT JOIN o.items i
 +        WHERE o.user.id = :userId
 +        GROUP BY o.id, o.totalAmount, o.status, o.createdAt
 +    """)
 +    fun findOrderSummariesByUserId(userId: UUID): List<OrderSummaryDto>
 +}
 +</sxh>
  
 ===== Partie 3 : Héritage JPA (1h) ===== ===== Partie 3 : Héritage JPA (1h) =====
Ligne 97: Ligne 309:
 **À explorer (au choix ou comparaison) :** **À explorer (au choix ou comparaison) :**
  
-<sxh java;gutter:false>+<sxh kotlin>
 // Option 1 : SINGLE_TABLE (par défaut) // Option 1 : SINGLE_TABLE (par défaut)
 +@Entity
 +@Table(name = "products")
 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
-@DiscriminatorColumn(name = "product_type")+@DiscriminatorColumn(name = "product_type", discriminatorType = DiscriminatorType.STRING) 
 +abstract class Product( 
 +    @Column(nullable = false) 
 +    open var name: String,
  
-// Option 2 : JOINED+    @Column(nullable = false, precision = 10, scale = 2) 
 +    open var price: BigDecimal, 
 + 
 +    @Column(nullable = false) 
 +    open var stock: Int, 
 + 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "category_id", nullable = false) 
 +    open var category: Category 
 +) { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    open var id: UUID? = null 
 +
 + 
 +@Entity 
 +@DiscriminatorValue("PHYSICAL"
 +class PhysicalProduct( 
 +    name: String, 
 +    price: BigDecimal, 
 +    stock: Int, 
 +    category: Category, 
 + 
 +    @Column(name = "weight_kg"
 +    var weight: Double, 
 + 
 +    @Column(name = "dimensions"
 +    var dimensions: String, // "30x20x10" 
 + 
 +    @Column(name = "shipping_cost", precision = 10, scale = 2) 
 +    var shippingCost: BigDecimal 
 +) : Product(name, price, stock, category) 
 + 
 +@Entity 
 +@DiscriminatorValue("DIGITAL"
 +class DigitalProduct( 
 +    name: String, 
 +    price: BigDecimal, 
 +    stock: Int, 
 +    category: Category, 
 + 
 +    @Column(name = "file_size_mb"
 +    var fileSize: Double, 
 + 
 +    @Column(name = "download_url"
 +    var downloadUrl: String, 
 + 
 +    @Column(name = "file_format"
 +    var format: String // PDF, MP4, ZIP... 
 +) : Product(name, price, stock, category) 
 + 
 +@Entity 
 +@DiscriminatorValue("SERVICE"
 +class ServiceProduct( 
 +    name: String, 
 +    price: BigDecimal, 
 +    stock: Int, 
 +    category: Category, 
 + 
 +    @Column(name = "duration_hours"
 +    var duration: Int, 
 + 
 +    @Column(name = "service_date"
 +    var serviceDate: LocalDate? 
 +) : Product(name, price, stock, category) 
 + 
 +// Option 2 : JOINED (tables séparées) 
 +@Entity 
 +@Table(name = "products")
 @Inheritance(strategy = InheritanceType.JOINED) @Inheritance(strategy = InheritanceType.JOINED)
 +abstract class Product(
 +    // ... mêmes champs
 +)
 +
 +@Entity
 +@Table(name = "physical_products")
 +class PhysicalProduct(
 +    // ... champs spécifiques
 +) : Product(...)
  
-// Option 3 : TABLE_PER_CLASS+// Option 3 : TABLE_PER_CLASS (une table complète par classe concrète) 
 +@Entity
 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
 +abstract class Product(
 +    // ... mêmes champs
 +)
 </sxh> </sxh>
  
Ligne 116: Ligne 414:
 ==== 3.3 Requêtes polymorphiques ==== ==== 3.3 Requêtes polymorphiques ====
  
-<sxh java;gutter:false>+<sxh kotlin>
 // Repository // Repository
-List<Product> findAll(); // Tous types confondus +interface ProductRepository : JpaRepository<Product, UUID
-List<PhysicalProduct> findPhysicalProducts();+    // Tous types confondus 
 +    override fun findAll(): List<Product> 
 +     
 +    // Seulement les produits physiques 
 +    @Query("SELECT p FROM PhysicalProduct p") 
 +    fun findPhysicalProducts(): List<PhysicalProduct> 
 +     
 +    // Filtrage par type 
 +    @Query("SELECT p FROM Product p WHERE TYPE(p) = :type"
 +    fun findByType(type: Class<out Product>): List<Product> 
 +}
  
-// Nouveaux endpoints +// Controller 
-GET /products?type=PHYSICAL +@RestController 
-GET /products?type=DIGITAL+@RequestMapping("/products") 
 +class ProductController(private val repository: ProductRepository) { 
 + 
 +    @GetMapping 
 +    fun getProducts(@RequestParam(required = false) type: String?): List<Product>
 +        return when (type?.uppercase()) { 
 +            "PHYSICAL" -> repository.findByType(PhysicalProduct::class.java) 
 +            "DIGITAL" -> repository.findByType(DigitalProduct::class.java) 
 +            "SERVICE" -> repository.findByType(ServiceProduct::class.java) 
 +            else -> repository.findAll() 
 +        } 
 +    } 
 +
 +</sxh> 
 + 
 +===== Partie 4 : Hypersistence Utils - Outils avancés (30min-1h) ===== 
 + 
 +==== 4.1 Introduction à Hypersistence Utils ==== 
 + 
 +<WRAP round bloc info> 
 +**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) 
 +</WRAP> 
 + 
 +=== Dépendance Maven === 
 + 
 +<sxh xml> 
 +<dependency> 
 +    <groupId>io.hypersistence</groupId> 
 +    <artifactId>hypersistence-utils-hibernate-63</artifactId> 
 +    <version>3.7.0</version> 
 +</dependency> 
 +</sxh> 
 + 
 +==== 4.2 Détection automatique des problèmes N+1 ==== 
 + 
 +<WRAP round bloc important> 
 +**Objectif :** Détecter automatiquement les problèmes de performance sans analyse manuelle des logs 
 +</WRAP> 
 + 
 +=== Configuration === 
 + 
 +<sxh kotlin> 
 +@Configuration 
 +class HypersistenceConfiguration { 
 +     
 +    @Bean 
 +    fun queryStackTraceLogger() = QueryStackTraceLogger() 
 +     
 +    @EventListener 
 +    fun onApplicationReady(event: ApplicationReadyEvent) { 
 +        // Active la détection des problèmes N+1 
 +        QueryStackTraceLogger.INSTANCE.threshold = 10 // Alerte si > 10 requêtes 
 +    } 
 +
 +</sxh> 
 + 
 +**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 ==== 
 + 
 +<WRAP round bloc todo> 
 +**Cas d'usage :** Stocker des métadonnées flexibles sur les produits 
 +</WRAP> 
 + 
 +=== Exemple : Attributs dynamiques produit === 
 + 
 +<sxh kotlin> 
 +@Entity 
 +@Table(name = "products"
 +class Product( 
 +    @Column(nullable = false) 
 +    var name: String, 
 + 
 +    @Column(nullable = false, precision = 10, scale = 2) 
 +    var price: BigDecimal, 
 + 
 +    @Column(nullable = false) 
 +    var stock: Int, 
 + 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "category_id"
 +    var category: Category, 
 + 
 +    // ✅ Stockage JSON pour attributs dynamiques 
 +    @Type(JsonType::class) 
 +    @Column(columnDefinition = "json"
 +    var attributes: Map<String, Any> = emptyMap() 
 +) { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null 
 +
 + 
 +// Utilisation 
 +val product = Product( 
 +    name = "iPhone 15 Pro", 
 +    price = BigDecimal("1199.99"), 
 +    stock = 25, 
 +    category = electronicsCategory, 
 +    attributes = mapOf( 
 +        "color" to "Titanium Blue", 
 +        "storage" to "256GB", 
 +        "warranty" to "2 years", 
 +        "features" to listOf("5G", "Face ID", "A17 Pro"
 +    ) 
 +
 +</sxh> 
 + 
 +=== Données exemple === 
 + 
 +<sxh json> 
 +
 +  "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", 
 +    "features": ["5G", "Face ID", "A17 Pro"
 +  } 
 +
 +</sxh> 
 + 
 +**Exercice :** 
 +  * Ajouter le champ ''attributes'' à ''Product'' 
 +  * Créer un endpoint ''GET /products/{id}/attributes'' 
 +  * Filtrer les produits par attribut : ''GET /products?attr.color=Blue'' 
 + 
 +==== 4.4 Optimisation des identifiants avec Tsid ==== 
 + 
 +<WRAP round bloc info> 
 +**Tsid (Time-Sorted Identifiers)** : 
 +  * Alternative performante aux UUID 
 +  * Triables chronologiquement 
 +  * Plus compacts (Long au lieu de UUID) 
 +  * Meilleure performance en base 
 +</WRAP> 
 + 
 +=== Comparaison UUID vs Tsid === 
 + 
 +<sxh kotlin> 
 +// Avant (UUID) 
 +@Entity 
 +class Review( 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null, 
 +    // ... 
 +
 + 
 +// Après (Tsid) - Pour nouvelles entités 
 +@Entity 
 +@Table(name = "reviews"
 +class Review( 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "product_id", nullable = false) 
 +    val product: Product, 
 + 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "user_id", nullable = false) 
 +    val author: User, 
 + 
 +    @Column(nullable = false) 
 +    val rating: Int, // 1-5 
 + 
 +    @Column(nullable = false, length = 200) 
 +    val title: String, 
 + 
 +    @Column(nullable = false, length = 2000) 
 +    val comment: String, 
 + 
 +    @Column(nullable = false) 
 +    var verified: Boolean = false, 
 + 
 +    @Column(nullable = false) 
 +    var helpfulCount: Int = 0, 
 + 
 +    @Column(nullable = false) 
 +    val createdAt: Instant = Instant.now(), 
 + 
 +    @Column(nullable = false) 
 +    var updatedAt: Instant = Instant.now() 
 +) { 
 +    @Id 
 +    @TsidGenerator 
 +    var id: Long? = null 
 + 
 +    init { 
 +        require(rating in 1..5) { "Rating must be between 1 and 5" } 
 +        require(title.isNotBlank()) { "Title cannot be blank" } 
 +        require(comment.isNotBlank()) { "Comment cannot be blank" } 
 +        require(helpfulCount >= 0) { "Helpful count cannot be negative"
 +    } 
 +
 +</sxh> 
 + 
 +**Exercice optionnel :** 
 +  * Créer une nouvelle entité ''Review'' avec Tsid 
 +  * Comparer les performances d'insertion (benchmark) 
 + 
 +<html><div class="imageB"></html> 
 +<uml> 
 +@startuml Review Domain Model 
 + 
 +class Review { 
 +  - id : Long 
 +  - rating : Integer 
 +  - title : String 
 +  - comment : String 
 +  - verified : Boolean 
 +  - helpfulCount : Integer 
 +  - createdAt : LocalDateTime 
 +  - updatedAt : LocalDateTime 
 +
 + 
 +class Product { 
 +  - id : UUID 
 +  - name : String 
 +  - price : BigDecimal 
 +  - stock : Integer 
 +
 + 
 +class User { 
 +  - id : UUID 
 +  - username : String 
 +  - email : String 
 +
 + 
 +Product "1" -- "0..*" Review : product 
 +User "1" -- "0..*" Review : author 
 + 
 +note right of Review 
 +  Contraintes 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) 
 +end note 
 + 
 +@enduml 
 +</uml> 
 +<html></div></html> 
 + 
 +==== 4.5 Monitoring des requêtes en temps réel ==== 
 + 
 +=== DataSourceProxyBeanPostProcessor === 
 + 
 +<sxh kotlin> 
 +@Configuration 
 +class DataSourceProxyConfiguration { 
 +     
 +    @Bean 
 +    fun dataSourceProxyBeanPostProcessor() = object : DataSourceProxyBeanPostProcessor() { 
 +        override fun createDataSourceProxy(dataSource: DataSource): DataSourceProxy { 
 +            return DataSourceProxy(dataSource, QueryCountHolder()) 
 +        } 
 +    } 
 +
 +</sxh> 
 + 
 +**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 
 + 
 +<sxh kotlin> 
 +@SpringBootTest 
 +@AutoConfigureMockMvc 
 +@ActiveProfiles("test"
 +@Transactional 
 +class OrderPerformanceTest { 
 + 
 +    @Autowired 
 +    private lateinit var mockMvc: MockMvc 
 + 
 +    @Test 
 +    fun `should not trigger N+1 queries when fetching user orders`() { 
 +        // Given 
 +        val userId = createUserWithOrders() 
 + 
 +        // When 
 +        SQLStatementCountValidator.reset() 
 +         
 +        mockMvc.perform(get("/users/$userId/orders")) 
 +            .andExpect(status().isOk) 
 + 
 +        // Then - Vérifier le nombre de requêtes SQL 
 +        assertSelectCount(2) // 1 pour User + 1 pour Orders avec items (JOIN FETCH) 
 +    } 
 +
 +</sxh> 
 + 
 +==== 4.6 Exercice intégratif ==== 
 + 
 +<WRAP round bloc todo> 
 +**Mission :** Améliorer l'endpoint recommendations 
 + 
 +<sxh kotlin> 
 +// GET /users/{id}/recommendations 
 +</sxh> 
 + 
 +**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 :** 
 +<sxh kotlin> 
 +@Entity 
 +@Table(name = "users"
 +class User( 
 +    @Column(nullable = false) 
 +    var name: String, 
 + 
 +    @Column(nullable = false, unique = true) 
 +    var email: String, 
 + 
 +    // ✅ Préférences stockées en JSON 
 +    @Type(JsonType::class) 
 +    @Column(columnDefinition = "json"
 +    var preferences: UserPreferences = UserPreferences() 
 +) { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null 
 +
 + 
 +data class UserPreferences( 
 +    val priceRange: PriceRange = PriceRange(), 
 +    val brands: List<String> = emptyList(), 
 +    val excludeCategories: List<UUID> = emptyList() 
 +
 + 
 +data class PriceRange( 
 +    val min: BigDecimal = BigDecimal.ZERO, 
 +    val max: BigDecimal = BigDecimal("10000"
 +
 +</sxh> 
 +</WRAP> 
 + 
 +===== Configuration complète ===== 
 + 
 +<sxh bash> 
 +# 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
 </sxh> </sxh>
  
Ligne 135: Ligne 826:
   * ✅ Résolution problème N+1 sur au moins 2 endpoints   * ✅ Résolution problème N+1 sur au moins 2 endpoints
   * ✅ Implémentation héritage produits (1 stratégie au choix)   * ✅ 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   * ✅ Tests d'intégration validant les performances
  
 **Nice to have :** **Nice to have :**
   * Comparaison des 3 stratégies d'héritage   * Comparaison des 3 stratégies d'héritage
-  * DTO Projections avec MapStruct +  * **Type JSON pour attributs dynamiques produits** 
-  * Benchmark avant/après optimisations+  * **Tsid sur une nouvelle entité (Review, Wishlist...)** 
 +  * Benchmark avant/après optimisations avec query count assertions
   * Documentation des choix architecturaux   * Documentation des choix architecturaux
 </WRAP> </WRAP>
- 
-===== Critères d'évaluation ===== 
- 
-^ Critère ^ Points ^ 
-| Associations correctement mappées | 25% | 
-| Résolution problèmes N+1 | 30% | 
-| Implémentation héritage | 25% | 
-| Tests et qualité code | 20% | 
- 
-===== Configuration supplémentaire ===== 
- 
-<sxh yaml;gutter:false> 
-# application.yml - pour la séance 
-spring: 
-  jpa: 
-    show-sql: true 
-    properties: 
-      hibernate: 
-        format_sql: true 
-        use_sql_comments: true 
-        generate_statistics: true # Pour mesurer les perfs 
-logging: 
-  level: 
-    org.hibernate.stat: DEBUG # Statistiques Hibernate 
-</sxh> 
  
 ===== Ressources ===== ===== Ressources =====
  
   * [[https://vladmihalcea.com/tutorials/hibernate/|Hibernate Performance Best Practices]]   * [[https://vladmihalcea.com/tutorials/hibernate/|Hibernate Performance Best Practices]]
 +  * [[https://github.com/vladmihalcea/hypersistence-utils|Hypersistence Utils GitHub]]
 +  * [[https://hypersistence.io/|Documentation officielle Hypersistence]]
   * [[https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods|Spring Data JPA Query Methods]]   * [[https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods|Spring Data JPA Query Methods]]
 +  * [[https://kotlinlang.org/docs/jpa.html|Kotlin JPA Plugin]]
  
 <WRAP round bloc info> <WRAP round bloc info>
Ligne 180: Ligne 851:
   * L'héritage n'est pas toujours la meilleure solution (composition > héritage)   * L'héritage n'est pas toujours la meilleure solution (composition > héritage)
   * Privilégier ''@ManyToOne'' LAZY par défaut   * Privilégier ''@ManyToOne'' LAZY par défaut
 +  * Utiliser des data classes pour les DTOs
 +  * Attention aux classes ouvertes (''open'') nécessaires pour JPA en Kotlin
 </WRAP> </WRAP>
- 
  • eadl/bloc3/dev_av/td2.1759872946.txt.gz
  • Dernière modification : il y a 2 mois
  • de jcheron