Aller au contenu principal

TP6.1 - Conventions de Nommage Avancées

Objectifs​

À la fin de ce TP, vous serez capable de:

  1. ✅ Utiliser les 15+ mots-clés de Spring Data JPA
  2. ✅ Créer des requêtes complexes sans SQL
  3. ✅ Combiner plusieurs mots-clés
  4. ✅ Tester toutes vos méthodes

Durée estimée: 2h


Prérequis​

  • ✅ Chapitre 1 du Module 6 complété
  • ✅ Maven 3.9+ et Java 17+
  • ✅ VS Code avec extensions Java

Préparation: Créer le Projet​

Étape 1: Spring Initializr​

Allez sur https://start.spring.io/

Configuration:

  • Project: Maven
  • Spring Boot: 3.2.0
  • Group: com.example
  • Artifact: jpa-conventions
  • Packaging: Jar
  • Java: 17

Dépendances:

  • Spring Web
  • Spring Data JPA
  • H2 Database

Téléchargez, dézippez, ouvrez dans VS Code


Étape 2: Configuration​

Fichier: src/main/resources/application.properties

# H2 Database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Server
server.port=8080

Exercice 1: Créer l'Entité Product​

Étape 1.1: Créer l'Entité​

Fichier: model/Product.java

package com.example.jpa.model;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "products")
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String description;
private Double price;
private Integer stock;
private String category;
private Boolean active;
private Double discount;
private LocalDate createdAt;

// Constructeurs
public Product() {}

public Product(String name, Double price, Integer stock, String category, Boolean active) {
this.name = name;
this.price = price;
this.stock = stock;
this.category = category;
this.active = active;
this.createdAt = LocalDate.now();
}

// Getters et Setters (tous!)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }

public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }

public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }

public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }

public Double getDiscount() { return discount; }
public void setDiscount(Double discount) { this.discount = discount; }

public LocalDate getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDate createdAt) { this.createdAt = createdAt; }
}

Exercice 2: Créer le Repository avec 20 Méthodes​

Étape 2.1: Repository de Base​

Fichier: repository/ProductRepository.java

package com.example.jpa.repository;

import com.example.jpa.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

// NIVEAU FACILE (5 méthodes)

// 1. Trouver par nom exact
List<Product> findByName(String name);

// 2. Trouver par catégorie
List<Product> findByCategory(String category);

// 3. Trouver les produits actifs
List<Product> findByActiveTrue();

// 4. Trouver les produits inactifs
List<Product> findByActiveFalse();

// 5. Trouver par prix exact
List<Product> findByPrice(Double price);


// NIVEAU MOYEN (10 méthodes)

// 6. Prix inférieur à
List<Product> findByPriceLessThan(Double maxPrice);

// 7. Prix supérieur à
List<Product> findByPriceGreaterThan(Double minPrice);

// 8. Prix dans un intervalle
List<Product> findByPriceBetween(Double min, Double max);

// 9. Stock inférieur à (rupture)
List<Product> findByStockLessThan(Integer minStock);

// 10. Nom contenant un mot-clé
List<Product> findByNameContaining(String keyword);

// 11. Nom commençant par
List<Product> findByNameStartingWith(String prefix);

// 12. Catégorie dans une liste
List<Product> findByCategoryIn(List<String> categories);

// 13. Produits sans réduction
List<Product> findByDiscountIsNull();

// 14. Produits avec réduction
List<Product> findByDiscountIsNotNull();

// 15. Nom insensible à la casse
List<Product> findByNameIgnoreCase(String name);


// NIVEAU AVANCÉ (5 méthodes)

// 16. Combinaison: Actif ET prix < max
List<Product> findByActiveTrueAndPriceLessThan(Double maxPrice);

// 17. Combinaison: Catégorie ET stock > min, triés par prix
List<Product> findByCategoryAndStockGreaterThanOrderByPriceAsc(
String category,
Integer minStock
);

// 18. Combinaison: Actif ET prix dans intervalle ET catégorie, triés
List<Product> findByActiveTrueAndPriceBetweenAndCategoryOrderByNameAsc(
Double min,
Double max,
String category
);

// 19. Combinaison: Nom contenant OU catégorie égale
List<Product> findByNameContainingOrCategory(String keyword, String category);

// 20. Créés après une date, triés par date
List<Product> findByCreatedAtAfterOrderByCreatedAtDesc(LocalDate date);
}

Exercice 3: Initialiser les Données de Test​

Étape 3.1: Créer DataLoader​

Fichier: config/DataLoader.java

package com.example.jpa.config;

import com.example.jpa.model.Product;
import com.example.jpa.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDate;

