404 Not Found

404 Not Found


nginx

التحقق من البيانات

التحقق من البيانات هو خط الدفاع الأول لواجهة API—الإدخال غير الصالح يجب ألا يصل إلى منطق الأعمال، وكلما رُفض مبكراً كان ذلك أفضل.

1. ما ستتعلمه


2. قصة حقيقية من مطوّر واجهات

(1) نقطة الألم: بيانات قذرة مُخزنة في قاعدة البيانات

اكتشفت Alice بيانات غريبة في قاعدة بيانات OrderFlow: كمية طلب بقيمة -5، عنوان بريد إلكتروني بصيغة "abc"، وسعر منتج بقيمة 0. أبلغ Bob أن مستخدماً قد أرسل منتجاً بمخزون سلبي عبر واجهة API، مما تسبب في شذوذ في إحصائيات التقارير. كانت Alice قد كتبت سابقاً الكثير من كود التحقق if-else في طبقة الخدمة، وهو كود مطول ومعرض للسهو.

(2) حل Bean Validation

أعلن قواعد التحقق باستخدام التعليقات، وSpring يُفعّل التحقق تلقائياً:

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @Min(1) @Max(100) Integer quantity,
    @Email String customerEmail
) {}

الطلبات غير الصالحة تُرفض قبل أن تصل إلى المتحكم.

(3) العوائد

استبدلت Alice جميع كود التحقق اليدوي بـ Bean Validation، مما قلل حجم الكود في المتحكم بنسبة 40% وضمن عدم تفويت أي قاعدة تحقق. لم يعد هناك بيانات قذرة في قاعدة البيانات.


3. نظام تعليقات Bean Validation

(1) تدفق تنفيذ التحقق

100%
graph TD
    A["طلب العميل<br/>@RequestBody"] --> B{"@Valid<br/>مُفعّل؟"}
    B -->|نعم| C["Hibernate Validator<br/>فحص القيود"]
    C --> D{"الكل صالح؟"}
    D -->|نعم| E["طريقة المتحكم<br/>تُنفّذ"]
    D -->|لا| F["MethodArgumentNotValidException<br/>400 Bad Request"]
    B -->|لا| G["تخطي التحقق<br/>بيانات قذرة محتملة"]

(3) طريقة تفعيل التحقق

التعليق الغرض الموقع
@Valid تفعيل التحقق المتسلسل (بما في ذلك الكائنات المتداخلة) معاملات الطريقة، الحقول
@Validated يدعم التحقق المجمع الصنف ومعاملات الطريقة
@Validated(Group.class) تحديد مجموعة التحقق معاملات الطريقة

(1) ▶ مثال: التحقق من معاملات المتحكم

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @PostMapping
    public ResponseEntity<Order> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        // إذا فشل التحقق، يتم إطلاق MethodArgumentNotValidException
        // قبل الوصول إلى هذا السطر
        Order order = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(order);
    }
}

public record CreateOrderRequest(
    @NotNull(message = "Product ID is required")
    Long productId,

    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,

    @Email(message = "Invalid email format")
    String customerEmail
) {}

الناتج:

TEXT
// التنفيذ ناجح

4. تعليقات القيود الشائعة

(1) مرجع سريع لتعليقات القيود

التعليق النوع المناسب الوصف مثال
@NotNull جميع الأنواع لا يمكن أن يكون null @NotNull Long id
@NotBlank String لا يمكن أن يكون فارغاً/null/مسافات فقط @NotBlank String name
@NotEmpty String/Collection لا يمكن أن يكون فارغاً/null @NotEmpty List<String> tags
@Size String/Collection نطاق الطول/الحجم @Size(min=2, max=100)
@Min / @Max أنواع البيانات نطاق القيمة @Min(0) @Max(99999)
@Positive أنواع رقمية رقم موجب @Positive BigDecimal price
@Email String تنسيق البريد الإلكتروني @Email String email
@Pattern String مطابقة تعبير نمطي @Pattern(regexp="^[A-Z]")
@Past / @Future أنواع التاريخ ماضٍ/مستقبل @Past LocalDate birthDate

(1) ▶ مثال: التحقق من DTO للمنتج

