TP6.1 - اتفاقيات التسمية المتقدمة
الأهداف
في نهاية هذا التمرين، ستكون قادرًا على:
- ✅ استخدام أكثر من 15 كلمة مفتاحية في Spring Data JPA
- ✅ إنشاء استعلامات معقدة بدون SQL
- ✅ دمج عدة كلمات مفتاحية
- ✅ اختبار جميع طرقك
المدة المقدرة: 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 طرق إضافية
حان دورك! أنشئ هذه الطرق:
- العثور على الوصف الذي يحتوي على كلمة مفتاحية
- العثور على المنتجات التي تم إنشاؤها قبل تاريخ معين
- العثور على المنتجات حسب الفئة والنشاط، مرتبة حسب السعر تنازليًا
- العثور على المنتجات مع المخزون بين الحد الأدنى والحد الأقصى
- العثور على المنتجات التي ينتهي اسمها بلاحقة
- العثور على المنتجات مع السعر >= الحد الأدنى أو المخزون < الحد الأدنى
- العثور على المنتجات حسب الفئة غير موجودة في قائمة
- العثور على المنتجات النشطة بدون تخفيض
- العثور على المنتجات حسب السعر ومرتبة حسب المخزون تنازليًا ثم الاسم تصاعديًا
- العثور على أرخص 5 منتجات (تلميح: استخدم Pageable)
الملخص
ما الذي قمت بممارسته
✅ 20 طريقة مع اتفاقيات التسمية
✅ 9 كلمات مفتاحية مختلفة (Between, LessThan, Containing, In, IsNull, True, False, IgnoreCase, OrderBy)
✅ دمج عدة كلمات مفتاحية
✅ اختبارات يدوية باستخدام المتصفح
✅ اختبارات وحدات باستخدام JUnit
الخطوة التالية
لقد أتقنت الاتفاقيات! انتقل إلى العلاقات بين الكيانات (TP2).