@Configuration
public class DataLoader {

@Bean
CommandLineRunner initDatabase(ProductRepository repository) {
return args -> {
System.out.println("=== Initialisation des données ===");

// Electronics
repository.save(new Product("MacBook Pro", 2499.99, 15, "Electronics", true));
repository.save(new Product("iPhone 15", 999.99, 50, "Electronics", true));
repository.save(new Product("iPad Pro", 1299.99, 30, "Electronics", true));

// Computers
repository.save(new Product("Dell Laptop", 899.99, 25, "Computers", true));
repository.save(new Product("HP Desktop", 799.99, 10, "Computers", true));

// Phones
repository.save(new Product("Samsung Phone", 699.99, 40, "Phones", true));
repository.save(new Product("Google Pixel", 599.99, 20, "Phones", true));

// Produits avec réduction
Product macMini = new Product("Mac Mini", 699.99, 5, "Computers", true);
macMini.setDiscount(50.0);
repository.save(macMini);

// Produits inactifs
repository.save(new Product("Old Phone", 199.99, 0, "Phones", false));

// Produits en rupture
repository.save(new Product("Rare Item", 1499.99, 2, "Electronics", true));

System.out.println("=== " + repository.count() + " produits créés ===");
};
}
}

Exercice 4: Créer le Controller de Test​

Étape 4.1: Controller Complet​

Fichier: controller/ProductController.java

package com.example.jpa.controller;

import com.example.jpa.model.Product;
import com.example.jpa.repository.ProductRepository;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductRepository productRepo;

public ProductController(ProductRepository productRepo) {
this.productRepo = productRepo;
}

// Test 1: findByName
@GetMapping("/by-name/{name}")
public List<Product> byName(@PathVariable String name) {
return productRepo.findByName(name);
}

// Test 2: findByCategory
@GetMapping("/by-category/{category}")
public List<Product> byCategory(@PathVariable String category) {
return productRepo.findByCategory(category);
}

// Test 3: findByActiveTrue
@GetMapping("/active")
public List<Product> activeProducts() {
return productRepo.findByActiveTrue();
}

// Test 4: findByActiveFalse
@GetMapping("/inactive")
public List<Product> inactiveProducts() {
return productRepo.findByActiveFalse();
}

// Test 5: findByPriceLessThan
@GetMapping("/cheap/{maxPrice}")
public List<Product> cheapProducts(@PathVariable Double maxPrice) {
return productRepo.findByPriceLessThan(maxPrice);
}

// Test 6: findByPriceGreaterThan
@GetMapping("/expensive/{minPrice}")
public List<Product> expensiveProducts(@PathVariable Double minPrice) {
return productRepo.findByPriceGreaterThan(minPrice);
}

// Test 7: findByPriceBetween
@GetMapping("/price-range")
public List<Product> priceRange(
@RequestParam Double min,
@RequestParam Double max
) {
return productRepo.findByPriceBetween(min, max);
}

// Test 8: findByStockLessThan
@GetMapping("/low-stock/{minStock}")
public List<Product> lowStock(@PathVariable Integer minStock) {
return productRepo.findByStockLessThan(minStock);
}

// Test 9: findByNameContaining
@GetMapping("/search/{keyword}")
public List<Product> search(@PathVariable String keyword) {
return productRepo.findByNameContaining(keyword);
}

// Test 10: findByNameStartingWith
@GetMapping("/starts-with/{prefix}")
public List<Product> startsWith(@PathVariable String prefix) {
return productRepo.findByNameStartingWith(prefix);
}

// Test 11: findByCategoryIn
@GetMapping("/categories")
public List<Product> byCategories(@RequestParam List<String> categories) {
return productRepo.findByCategoryIn(categories);
}

// Test 12: findByDiscountIsNull
@GetMapping("/no-discount")
public List<Product> noDiscount() {
return productRepo.findByDiscountIsNull();
}

// Test 13: findByDiscountIsNotNull
@GetMapping("/with-discount")
public List<Product> withDiscount() {
return productRepo.findByDiscountIsNotNull();
}

// Test 14: findByActiveTrueAndPriceLessThan
@GetMapping("/active-cheap/{maxPrice}")
public List<Product> activeCheap(@PathVariable Double maxPrice) {
return productRepo.findByActiveTrueAndPriceLessThan(maxPrice);
}

// Test 15: Combinaison complexe
@GetMapping("/complex")
public List<Product> complex(
@RequestParam Double min,
@RequestParam Double max,
@RequestParam String category
) {
return productRepo.findByActiveTrueAndPriceBetweenAndCategoryOrderByNameAsc(
min, max, category
);
}
}

Exercice 5: Tests Manuels​

Démarrer l'Application​

mvn clean install -DskipTests spring-boot:run

Console devrait afficher:

