Différences
Ci-dessous, les différences entre deux révisions de la page.
| 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] – jcheron | eadl: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 ==== | ||
| - | < | + | < |
| // 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 = " | ||
| + | class Order( | ||
| + | @ManyToOne(fetch = FetchType.LAZY) | ||
| + | @JoinColumn(name = " | ||
| + | val user: User, | ||
| + | |||
| + | @Enumerated(EnumType.STRING) | ||
| + | @Column(nullable = false) | ||
| + | var status: OrderStatus = OrderStatus.PENDING, | ||
| + | |||
| + | @Column(nullable = false) | ||
| + | var totalAmount: | ||
| + | |||
| + | @Column(nullable = false) | ||
| + | val createdAt: Instant = Instant.now() | ||
| + | ) { | ||
| + | @Id | ||
| + | @GeneratedValue(strategy = GenerationType.UUID) | ||
| + | var id: UUID? = null | ||
| + | |||
| + | @OneToMany( | ||
| + | mappedBy = " | ||
| + | cascade = [CascadeType.ALL], | ||
| + | orphanRemoval = true, | ||
| + | fetch = FetchType.LAZY | ||
| + | ) | ||
| + | @JsonManagedReference | ||
| + | private val _items: MutableList< | ||
| + | |||
| + | val items: List< | ||
| + | get() = _items.toList() | ||
| + | |||
| + | fun addItem(item: | ||
| + | require(_items.isEmpty() || _items.size < 100) { | ||
| + | " | ||
| + | } | ||
| + | _items.add(item) | ||
| + | item.order = this | ||
| + | recalculateTotal() | ||
| + | } | ||
| + | |||
| + | fun removeItem(item: | ||
| + | _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 = " | ||
| + | class OrderItem( | ||
| + | @ManyToOne(fetch = FetchType.LAZY) | ||
| + | @JoinColumn(name = " | ||
| + | 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 = " | ||
| + | @JsonBackReference | ||
| + | var order: Order? = null | ||
| + | |||
| + | init { | ||
| + | require(quantity > 0) { " | ||
| + | require(unitPrice > BigDecimal.ZERO) { "Unit price must be positive" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | enum class OrderStatus { | ||
| + | PENDING, | ||
| + | CONFIRMED, | ||
| + | SHIPPED, | ||
| + | DELIVERED, | ||
| + | CANCELLED | ||
| + | } | ||
| </ | </ | ||
| 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: | ||
| + | |||
| + | @Autowired | ||
| + | private lateinit var userRepository: | ||
| + | |||
| + | @Autowired | ||
| + | private lateinit var productRepository: | ||
| + | |||
| + | @Test | ||
| + | fun `should create order with items and calculate total`() { | ||
| + | // Given | ||
| + | val user = userRepository.save(User(" | ||
| + | val product1 = productRepository.save( | ||
| + | Product(" | ||
| + | ) | ||
| + | val product2 = productRepository.save( | ||
| + | Product(" | ||
| + | ) | ||
| + | |||
| + | val dto = CreateOrderDto( | ||
| + | userId = user.id!!, | ||
| + | items = listOf( | ||
| + | OrderItemDto(product1.id!!, | ||
| + | OrderItemDto(product2.id!!, | ||
| + | ) | ||
| + | ) | ||
| + | |||
| + | // When | ||
| + | val order = orderService.createOrder(dto) | ||
| + | |||
| + | // Then | ||
| + | assertThat(order.items).hasSize(2) | ||
| + | assertThat(order.totalAmount).isEqualByComparingTo(" | ||
| + | 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(" | ||
| + | val product = productRepository.save( | ||
| + | Product(" | ||
| + | ) | ||
| + | |||
| + | val dto = CreateOrderDto( | ||
| + | userId = user.id!!, | ||
| + | items = listOf(OrderItemDto(product.id!!, | ||
| + | ) | ||
| + | |||
| + | // When & Then | ||
| + | assertThatThrownBy { orderService.createOrder(dto) } | ||
| + | .isInstanceOf(InsufficientStockException:: | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Chargement minimaliste ==== | ||
| + | Pour recréer à moindre coût une relation (sans charger complètement l' | ||
| + | |||
| + | <sxh kotlin> | ||
| + | val user = entityManager.getReference(User:: | ||
| + | </ | ||
| ===== 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; | + | < |
| - | GET / | + | // GET / |
| // Retourne les commandes avec leurs items et produits | // Retourne les commandes avec leurs items et produits | ||
| </ | </ | ||
| Ligne 77: | Ligne 240: | ||
| - Créer une projection pour ''/ | - Créer une projection pour ''/ | ||
| - Comparer les performances avant/ | - Comparer les performances avant/ | ||
| + | |||
| + | <sxh kotlin> | ||
| + | // ❌ PROBLÈME N+1 : Sans optimisation | ||
| + | interface OrderRepository : JpaRepository< | ||
| + | fun findByUserId(userId: | ||
| + | // 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< | ||
| + | @Query(""" | ||
| + | SELECT DISTINCT o FROM Order o | ||
| + | JOIN FETCH o.items i | ||
| + | JOIN FETCH i.product | ||
| + | WHERE o.user.id = :userId | ||
| + | """ | ||
| + | fun findByUserIdWithItems(userId: | ||
| + | } | ||
| + | |||
| + | // ✅ SOLUTION 2 : @EntityGraph | ||
| + | interface OrderRepository : JpaRepository< | ||
| + | @EntityGraph(attributePaths = [" | ||
| + | fun findByUserId(userId: | ||
| + | } | ||
| + | |||
| + | // ✅ SOLUTION 3 : DTO Projection | ||
| + | data class OrderSummaryDto( | ||
| + | val id: UUID, | ||
| + | val totalAmount: | ||
| + | val status: OrderStatus, | ||
| + | val itemCount: Long, | ||
| + | val createdAt: Instant | ||
| + | ) | ||
| + | |||
| + | interface OrderRepository : JpaRepository< | ||
| + | @Query(""" | ||
| + | SELECT new com.ecommerce.order.dto.OrderSummaryDto( | ||
| + | o.id, o.totalAmount, | ||
| + | ) | ||
| + | FROM Order o | ||
| + | LEFT JOIN o.items i | ||
| + | WHERE o.user.id = :userId | ||
| + | GROUP BY o.id, o.totalAmount, | ||
| + | """ | ||
| + | fun findOrderSummariesByUserId(userId: | ||
| + | } | ||
| + | </ | ||
| ===== 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) :** | ||
| - | < | + | < |
| // Option 1 : SINGLE_TABLE (par défaut) | // Option 1 : SINGLE_TABLE (par défaut) | ||
| + | @Entity | ||
| + | @Table(name = " | ||
| @Inheritance(strategy = InheritanceType.SINGLE_TABLE) | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) | ||
| - | @DiscriminatorColumn(name = " | + | @DiscriminatorColumn(name = " |
| + | 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 = " | ||
| + | open var category: Category | ||
| + | ) { | ||
| + | @Id | ||
| + | @GeneratedValue(strategy = GenerationType.UUID) | ||
| + | open var id: UUID? = null | ||
| + | } | ||
| + | |||
| + | @Entity | ||
| + | @DiscriminatorValue(" | ||
| + | class PhysicalProduct( | ||
| + | name: String, | ||
| + | price: BigDecimal, | ||
| + | stock: Int, | ||
| + | category: Category, | ||
| + | |||
| + | @Column(name = " | ||
| + | var weight: Double, | ||
| + | |||
| + | @Column(name = " | ||
| + | var dimensions: String, // " | ||
| + | |||
| + | @Column(name = " | ||
| + | var shippingCost: | ||
| + | ) : Product(name, | ||
| + | |||
| + | @Entity | ||
| + | @DiscriminatorValue(" | ||
| + | class DigitalProduct( | ||
| + | name: String, | ||
| + | price: BigDecimal, | ||
| + | stock: Int, | ||
| + | category: Category, | ||
| + | |||
| + | @Column(name = " | ||
| + | var fileSize: Double, | ||
| + | |||
| + | @Column(name = " | ||
| + | var downloadUrl: | ||
| + | |||
| + | @Column(name = " | ||
| + | var format: String // PDF, MP4, ZIP... | ||
| + | ) : Product(name, | ||
| + | |||
| + | @Entity | ||
| + | @DiscriminatorValue(" | ||
| + | class ServiceProduct( | ||
| + | name: String, | ||
| + | price: BigDecimal, | ||
| + | stock: Int, | ||
| + | category: Category, | ||
| + | |||
| + | @Column(name = " | ||
| + | var duration: Int, | ||
| + | |||
| + | @Column(name = " | ||
| + | var serviceDate: | ||
| + | ) : Product(name, | ||
| + | |||
| + | // Option 2 : JOINED | ||
| + | @Entity | ||
| + | @Table(name = " | ||
| @Inheritance(strategy = InheritanceType.JOINED) | @Inheritance(strategy = InheritanceType.JOINED) | ||
| + | abstract class Product( | ||
| + | // ... mêmes champs | ||
| + | ) | ||
| - | // Option 3 : TABLE_PER_CLASS | + | @Entity |
| + | @Table(name = " | ||
| + | class PhysicalProduct( | ||
| + | // ... champs spécifiques | ||
| + | ) : Product(...) | ||
| + | |||
| + | // Option 3 : TABLE_PER_CLASS | ||
| + | @Entity | ||
| @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) | @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) | ||
| + | abstract class Product( | ||
| + | // ... mêmes champs | ||
| + | ) | ||
| </ | </ | ||
| Ligne 116: | Ligne 414: | ||
| ==== 3.3 Requêtes polymorphiques ==== | ==== 3.3 Requêtes polymorphiques ==== | ||
| - | < | + | < |
| // Repository | // Repository | ||
| - | List< | + | interface ProductRepository : JpaRepository<Product, UUID> { |
| - | List<PhysicalProduct> findPhysicalProducts(); | + | |
| + | | ||
| + | |||
| + | // Seulement les produits physiques | ||
| + | @Query(" | ||
| + | fun findPhysicalProducts(): List< | ||
| + | |||
| + | // Filtrage par type | ||
| + | @Query(" | ||
| + | fun findByType(type: | ||
| + | } | ||
| - | // Nouveaux endpoints | + | // Controller |
| - | GET / | + | @RestController |
| - | GET / | + | @RequestMapping(" |
| + | class ProductController(private val repository: ProductRepository) { | ||
| + | |||
| + | @GetMapping | ||
| + | fun getProducts(@RequestParam(required = false) type: String?): List< | ||
| + | return when (type? | ||
| + | "PHYSICAL" -> repository.findByType(PhysicalProduct:: | ||
| + | " | ||
| + | " | ||
| + | else -> repository.findAll() | ||
| + | } | ||
| + | } | ||
| + | } | ||
| </ | </ | ||
| Ligne 140: | Ligne 460: | ||
| === Dépendance Maven === | === Dépendance Maven === | ||
| - | <sxh xml; | + | <sxh xml> |
| < | < | ||
| < | < | ||
| Ligne 156: | Ligne 476: | ||
| === Configuration === | === Configuration === | ||
| - | < | + | < |
| - | # application.properties - Ajout pour Hypersistence | + | |
| - | + | ||
| - | # Détection des problèmes N+1 | + | |
| - | logging.level.io.hypersistence.utils=DEBUG | + | |
| - | + | ||
| - | # Limites d' | + | |
| - | hypersistence.query.fail.on.pagination.over.collection.fetch=false | + | |
| - | </ | + | |
| - | + | ||
| - | === Utilisation du QueryStackTraceLogger === | + | |
| - | + | ||
| - | <sxh java; | + | |
| - | // Configuration globale (classe @Configuration) | + | |
| @Configuration | @Configuration | ||
| - | public | + | class HypersistenceConfiguration { |
| | | ||
| @Bean | @Bean | ||
| - | | + | |
| - | return new QueryStackTraceLogger(); | + | |
| - | } | + | |
| | | ||
| @EventListener | @EventListener | ||
| - | | + | |
| // 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 === | ||
| - | < | + | < |
| @Entity | @Entity | ||
| @Table(name = " | @Table(name = " | ||
| - | public | + | class Product( |
| - | | + | |
| - | + | 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) | ||
| + | | ||
| + | var category: Category, | ||
| + | |||
| + | // ✅ Stockage JSON pour attributs dynamiques | ||
| + | @Type(JsonType::class) | ||
| @Column(columnDefinition = " | @Column(columnDefinition = " | ||
| - | | + | |
| - | + | ) { | |
| - | | + | |
| - | | + | |
| + | | ||
| } | } | ||
| + | |||
| + | // Utilisation | ||
| + | val product = Product( | ||
| + | name = " | ||
| + | price = BigDecimal(" | ||
| + | stock = 25, | ||
| + | category = electronicsCategory, | ||
| + | attributes = mapOf( | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ) | ||
| + | ) | ||
| </ | </ | ||
| === Données exemple === | === Données exemple === | ||
| - | <sxh json; | + | <sxh json> |
| { | { | ||
| " | " | ||
| Ligne 226: | Ligne 558: | ||
| " | " | ||
| " | " | ||
| - | " | + | " |
| + | " | ||
| } | } | ||
| } | } | ||
| Ligne 248: | Ligne 581: | ||
| === Comparaison UUID vs Tsid === | === Comparaison UUID vs Tsid === | ||
| - | < | + | < |
| // Avant (UUID) | // Avant (UUID) | ||
| - | @Id | + | @Entity |
| - | @GeneratedValue(strategy = GenerationType.UUID) | + | class Review( |
| - | private UUID id; | + | |
| + | @GeneratedValue(strategy = GenerationType.UUID) | ||
| + | | ||
| + | // ... | ||
| + | ) | ||
| // Après (Tsid) - Pour nouvelles entités | // Après (Tsid) - Pour nouvelles entités | ||
| - | @Id | + | @Entity |
| - | @TsidGenerator | + | @Table(name = " |
| - | private Long id; | + | class Review( |
| + | @ManyToOne(fetch = FetchType.LAZY) | ||
| + | @JoinColumn(name = " | ||
| + | val product: Product, | ||
| + | |||
| + | @ManyToOne(fetch = FetchType.LAZY) | ||
| + | @JoinColumn(name = " | ||
| + | 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: | ||
| + | |||
| + | @Column(nullable = false) | ||
| + | val createdAt: Instant = Instant.now(), | ||
| + | |||
| + | @Column(nullable = false) | ||
| + | var updatedAt: Instant = Instant.now() | ||
| + | ) { | ||
| + | | ||
| + | @TsidGenerator | ||
| + | | ||
| + | |||
| + | init { | ||
| + | require(rating in 1..5) { " | ||
| + | require(title.isNotBlank()) { "Title cannot be blank" } | ||
| + | require(comment.isNotBlank()) { " | ||
| + | require(helpfulCount >= 0) { " | ||
| + | } | ||
| + | } | ||
| </ | </ | ||
| Ligne 263: | Ligne 640: | ||
| * Créer une nouvelle entité '' | * Créer une nouvelle entité '' | ||
| * Comparer les performances d' | * Comparer les performances d' | ||
| + | |||
| + | < | ||
| + | <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 " | ||
| + | User " | ||
| + | |||
| + | 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 | ||
| + | </ | ||
| + | < | ||
| ==== 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 === | ||
| - | < | + | < |
| @Configuration | @Configuration | ||
| - | public | + | class DataSourceProxyConfiguration { |
| | | ||
| @Bean | @Bean | ||
| - | | + | |
| - | return new DataSourceProxyBeanPostProcessor() { | + | |
| - | | + | return DataSourceProxy(dataSource, |
| - | protected DataSourceProxy | + | } |
| - | return | + | |
| - | } | + | |
| - | }; | + | |
| } | } | ||
| } | } | ||
| Ligne 288: | Ligne 708: | ||
| * Créer un test d' | * Créer un test d' | ||
| * Exemple : '' | * Exemple : '' | ||
| + | |||
| + | <sxh kotlin> | ||
| + | @SpringBootTest | ||
| + | @AutoConfigureMockMvc | ||
| + | @ActiveProfiles(" | ||
| + | @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("/ | ||
| + | .andExpect(status().isOk) | ||
| + | |||
| + | // Then - Vérifier le nombre de requêtes SQL | ||
| + | assertSelectCount(2) // 1 pour User + 1 pour Orders avec items (JOIN FETCH) | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| ==== 4.6 Exercice intégratif ==== | ==== 4.6 Exercice intégratif ==== | ||
| Ligne 294: | Ligne 741: | ||
| **Mission :** Améliorer l' | **Mission :** Améliorer l' | ||
| - | <sxh; | + | < |
| - | GET / | + | // GET / |
| </ | </ | ||
| Ligne 305: | Ligne 752: | ||
| **Structure JSON recommandée :** | **Structure JSON recommandée :** | ||
| - | < | + | < |
| - | // User.preferences | + | @Entity |
| - | { | + | @Table(name = " |
| - | " | + | class User( |
| - | " | + | |
| - | "excludeCategories": | + | var name: String, |
| + | |||
| + | @Column(nullable = false, unique = true) | ||
| + | var email: String, | ||
| + | |||
| + | // ✅ Préférences stockées en JSON | ||
| + | @Type(JsonType:: | ||
| + | @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< | ||
| + | val excludeCategories: | ||
| + | ) | ||
| + | |||
| + | data class PriceRange( | ||
| + | val min: BigDecimal = BigDecimal.ZERO, | ||
| + | val max: BigDecimal = BigDecimal(" | ||
| + | ) | ||
| </ | </ | ||
| </ | </ | ||
| Ligne 317: | Ligne 787: | ||
| ===== Configuration complète ===== | ===== Configuration complète ===== | ||
| - | < | + | < |
| # application.properties - Configuration complète Séance 2 | # application.properties - Configuration complète Séance 2 | ||
| Ligne 347: | Ligne 817: | ||
| </ | </ | ||
| - | ===== Livrables attendus | + | ===== Livrables attendus ===== |
| <WRAP round bloc todo> | <WRAP round bloc todo> | ||
| Ligne 373: | Ligne 843: | ||
| * [[https:// | * [[https:// | ||
| * [[https:// | * [[https:// | ||
| + | * [[https:// | ||
| <WRAP round bloc info> | <WRAP round bloc info> | ||
| Ligne 381: | Ligne 851: | ||
| * L' | * L' | ||
| * Privilégier '' | * Privilégier '' | ||
| + | * Utiliser des data classes pour les DTOs | ||
| + | * Attention aux classes ouvertes ('' | ||
| </ | </ | ||
| - | |||