JAVA
public record CreateProductRequest(
    @NotBlank(message = "Product name is required")
    @Size(min = 2, max = 200, message = "Name must be 2-200 characters")
    String name,

    @NotNull(message = "Price is required")
    @Positive(message = "Price must be positive")
    @DecimalMin(value = "0.01", message = "Price must be at least 0.01")
    BigDecimal price,

    @NotNull(message = "Stock is required")
    @Min(value = 0, message = "Stock cannot be negative")
    Integer stock,

    @Email(message = "Supplier email must be valid")
    String supplierEmail,

    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU format: XXX-0000")
    String sku
) {}

الناتج:

TEXT
// التنفيذ ناجح
مقارنة التعليقات null "" " " "abc"
@NotNull
@NotBlank
@NotEmpty
🔥 خطأ شائع: عند التحقق من نوع String، استخدم @NotBlank بدلاً من @NotNull، لأن السلسلة الفارغة عادةً غير صالحة أيضاً.


5. تحققات مخصصة

(1) خطوات التنفيذ

  1. تعريف تعليق القيد
  2. تنفيذ ConstraintValidator<A, T>
  3. الاستخدام على حقول DTO

(1) ▶ مثال: تحقق مخصص @ValidOrderQuantity

JAVA
// الخطوة 1: تعريف تعليق القيد
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderQuantityValidator.class)
public @interface ValidOrderQuantity {
    String message() default "Order quantity exceeds product stock limit";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// الخطوة 2: تنفيذ ConstraintValidator
public class OrderQuantityValidator
        implements ConstraintValidator<ValidOrderQuantity, Integer> {

    private static final int MAX_QUANTITY_PER_ITEM = 100;

    @Override
    public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
        if (quantity == null) {
            return true; // دع @NotNull يتولى فحص null
        }
        return quantity >= 1 && quantity <= MAX_QUANTITY_PER_ITEM;
    }
}

// الخطوة 3: الاستخدام في DTO
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity
) {}

الناتج:

TEXT
// التنفيذ ناجح

6. التحقق المجمع

(1) تصنيف قواعد التحقق حسب السيناريو

سيناريو "الإنشاء" و"التحديث" يتطلبان عادةً قواعد تحقق مختلفة:

السيناريو مُعرّف المنتج الاسم السعر
إنشاء يُولّد تلقائياً؛ غير مطلوب مطلوب مطلوب
تحديث مطلوب (للدلالة على من أجرى التغيير) اختياري اختياري

(1) ▶ مثال: التحقق المجمع

JAVA
// تعريف واجهات المجموعة
public interface Create {}
public interface Update {}

// DTO مع تحقق واعٍ بالمجموعة
public record ProductRequest(
    @Null(groups = Create.class, message = "ID must be null for creation")
    @NotNull(groups = Update.class, message = "ID is required for update")
    Long id,

    @NotBlank(groups = Create.class, message = "Name is required for creation")
    @Size(min = 2, max = 200)
    String name,

    @NotNull(groups = Create.class, message = "Price is required for creation")
    @Positive BigDecimal price
) {}

الناتج:

TEXT
// التنفيذ ناجح
JAVA
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    @PostMapping
    public ResponseEntity<Product> create(
            @Validated(Create.class) @RequestBody ProductRequest request) {
        // يتم تطبيق تحققات مجموعة Create فقط
        // ...
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(
            @PathVariable Long id,
            @Validated(Update.class) @RequestBody ProductRequest request) {
        // يتم تطبيق تحققات مجموعة Update فقط
        // ...
        return ResponseEntity.ok().build();
    }
}

(2) ▶ مثال: التحقق المتداخل

JAVA
public record CreateOrderRequest(
    @NotNull Long productId,
    @ValidOrderQuantity Integer quantity,
    @Valid @NotNull ShippingAddress shippingAddress
) {}

public record ShippingAddress(
    @NotBlank String street,
    @NotBlank String city,
    @NotBlank String zipCode,
    @Pattern(regexp = "^[A-Z]{2}$") String country
) {}

الناتج:

TEXT
// التنفيذ ناجح
📌 نقطة رئيسية: يجب أن تتضمن الكائنات المتداخلة @Valid؛ وإلا فلن يسري التحقق من الحقول المتداخلة. @Validated لا يدعم التحقق المتسلسل المتداخل.


7. تنسيق استجابات أخطاء التحقق

(1) توحيد تنسيق استجابة الخطأ

JAVA
@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());

        List<Map<String, String>> errors = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> Map.of(
                "field", fe.getField(),
                "message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "",
                "rejectedValue", fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"
            ))
            .toList();
        body.put("errors", errors);
        return ResponseEntity.badRequest().body(body);
    }
}
💻 الناتج:

