الفصل 2: التحقق من البيانات
مقدمة
التحقق من البيانات ضروري لضمان سلامة وجودة البيانات في تطبيقك. يدمج Spring Boot Bean Validation (JSR 380) للتحقق بسهولة من بيانات الإدخال.
Bean Validation (JSR 380)
تبعية Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
ملاحظة: هذه التبعية مضمنة بالفعل في spring-boot-starter-web.
التعليقات التوضيحية الرئيسية للتحقق
جدول ملخص
| التعليق التوضيحي | الاستخدام | المثال |
|---|---|---|
@NotNull | لا يمكن أن يكون null | @NotNull private String name; |
@NotEmpty | لا يمكن أن يكون null أو فارغ | @NotEmpty private String email; |
@NotBlank | لا يمكن أن يكون null أو فارغ أو مجرد مسافات | @NotBlank private String title; |
@Size | حجم الحد الأدنى/الأقصى | @Size(min=3, max=50) private String name; |
@Min | القيمة الدنيا | @Min(0) private int age; |
@Max | القيمة القصوى | @Max(100) private int grade; |
@Email | تنسيق بريد إلكتروني صالح | @Email private String email; |
@Pattern | تعبير منتظم | @Pattern(regexp="[0-9]{10}") private String phone; |
@Past | تاريخ في الماضي | @Past private LocalDate birthDate; |
@Future | تاريخ في المستقبل | @Future private LocalDate appointmentDate; |
مثال كامل: كيان مع التحقق
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "الاسم لا يمكن أن يكون فارغًا")
@Size(min = 3, max = 50, message = "يجب أن يحتوي الاسم على 3 إلى 50 حرفًا")
private String name;
@NotNull(message = "السعر مطلوب")
@Min(value = 0, message = "لا يمكن أن يكون السعر سالبًا")
private Double price;
@NotNull(message = "الكمية مطلوبة")
@Min(value = 0, message = "لا يمكن أن تكون الكمية سالبة")
private Integer quantity;
@Email(message = "البريد الإلكتروني غير صالح")
private String supplierEmail;
// Constructors, getters, setters
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters et Setters
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 Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public String getSupplierEmail() {
return supplierEmail;
}
public void setSupplierEmail(String supplierEmail) {
this.supplierEmail = supplierEmail;
}
}
الاستخدام في الـ Controller مع @Valid
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// CREATE مع التحقق
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// UPDATE مع التحقق
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// GET - الكل
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
// GET - حسب ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
نقاط رئيسية:
@Validيُفعّل التحقق التلقائي- إذا فشل التحقق، يقوم Spring بإرجاع كود 400 Bad Request تلقائيًا
إدارة أخطاء التحقق
استجابة تلقائية (افتراضيًا)
عندما يفشل التحقق، يقوم Spring بإرجاع:
{
"timestamp": "2025-11-28T10:15:30.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "الاسم لا يمكن أن يكون فارغًا"
},
{
"field": "price",
"message": "لا يمكن أن يكون السعر سالبًا"
}
]
}
تخصيص مع @ControllerAdvice
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
استجابة مخصصة:
{
"name": "الاسم لا يمكن أن يكون فارغًا",
"price": "لا يمكن أن يكون السعر سالبًا"
}
رسائل مخصصة
النهج 1: مباشرة في التعليق التوضيحي
@NotBlank(message = "اسم المنتج مطلوب")
@Size(min = 3, max = 50, message = "يجب أن يحتوي الاسم على {min} إلى {max} حرفًا")
private String name;
النهج 2: ملف الرسائل (messages.properties)
ملف src/main/resources/messages.properties:
product.name.notblank=اسم المنتج مطلوب
product.name.size=يجب أن يحتوي الاسم على {min} إلى {max} حرفًا
product.price.min=لا يمكن أن يكون السعر أقل من {value}
كيان:
@NotBlank(message = "{product.name.notblank}")
@Size(min = 3, max = 50, message = "{product.name.size}")
private String name;
@Min(value = 0, message = "{product.price.min}")
private Double price;
التحقق المخصص
إنشاء تعليق توضيحي مخصص
package com.example.demo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
String message() default "رقم الهاتف غير صالح";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
إنشاء المدقق
package com.example.demo.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public void initialize(ValidPhoneNumber constraintAnnotation) {
}
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
if (phoneNumber == null) {
return false;
}
// تنسيق كندي: (XXX) XXX-XXXX أو XXX-XXX-XXXX
return phoneNumber.matches("\\(?\\d{3}\\)?[- ]?\\d{3}[- ]?\\d{4}");
}
}
الاستخدام
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@ValidPhoneNumber(message = "يجب أن يكون رقم الهاتف بالتنسيق الكندي")
private String phoneNumber;
// Getters, setters
}
مجموعات التحقق
تتيح التحقق بشكل مختلف حسب السياق (إنشاء مقابل تحديث).
public interface CreateValidation {}
public interface UpdateValidation {}
@Entity
public class Product {
@Id
@GeneratedValue
@Null(groups = CreateValidation.class, message = "يجب أن يكون المعرف null عند الإنشاء")
@NotNull(groups = UpdateValidation.class, message = "المعرف مطلوب للتحديث")
private Long id;
@NotBlank(groups = {CreateValidation.class, UpdateValidation.class})
private String name;
}
// Controller
@PostMapping
public ResponseEntity<Product> create(@Validated(CreateValidation.class) @RequestBody Product product) {
// ...
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id,
@Validated(UpdateValidation.class) @RequestBody Product product) {
// ...
}
ملخص الفصل
النقاط الرئيسية
- Bean Validation (JSR 380) للتحقق من البيانات
- @Valid في الـ controller لتفعيل التحقق
- رسائل مخصصة في التعليقات التوضيحية أو ملفات الخصائص
- @ControllerAdvice لإدارة الأخطاء بشكل عام
- التحقق المخصص مع التعليقات التوضيحية المخصصة
التعليقات التوضيحية الأساسية
✅ @NotNull, @NotEmpty, @NotBlank
✅ @Size, @Min, @Max
✅ @Email, @Pattern
✅ @Past, @Future
✅ @Valid