| 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 |
|---|
| ==== 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> |
| |
| * 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) ===== |
| <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> |
| - 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) ===== |
| **À 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> |
| |
| ==== 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> |
| |
| === Dépendance Maven === | === Dépendance Maven === |
| |
| <sxh xml;gutter:false> | <sxh xml> |
| <dependency> | <dependency> |
| <groupId>io.hypersistence</groupId> | <groupId>io.hypersistence</groupId> |
| === 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 |
| } | } |
| } | } |
| === 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, Object> attributes; | 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 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> | </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", |
| "color": "Titanium Blue", | "color": "Titanium Blue", |
| "storage": "256GB", | "storage": "256GB", |
| "warranty": "2 years" | "warranty": "2 years", |
| | "features": ["5G", "Face ID", "A17 Pro"] |
| } | } |
| } | } |
| === 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> |
| |
| * 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 ==== |
| === 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()); | |
| } | |
| }; | |
| } | } |
| } | } |
| * 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 ==== |
| **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> |
| |
| |
| **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 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> | </sxh> |
| </WRAP> | </WRAP> |
| ===== Configuration complète ===== | ===== Configuration complète ===== |
| |
| <sxh properties;gutter:false> | <sxh bash> |
| # application.properties - Configuration complète Séance 2 | # application.properties - Configuration complète Séance 2 |
| |
| </sxh> | </sxh> |
| |
| ===== Livrables attendus (MAJ) ===== | ===== Livrables attendus ===== |
| |
| <WRAP round bloc todo> | <WRAP round bloc todo> |
| * [[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> |
| * 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> |
| |