JSON
{
  "timestamp": "2024-01-15T10:00:00Z",
  "status": 400,
  "errors": [
    {"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": "0"},
    {"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
  ]
}

8. مثال شامل: نظام التحقق الكامل لـ OrderFlow

JAVA
// مجموعات التحقق
package com.orderflow.validation;
public interface Create {}
public interface Update {}

// تحقق مخصص: @ValidShippingAddress
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ShippingAddressValidator.class)
public @interface ValidShippingAddress {
    String message() default "Invalid shipping address";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ShippingAddressValidator
        implements ConstraintValidator<ValidShippingAddress, String> {
    @Override
    public boolean isValid(String address, ConstraintValidatorContext ctx) {
        if (address == null || address.isBlank()) return false;
        return address.length() >= 10 && address.length() <= 500;
    }
}

// DTOs
public record CreateOrderRequest(
    @NotNull(groups = Create.class) Long productId,
    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity,
    @Email String customerEmail,
    @ValidShippingAddress String shippingAddress
) {}

public record CreateProductRequest(
    @Null(groups = Create.class) @NotNull(groups = Update.class) Long id,
    @NotBlank(groups = Create.class) @Size(min = 2, max = 200) String name,
    @NotNull(groups = Create.class) @Positive BigDecimal price,
    @Min(0) Integer stock,
    @Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU: XXX-0000") String sku
) {}

// المتحكم
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    @PostMapping
    public ResponseEntity<Void> create(
            @Validated(Create.class) @RequestBody CreateOrderRequest req) {
        // التحقق ناجح، الانتقال إلى منطق الأعمال
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

❓ أسئلة شائعة

س ما الفرق بين @Valid و@Validated؟
ج @Valid هو تعليق معيار JSR-380 يدعم التحقق المتداخل والمتسلسل. @Validated هو تعليق امتداد من Spring يدعم التحقق المجمع. استخدم @Validated للتجميع و@Valid للتداخل؛ يمكن استخدام كليهما معاً.
س ماذا يُرجع عند فشل التحقق؟
ج بشكل افتراضي، يُرجع Spring Boot رمز حالة 400 Bad Request مع رسالة خطأ JSON. يمكنك تخصيص التنسيق باستخدام @RestControllerAdvice؛ هذا الدرس يوفر حلاً للتنسيق المعياري.
س كيف أختار بين @NotBlank و@NotEmpty و@NotNull؟
ج استخدم @NotBlank للسلاسل (لا يسمح بـ null أو سلاسل فارغة أو سلاسل من مسافات فقط)؛ استخدم @NotEmpty للمجموعات (لا يسمح بـ null أو مجموعات فارغة)؛ واستخدم @NotNull لجميع الأنواع الأخرى.
س هل يمكن تفعيل التحقق المجمع والتحقق الافتراضي في نفس الوقت؟
ج بشكل افتراضي، يتم تعطيل مجموعة "Default" بمجرد تحديد مجموعة محددة. إذا كنت تريد تفعيل كليهما، اجعل المجموعة المخصصة ترث من "Default": public interface Create extends Default {}.
س هل يمكن حقن Spring Beans في التحققات المخصصة؟
ج نعم. تُدار ConstraintValidators بواسطة حاوية Spring، لذا يمكنك استخدام @Autowired لحقن Beans في طريقة isValid (مثلاً للاستعلام عن قاعدة البيانات للتحقق من التفرد).
س كيف أُدول رسائل الخطأ؟
ج عرّف مفاتيح الرسائل في resources/ValidationMessages.properties، مثل order.quantity.invalid=Order quantity must be between {min} and {max}؛ يتم دعم ملفات متعددة اللغات.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): أضف تعليقات Bean Validation إلى CreateOrderRequest وCreateProductRequest في OrderFlow للتحقق من الإدخال غير الصالح وإرجاع خطأ 400.

  2. تمرين متقدم (الصعوبة ⭐⭐): نفّذ تحققاً مجمعاً—لعمليات Create، يكون name وprice مطلوبين؛ لعمليات Update، يكون id مطلوباً وname وprice اختياريين. نفّذ تحققاً مخصصاً @ValidShippingAddress.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ تحققاً @UniqueProductSku يحقن ProductRepository للتحقق مما إذا كان SKU موجوداً مسبقاً، لتنفيذ التحقق من التفرد على مستوى قاعدة البيانات. فكّر في الحد الفاصل بين مسؤوليات التحقق وطبقة الأعمال.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%