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:49] – [Configuration complète] 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
 +)
  
-// Option 3 : TABLE_PER_CLASS+@Entity 
 +@Table(name = "physical_products"
 +class PhysicalProduct( 
 +    // ... champs spécifiques 
 +) : Product(...) 
 + 
 +// 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> </sxh>
  
Ligne 140: Ligne 460:
 === Dépendance Maven === === Dépendance Maven ===
  
-<sxh xml;gutter:false>+<sxh xml>
 <dependency> <dependency>
     <groupId>io.hypersistence</groupId>     <groupId>io.hypersistence</groupId>
Ligne 156: Ligne 476:
 === Configuration === === Configuration ===
  
-<sxh properties;gutter:false> +<sxh kotlin>
-# 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 +
-</sxh> +
- +
-=== Utilisation du QueryStackTraceLogger === +
- +
-<sxh java;gutter:false> +
-// Configuration globale (classe @Configuration)+
 @Configuration @Configuration
-public class HypersistenceConfiguration {+class HypersistenceConfiguration {
          
     @Bean     @Bean
-    public QueryStackTraceLogger queryStackTraceLogger() +    fun queryStackTraceLogger() QueryStackTraceLogger()
-        return new QueryStackTraceLogger()+
-    }+
          
     @EventListener     @EventListener
-    public void onApplicationEvent(ApplicationReadyEvent event) {+    fun onApplicationReady(event: ApplicationReadyEvent) {
         // Active la détection des problèmes N+1         // Active la détection des problèmes N+1
-        QueryStackTraceLogger.INSTANCE.setThreshold(10); // Alerte si > 10 requêtes+        QueryStackTraceLogger.INSTANCE.threshold = 10 // Alerte si > 10 requêtes
     }     }
 } }
Ligne 199: Ligne 504:
 === Exemple : Attributs dynamiques produit === === Exemple : Attributs dynamiques produit ===
  
-<sxh java;gutter:false>+<sxh kotlin>
 @Entity @Entity
 @Table(name = "products") @Table(name = "products")
-public class Product { +class Product( 
-    // ... attributs existants +    @Column(nullable = false) 
-     +    var name: String, 
-    @Type(JsonType.class)+ 
 +    @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")     @Column(columnDefinition = "json")
-    private Map<String, Objectattributes; +    var attributes: Map<String, Any= emptyMap() 
-     +) { 
-    // Pour PhysicalProduct : {"weight": 2.5, "dimensions": "30x20x10"} +    @Id 
-    // Pour DigitalProduct : {"fileSize": "1.2GB", "format""PDF"}+    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var idUUID? = 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> </sxh>
  
 === Données exemple === === Données exemple ===
  
-<sxh json;gutter:false>+<sxh json>
 { {
   "id": "550e8400-e29b-41d4-a716-446655440020",   "id": "550e8400-e29b-41d4-a716-446655440020",
Ligne 226: Ligne 558:
     "color": "Titanium Blue",     "color": "Titanium Blue",
     "storage": "256GB",     "storage": "256GB",
-    "warranty": "2 years"+    "warranty": "2 years"
 +    "features": ["5G", "Face ID", "A17 Pro"]
   }   }
 } }
Ligne 248: Ligne 581:
 === Comparaison UUID vs Tsid === === Comparaison UUID vs Tsid ===
  
-<sxh java;gutter:false>+<sxh kotlin>
 // Avant (UUID) // Avant (UUID)
-@Id +@Entity 
-@GeneratedValue(strategy = GenerationType.UUID) +class Review( 
-private UUID id;+    @Id 
 +    @GeneratedValue(strategy = GenerationType.UUID) 
 +    var id: UUID? = null, 
 +    // ... 
 +)
  
 // Après (Tsid) - Pour nouvelles entités // Après (Tsid) - Pour nouvelles entités
-@Id +@Entity 
-@TsidGenerator +@Table(name = "reviews"
-private Long id;+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> </sxh>
  
Ligne 263: Ligne 640:
   * Créer une nouvelle entité ''Review'' avec Tsid   * Créer une nouvelle entité ''Review'' avec Tsid
   * Comparer les performances d'insertion (benchmark)   * 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 ==== ==== 4.5 Monitoring des requêtes en temps réel ====
Ligne 268: Ligne 691:
 === DataSourceProxyBeanPostProcessor === === DataSourceProxyBeanPostProcessor ===
  
-<sxh java;gutter:false>+<sxh kotlin>
 @Configuration @Configuration
-public class DataSourceProxyConfiguration {+class DataSourceProxyConfiguration {
          
     @Bean     @Bean
-    public DataSourceProxyBeanPostProcessor dataSourceProxyBeanPostProcessor() +    fun dataSourceProxyBeanPostProcessor() = object : DataSourceProxyBeanPostProcessor() { 
-        return new DataSourceProxyBeanPostProcessor() { +        override fun createDataSourceProxy(dataSource: DataSource): DataSourceProxy 
-            @Override +            return DataSourceProxy(dataSource, QueryCountHolder()) 
-            protected DataSourceProxy createDataSourceProxy(DataSource dataSource) { +        }
-                return new DataSourceProxy(dataSource, new QueryCountHolder())+
-            } +
-        };+
     }     }
 } }
Ligne 288: Ligne 708:
   * Créer un test d'intégration qui vérifie le nombre exact de requêtes   * Créer un test d'intégration qui vérifie le nombre exact de requêtes
   * Exemple : ''assertQueryCount(3)'' après un appel API   * 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 ==== ==== 4.6 Exercice intégratif ====
Ligne 294: Ligne 741:
 **Mission :** Améliorer l'endpoint recommendations **Mission :** Améliorer l'endpoint recommendations
  
-<sxh;gutter:false+<sxh kotlin
-GET /users/{id}/recommendations+// GET /users/{id}/recommendations
 </sxh> </sxh>
  
Ligne 305: Ligne 752:
  
 **Structure JSON recommandée :** **Structure JSON recommandée :**
-<sxh json;gutter:false+<sxh kotlin
-// User.preferences (JSON+@Entity 
-{ +@Table(name = "users"
-  "priceRange": {"min"50"max": 500}+class User( 
-  "brands"["Apple", "Samsung"]+    @Column(nullable = false) 
-  "excludeCategories": ["550e8400-..."]+    var nameString, 
 + 
 +    @Column(nullable = falseunique = true) 
 +    var emailString
 + 
 +    // ✅ Préférences stockées en JSON 
 +    @Type(JsonType::class) 
 +    @Column(columnDefinition = "json"
 +    var preferencesUserPreferences = 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> </sxh>
 </WRAP> </WRAP>
Ligne 317: Ligne 787:
 ===== Configuration complète ===== ===== Configuration complète =====
  
-<sxh bash;gutter:false>+<sxh bash>
 # application.properties - Configuration complète Séance 2 # application.properties - Configuration complète Séance 2
  
Ligne 347: Ligne 817:
 </sxh> </sxh>
  
-===== Livrables attendus (MAJ) =====+===== Livrables attendus =====
  
 <WRAP round bloc todo> <WRAP round bloc todo>
Ligne 373: Ligne 843:
   * [[https://hypersistence.io/|Documentation officielle Hypersistence]]   * [[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 381: 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.1759873796.txt.gz
  • Dernière modification : il y a 2 mois
  • de jcheron