=== Initialisation des données ===
=== 10 produits créés ===

Test 1: Produits par catégorie​

GET http://localhost:8080/api/products/by-category/Electronics

Résultat attendu: 3 produits (MacBook Pro, iPhone 15, iPad Pro, Rare Item)


Test 2: Produits actifs​

GET http://localhost:8080/api/products/active

Résultat attendu: 9 produits (tous sauf "Old Phone")


Test 3: Produits moins de 700€​

GET http://localhost:8080/api/products/cheap/700

Résultat attendu: Samsung Phone, Google Pixel, Mac Mini, Old Phone, HP Desktop


Test 4: Prix entre 600€ et 1000€​

GET http://localhost:8080/api/products/price-range?min=600&max=1000

Résultat attendu: Samsung Phone, Mac Mini, Dell Laptop, HP Desktop, iPhone 15


Test 5: Recherche par mot-clé​

GET http://localhost:8080/api/products/search/Phone

Résultat attendu: iPhone 15, Samsung Phone, Google Pixel, Old Phone


Test 6: Commence par "Mac"​

GET http://localhost:8080/api/products/starts-with/Mac

Résultat attendu: MacBook Pro, Mac Mini


Test 7: Plusieurs catégories​

GET http://localhost:8080/api/products/categories?categories=Electronics,Phones

Résultat attendu: Tous les produits Electronics + Phones


Test 8: Produits avec réduction​

GET http://localhost:8080/api/products/with-discount

Résultat attendu: Mac Mini (seul avec discount)


Test 9: Produits actifs moins de 1000€​

GET http://localhost:8080/api/products/active-cheap/1000

Résultat attendu: Tous les actifs < 1000€


Test 10: Recherche complexe​

GET http://localhost:8080/api/products/complex?min=500&max=1500&category=Electronics

Résultat attendu: iPhone 15, iPad Pro, Rare Item (triés par nom)


Exercice 6: Tests Unitaires (Bonus)​

Créer les Tests​

Fichier: src/test/java/ProductRepositoryTest.java

package com.example.jpa;

import com.example.jpa.model.Product;
import com.example.jpa.repository.ProductRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
public class ProductRepositoryTest {

@Autowired
private ProductRepository productRepo;

@BeforeEach
void setUp() {
productRepo.deleteAll();

productRepo.save(new Product("Laptop", 999.99, 10, "Computers", true));
productRepo.save(new Product("Phone", 599.99, 20, "Phones", true));
productRepo.save(new Product("Tablet", 399.99, 5, "Electronics", false));
}

@Test
void testFindByPriceLessThan() {
List<Product> results = productRepo.findByPriceLessThan(700.0);
assertEquals(2, results.size());
}

@Test
void testFindByActiveTrue() {
List<Product> results = productRepo.findByActiveTrue();
assertEquals(2, results.size());
}

@Test
void testFindByNameContaining() {
List<Product> results = productRepo.findByNameContaining("top");
assertEquals(1, results.size());
assertEquals("Laptop", results.get(0).getName());
}

@Test
void testFindByCategoryIn() {
List<String> categories = Arrays.asList("Computers", "Phones");
List<Product> results = productRepo.findByCategoryIn(categories);
assertEquals(2, results.size());
}

@Test
void testFindByPriceBetween() {
List<Product> results = productRepo.findByPriceBetween(400.0, 1000.0);
assertEquals(2, results.size());
}
}

Exécuter les tests:

mvn test

Challenge: Créez 10 Méthodes Supplémentaires​

À vous de jouer! Créez ces méthodes:

  1. Trouver par description contenant un mot-clé
  2. Trouver les produits créés avant une date
  3. Trouver par catégorie ET actif, triés par prix DESC
  4. Trouver les produits avec stock entre min et max
  5. Trouver par nom se terminant par un suffixe
  6. Trouver les produits avec prix >= min OU stock < min
  7. Trouver par catégorie pas dans une liste
  8. Trouver les produits actifs sans réduction
  9. Trouver par prix ET triés par stock DESC puis nom ASC
  10. Trouver les 5 produits les moins chers (hint: utilisez Pageable)

Récapitulatif​

Ce que Vous Avez Pratiqué​

✅ 20 méthodes avec les conventions de nommage
✅ 9 mots-clés différents (Between, LessThan, Containing, In, IsNull, True, False, IgnoreCase, OrderBy)
✅ Combinaisons de plusieurs mots-clés
✅ Tests manuels avec le navigateur
✅ Tests unitaires avec JUnit


Prochaine Étape​

Vous maîtrisez les conventions! Passez aux Relations entre Entités (TP2).

➡️ TP6.2: Relations Employee-Department

⬅️ Retour au Module 6