انتقل إلى المحتوى الرئيسي

TP6.1 - اتفاقيات التسمية المتقدمة

الأهداف​

في نهاية هذا التمرين، ستكون قادرًا على:

  1. ✅ استخدام أكثر من 15 كلمة مفتاحية في Spring Data JPA
  2. ✅ إنشاء استعلامات معقدة بدون SQL
  3. ✅ دمج عدة كلمات مفتاحية
  4. ✅ اختبار جميع طرقك

المدة المقدرة: 2 ساعة


المتطلبات الأساسية​

  • ✅ إكمال الفصل 1 من الوحدة 6
  • ✅ Maven 3.9+ و Java 17+
  • ✅ VS Code مع ملحقات Java

التحضير: إنشاء المشروع​

الخطوة 1: Spring Initializr​

اذهب إلى https://start.spring.io/

التكوين:

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

التبعيات:

  • Spring Web
  • Spring Data JPA
  • H2 Database

قم بالتنزيل، فك الضغط، وافتح في VS Code


الخطوة 2: التكوين​

الملف: 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

التمرين 1: إنشاء الكيان Product​

الخطوة 1.1: إنشاء الكيان​

الملف: 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; }
}

التمرين 2: إنشاء المستودع مع 20 طريقة​

الخطوة 2.1: مستودع الأساس​

الملف: 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);
}

التمرين 3: تهيئة بيانات الاختبار​

الخطوة 3.1: إنشاء DataLoader​

الملف: 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 ===");
};
}
}

التمرين 4: إنشاء وحدة التحكم للاختبار​

الخطوة 4.1: وحدة تحكم كاملة​

الملف: 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
);
}
}

التمرين 5: اختبارات يدوية​

بدء التطبيق​

mvn clean install -DskipTests spring-boot:run

يجب أن تعرض وحدة التحكم:

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

الاختبار 1: المنتجات حسب الفئة​

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

النتيجة المتوقعة: 3 منتجات (MacBook Pro, iPhone 15, iPad Pro, Rare Item)


الاختبار 2: المنتجات النشطة​

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

النتيجة المتوقعة: 9 منتجات (جميعها باستثناء "Old Phone")


الاختبار 3: المنتجات أقل من 700€​

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

النتيجة المتوقعة: Samsung Phone, Google Pixel, Mac Mini, Old Phone, HP Desktop


الاختبار 4: السعر بين 600€ و 1000€​

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

النتيجة المتوقعة: Samsung Phone, Mac Mini, Dell Laptop, HP Desktop, iPhone 15


الاختبار 5: البحث عن طريق الكلمة المفتاحية​

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

النتيجة المتوقعة: iPhone 15, Samsung Phone, Google Pixel, Old Phone


الاختبار 6: يبدأ بـ "Mac"​

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

النتيجة المتوقعة: MacBook Pro, Mac Mini


الاختبار 7: عدة فئات​

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

النتيجة المتوقعة: جميع المنتجات Electronics + Phones


الاختبار 8: المنتجات مع تخفيض​

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

النتيجة المتوقعة: Mac Mini (الوحيد مع تخفيض)


الاختبار 9: المنتجات النشطة أقل من 1000€​

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

النتيجة المتوقعة: جميع النشطة < 1000€


الاختبار 10: البحث المعقد​

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

النتيجة المتوقعة: iPhone 15, iPad Pro, Rare Item (مرتبة حسب الاسم)


التمرين 6: اختبارات وحدات (إضافي)​

إنشاء الاختبارات​

الملف: 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());
}
}

تشغيل الاختبارات:

mvn test

التحدي: إنشاء 10 طرق إضافية​

حان دورك! أنشئ هذه الطرق:

  1. العثور على الوصف الذي يحتوي على كلمة مفتاحية
  2. العثور على المنتجات التي تم إنشاؤها قبل تاريخ معين
  3. العثور على المنتجات حسب الفئة والنشاط، مرتبة حسب السعر تنازليًا
  4. العثور على المنتجات مع المخزون بين الحد الأدنى والحد الأقصى
  5. العثور على المنتجات التي ينتهي اسمها بلاحقة
  6. العثور على المنتجات مع السعر >= الحد الأدنى أو المخزون < الحد الأدنى
  7. العثور على المنتجات حسب الفئة غير موجودة في قائمة
  8. العثور على المنتجات النشطة بدون تخفيض
  9. العثور على المنتجات حسب السعر ومرتبة حسب المخزون تنازليًا ثم الاسم تصاعديًا
  10. العثور على أرخص 5 منتجات (تلميح: استخدم Pageable)

الملخص​

ما الذي قمت بممارسته​

✅ 20 طريقة مع اتفاقيات التسمية
✅ 9 كلمات مفتاحية مختلفة (Between, LessThan, Containing, In, IsNull, True, False, IgnoreCase, OrderBy)
✅ دمج عدة كلمات مفتاحية
✅ اختبارات يدوية باستخدام المتصفح
✅ اختبارات وحدات باستخدام JUnit


الخطوة التالية​

لقد أتقنت الاتفاقيات! انتقل إلى العلاقات بين الكيانات (TP2).

➡️ TP6.2: علاقات الموظف-القسم

⬅️ العودة إلى الوحدة 6