Spring REST API & Data JPA/Jakarta Bean Validation & @Valid — declarative constraint trên DTO
16/46
Bài 16 / 46~12 phútError, Validation & API DocsMiễn phí lượt xem

Jakarta Bean Validation & @Valid — declarative constraint trên DTO

Jakarta Bean Validation 3.0 (JSR 380): 23 built-in constraint, @Valid trigger và cascade nested object, @Valid vs @Validated — cơ chế AOP phía dưới, và tại sao declarative validation tách rule khỏi business logic.

TL;DR: Jakarta Bean Validation 3.0 (JSR 380) cung cấp 23 annotation constraint — @NotBlank, @Size, @Email, @Min/@Max, @Positive, @Past/@Future... Đặt annotation lên field DTO, sau đó thêm @Valid trên parameter method để Spring kích hoạt validation. Khi constraint fail, Spring ném MethodArgumentNotValidException và trả HTTP 400 trước khi method body chạy. @Valid cascade vào nested object; @Validated (Spring) bật method-level validation qua AOP proxy — khác với @Valid (Jakarta). Pitfall lớn nhất: Boot 3 tách spring-boot-starter-validation khỏi starter-web — quên add dependency thì annotation có nhưng validation silent skip.

Bài trước (Request binding) giải thích Spring map JSON body vào Java object như thế nào. Sau khi bind xong, câu hỏi tiếp theo là: dữ liệu vừa bind có hợp lệ không? Bài này trả lời đúng câu đó — cơ chế, annotation, và tại sao chọn declarative thay vì if/else manual.

1. Jakarta Bean Validation — chuẩn JSR 380

Jakarta Bean Validation 3.0 (trước đây là JSR 380, namespace javax.validation) là chuẩn Java EE định nghĩa API khai báo constraint lên field và method bằng annotation. Chuẩn này chỉ định nghĩa interface — implementation thật là Hibernate Validator, thư viện đi kèm spring-boot-starter-validation.

<!-- Boot 3+: PHAI add explicit, khong tu dong trong starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Dependency này kéo theo jakarta.validation-api (spec) và hibernate-validator (implementation). Thiếu nó — annotation compile được, nhưng constraint không trigger lúc runtime.

Vì sao declarative thay if/else?

Cách viết validation thủ công:

// Manual validation -- tron vao business logic
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) {
    if (req.customer() == null || req.customer().isBlank()) {
        throw new IllegalArgumentException("Customer required");
    }
    if (req.total() == null || req.total().compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("Total must be positive");
    }
    if (req.items() == null || req.items().isEmpty()) {
        throw new IllegalArgumentException("Items required");
    }
    // ... 10 field nua, 30 dong if/else
    return orderService.create(req);
}

Mỗi field thêm một if. Rule lặp lại qua mọi controller dùng cùng DTO. Khi rule thay đổi, phải tìm sửa ở nhiều chỗ.

Declarative:

public record OrderRequest(
    @NotBlank String customer,
    @NotNull @Positive BigDecimal total,
    @NotEmpty @Valid List<OrderItem> items
) {}

@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) {
    // Khong mot dong if/else -- Spring validate truoc khi vao day
    return orderService.create(req);
}

Rule sống trên DTO — một nơi duy nhất. Controller, service, test đều nhìn thấy cùng constraint. Đây là nguyên lý separation of concerns: rule validation tách khỏi logic xử lý.

flowchart LR
    Client["Client<br/>POST /orders"] --> DS["DispatcherServlet"]
    DS --> Resolver["ArgumentResolver<br/>bind JSON -> OrderRequest"]
    Resolver --> Validator["Hibernate Validator<br/>check constraint"]
    Validator -->|"fail"| Ex["MethodArgumentNotValidException<br/>-> 400 Bad Request"]
    Validator -->|"pass"| Method["OrderController.create()<br/>business logic"]

Validation xảy ra trước khi method body chạy — controller không bao giờ nhận object invalid.

2. 23 built-in constraint — bảng tra cứu

Jakarta Bean Validation 3.0 đi kèm 23 constraint chia theo nhóm:

Null / presence

AnnotationÁp dụngÝ nghĩa
@NotNullObjectKhông được null
@NullObjectPhải null (dùng với validation group)
@NotBlankStringKhông null, không empty, không toàn whitespace
@NotEmptyString, Collection, Map, ArrayKhông null, không empty

@NotBlank mạnh hơn @NotEmpty: " " (chỉ space) fail @NotBlank nhưng pass @NotEmpty.

Number — range và sign

AnnotationÝ nghĩaVí dụ
@Min(value)Số nguyên không nhỏ hơn value@Min(1)
@Max(value)Số nguyên không lớn hơn value@Max(100)
@DecimalMin(value)Số thực không nhỏ hơn value (BigDecimal precision)@DecimalMin("0.01")
@DecimalMax(value)Số thực không lớn hơn value@DecimalMax("999999.99")
@PositiveSố dương, lớn hơn 0
@PositiveOrZeroKhông âm, từ 0 trở lên
@NegativeSố âm, nhỏ hơn 0
@NegativeOrZeroKhông dương, từ 0 trở xuống
@Digits(integer, fraction)Tối đa N chữ số phần nguyên, M phần thập phân@Digits(6, 2)

String — độ dài và format

AnnotationÝ nghĩa
@Size(min, max)Length của String, Collection, Array trong khoảng min-max
@EmailChuỗi hợp lệ theo format email
@Pattern(regexp)Match biểu thức chính quy Java

Date / time

AnnotationÝ nghĩa
@PastThời điểm trong quá khứ (trước now)
@PastOrPresentQuá khứ hoặc hiện tại
@FutureThời điểm trong tương lai (sau now)
@FutureOrPresentTương lai hoặc hiện tại

Áp dụng cho LocalDate, LocalDateTime, Instant, Date, Calendar.

Boolean và cascade

AnnotationÝ nghĩa
@AssertTrueboolean field hoặc method is*() phải trả true
@AssertFalseboolean field hoặc method phải trả false
@ValidKích hoạt cascade — validate nested object

Ví dụ kết hợp thực tế

public record OrderRequest(
    @NotBlank(message = "Customer name is required")
    @Size(min = 2, max = 100)
    String customer,

    @NotBlank
    @Email
    String email,

    @NotNull
    @Positive
    @DecimalMax("999999.99")
    BigDecimal total,

    @NotNull
    @Future(message = "Delivery date must be in the future")
    LocalDate deliveryDate,

    @NotEmpty
    @Size(max = 100, message = "Maximum 100 items per order")
    @Valid                      // cascade -- validate tung OrderItem
    List<OrderItem> items,

    @Pattern(regexp = "^[A-Z]{2}\\d{4}$", message = "Voucher format: 2 letters + 4 digits")
    String voucherCode          // nullable -- khong co @NotBlank
) {}

public record OrderItem(
    @NotBlank String sku,
    @Min(1) @Max(99) int quantity,
    @NotNull @Positive BigDecimal unitPrice
) {}

Constraint gắn trên record component — Spring Hibernate Validator tự pick up khi @Valid kích hoạt.

3. Cơ chế bên dưới — @Valid trigger MethodArgumentNotValidException

@Valid trên method parameter không làm gì ở compile time. Tác dụng thật xảy ra lúc runtime, bên trong HandlerMethodArgumentResolver.

flowchart TB
    Req["HTTP POST<br/>{\"customer\":\"\",\"total\":-5}"] --> DS["DispatcherServlet"]
    DS --> RMAB["RequestResponseBodyMethodProcessor<br/>(ArgumentResolver cho @RequestBody)"]
    RMAB --> Jackson["Jackson deserialize<br/>JSON -> OrderRequest object"]
    Jackson --> Bind["DataBinder.validate()<br/>neu co @Valid tren parameter"]
    Bind --> HV["Hibernate Validator<br/>doc constraint tren field"]
    HV -->|"co violation"| MANE["MethodArgumentNotValidException<br/>chua BindingResult voi list FieldError"]
    HV -->|"pass"| Controller["OrderController.create(req)<br/>business logic"]
    MANE --> EH["@RestControllerAdvice<br/>handle -> 400 Problem Details"]

Bước quan trọng là DataBinder.validate() — nó được gọi chỉ khi parameter có annotation @Valid hoặc @Validated. Không có annotation đó, validate() không được gọi, Hibernate Validator không chạy.

MethodArgumentNotValidException chứa BindingResult — danh sách FieldError, mỗi lỗi có:

  • field: tên field vi phạm
  • code: tên constraint (NotBlank, Size.min, ...)
  • defaultMessage: message từ annotation

Bài Problem Details mổ xẻ cách build response 400 từ BindingResult thành format chuẩn RFC 9457.

@Valid cascade nested object

Nếu DTO chứa nested object, constraint bên trong nested chỉ được validate khi field đó có @Valid:

// Khong cascade -- constraint trong OrderItem bi skip
public record OrderRequest(
    List<OrderItem> items
) {}

// Co cascade -- Hibernate Validator di vao tung OrderItem
public record OrderRequest(
    @NotEmpty
    @Valid List<OrderItem> items
) {}

Cascade hoạt động đệ quy: nếu OrderItem cũng chứa nested object có @Valid, Hibernate Validator tiếp tục đi sâu thêm.

4. @Valid vs @Validated — hai annotation khác gốc, khác mục đích

Đây là điểm gây nhầm lẫn phổ biến nhất:

Aspect@Valid (Jakarta)@Validated (Spring)
Packagejakarta.validationorg.springframework.validation.annotation
Mục đích chínhTrigger validation trên parameter, cascade nestedBật method-level validation qua AOP proxy
Vị trí dùngMethod parameter, fieldClass, method, parameter
Hỗ trợ validation groupKhôngCó — @Validated(Create.class)
Exception ném ra (controller)MethodArgumentNotValidExceptionConstraintViolationException

@Validated class-level — bật method validation AOP

Đặt @Validated trên controller class để kích hoạt validation cho @RequestParam, @PathVariable, và parameter đơn lẻ:

@RestController
@Validated                          // BAT method-level validation
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    public List<OrderDto> list(
        @RequestParam @Min(0) int page,
        @RequestParam @Min(1) @Max(100) int size,
        @RequestParam @Email String filterEmail
    ) { ... }
}

Nếu không có @Validated trên class — @Min, @Max, @Email trên @RequestParam silently skip. Không có lỗi compile, không có warning — constraint âm thầm bị bỏ qua.

Cơ chế AOP phía dưới

Khi Spring thấy @Validated trên class, nó tạo một AOP proxy bao quanh class đó. Proxy intercept mọi method call, gọi MethodValidationInterceptor trước khi chuyển tiếp vào method thật:

flowchart LR
    Caller["DispatcherServlet<br/>goi list(page=-1, ...)"] --> Proxy["OrderController Proxy<br/>(CGLIB-generated)"]
    Proxy --> MVI["MethodValidationInterceptor<br/>@Min(0) tren page -- violation!"]
    MVI -->|"fail"| CVE["ConstraintViolationException<br/>-> 400"]
    MVI -->|"pass"| Real["OrderController.list()<br/>business logic"]

ConstraintViolationException (từ @Validated AOP) khác MethodArgumentNotValidException (từ @Valid trên @RequestBody). Cả hai đều cần handler trong @RestControllerAdvice — bài Problem Details cover cách map cả hai thành cùng format 400.

@Valid vs @Validated — quy tắc thực tế

// Quy tac nho:
// @Valid      --> dat tren @RequestBody hoac field de cascade
// @Validated  --> dat tren CLASS de bat method validation (param, path var)

@RestController
@Validated                          // (1) bat method validation cho param/path
public class OrderController {

    // (2) @Valid tren @RequestBody -- trigger + cascade
    @PostMapping
    public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }

    // (3) constraint tren param work nho @Validated class-level o tren
    @GetMapping("/{id}")
    public OrderDto findById(@PathVariable @NotNull @Positive Long id) { ... }
}

5. Pitfall thường gặp

Pitfall 1 — Quên @Valid trên @RequestBody

// SAI -- constraint tren OrderRequest bi skip hoan toan
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) { ... }

// DUNG
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }

Đây là pitfall số 1: annotation @NotBlank, @Size trên OrderRequest compile tốt nhưng không có hiệu lực. Invalid data đi thẳng vào service.

Pitfall 2 — Quên @Valid cascade nested object

// SAI -- OrderItem khong duoc validate du co @NotBlank @Min
public record OrderRequest(
    @NotEmpty List<OrderItem> items     // thieu @Valid
) {}

// DUNG
public record OrderRequest(
    @NotEmpty @Valid List<OrderItem> items
) {}

Pitfall 3 — Quên @Validated trên controller class cho @RequestParam

// SAI -- @Min tren page silent skip
@RestController                         // thieu @Validated
public class OrderController {
    @GetMapping
    public List<OrderDto> list(@RequestParam @Min(0) int page) { ... }
}

// DUNG
@RestController
@Validated
public class OrderController {
    @GetMapping
    public List<OrderDto> list(@RequestParam @Min(0) int page) { ... }
}

Pitfall 4 — Thiếu dependency Boot 3

<!-- Boot 3 TACH ra -- phai add explicit -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Không add dependency thì annotation vẫn compile OK (API jar có sẵn trong classpath qua starter-web transitive), nhưng Hibernate Validator vắng mặt khiến ValidationAutoConfiguration không active — constraint không bao giờ trigger.

Cách kiểm tra nhanh:

mvn dependency:tree | grep hibernate-validator
# Phai thay: hibernate-validator-X.Y.Z.Final.jar
Silent failure — nguyen nhan bug kho phat hien

Ba pitfall đầu (thiếu @Valid, thiếu cascade, thiếu @Validated) và thiếu dependency đều gây ra cùng triệu chứng: invalid data đi qua mà không có lỗi. Không có exception, không có log warning. Cách phát hiện đáng tin cậy nhất: viết integration test gửi request invalid, assert response là 400.

Liên hệ các bài khác

  • Request binding: bind JSON thành Java object là bước trước validation — @Valid chạy sau khi Jackson deserialize xong, trên object đã được bind.
  • Problem Details: khi @Valid trigger và constraint fail, Spring ném MethodArgumentNotValidException. Bài đó mổ xẻ cách @RestControllerAdvice bắt exception này và build response 400 theo RFC 9457 Problem Details — đây là nơi BindingResult được đọc và format.
  • Validation advanced: bài tiếp theo đào sâu validation groups (@Validated(Create.class) cho POST vs PUT khác rule), custom @Constraint annotation cho domain-specific rule, và i18n message qua messages.properties.

Tóm tắt

  • Jakarta Bean Validation 3.0 (JSR 380) là chuẩn Java cho declarative constraint. Hibernate Validator là implementation, đi với spring-boot-starter-validation — Boot 3 tách riêng, phải add explicit.
  • Declarative thắng manual: rule sống trên DTO — một nguồn sự thật. Không if/else scattered qua controller.
  • 23 built-in constraint nhóm theo: null/presence (@NotNull, @NotBlank, @NotEmpty), number (@Min, @Max, @Positive, @Digits...), string (@Size, @Email, @Pattern), date (@Past, @Future...), boolean (@AssertTrue), cascade (@Valid).
  • @Valid trigger validation: đặt trên @RequestBody parameter khiến DataBinder.validate() chạy; constraint fail ném MethodArgumentNotValidException và client nhận 400 trước khi method body thực thi.
  • @Valid cascade: thêm @Valid trên field collection/nested object trong DTO để Hibernate Validator đi vào từng phần tử và validate constraint bên trong.
  • @Valid vs @Validated: @Valid (Jakarta) trigger/cascade; @Validated (Spring) class-level bật AOP proxy cho method-level validation — cần cho @RequestParam/@PathVariable constraint.
  • Silent skip là bug class: thiếu @Valid, thiếu cascade, thiếu @Validated, hoặc thiếu dependency đều cho kết quả giống nhau — invalid data qua mà không cảnh báo.

Tự kiểm tra

Tự kiểm tra
Q1
Vì sao Jakarta Bean Validation chọn cách khai báo constraint bằng annotation thay vì yêu cầu dev viết logic validate bằng if/else trong mỗi method? Lợi ích thiết kế cụ thể là gì?

Cách annotation (declarative) tách rule validation ra khỏi logic xử lý. Rule nằm trên DTO — một nguồn sự thật duy nhất. Mọi nơi dùng DTO (controller, service, test) đều nhìn thấy cùng constraint mà không cần copy.

Cách if/else (imperative) gây ra 3 vấn đề: (1) Rule bị scatter qua nhiều method — khi rule thay đổi phải tìm sửa nhiều chỗ. (2) Logic validate trộn với business logic — controller phải "biết" rule của domain. (3) Dễ bỏ sót: dev thêm controller mới copy-paste không đầy đủ.

Với annotation, container (Hibernate Validator qua AOP) intercept method call, validate constraint trước khi method body chạy. Dev không thể "quên" validate — chỉ có thể quên đặt @Valid (pitfall có thể catch bằng test).

Đây là biểu hiện của nguyên lý Separation of Concerns: rule validation là một concern tách biệt khỏi business logic, nên sống ở tầng khai báo (annotation trên DTO), không phải tầng thực thi (method body controller).

Q2
Đoạn code sau có mấy vấn đề? Liệt kê đủ và giải thích mỗi vấn đề gây ra hậu quả gì.
@RestController
@RequestMapping("/api/products")
public class ProductController {

  @PostMapping
  public ProductDto create(@RequestBody ProductRequest req) {
      return service.create(req);
  }

  @GetMapping
  public List<ProductDto> list(
      @RequestParam @Min(0) int page,
      @RequestParam @Min(1) @Max(50) int size
  ) {
      return service.list(page, size);
  }
}

public record ProductRequest(
  @NotBlank String name,
  @Positive BigDecimal price,
  @NotEmpty @Valid List<CategoryRef> categories
) {}

public record CategoryRef(
  @NotNull Long id,
  @NotBlank String code
) {}

2 vấn đề:

  1. Thiếu @Valid trên @RequestBody:

    // SAI
    public ProductDto create(@RequestBody ProductRequest req) { ... }
    // DUNG
    public ProductDto create(@Valid @RequestBody ProductRequest req) { ... }

    Hậu quả: mọi constraint trên ProductRequest (@NotBlank name, @Positive price, @NotEmpty categories) bị skip hoàn toàn. Request với name="", price=-1 vẫn đi thẳng vào service.create(). Data invalid được persist vào DB. Bug này đặc biệt nguy hiểm vì không có exception, không có log — silent failure.

  2. Thiếu @Validated trên class controller:

    // SAI
    @RestController
    @RequestMapping("/api/products")
    public class ProductController { ... }
    
    // DUNG
    @RestController
    @Validated
    @RequestMapping("/api/products")
    public class ProductController { ... }

    Hậu quả: constraint trên @RequestParam@Min(0) int page, @Min(1) @Max(50) int size — silently skip. Request với page=-999 hoặc size=9999 qua mà không có lỗi 400. Pagination logic nhận giá trị tùy ý, có thể gây query chậm hoặc out-of-memory nếu size rất lớn.

Lưu ý: constraint cascade @Valid List<CategoryRef> trên ProductRequest khai báo đúng — nếu vấn đề (1) được sửa, cascade sẽ hoạt động và validate từng CategoryRef.

Code đúng:

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

  @PostMapping
  public ProductDto create(@Valid @RequestBody ProductRequest req) { ... }

  @GetMapping
  public List<ProductDto> list(
      @RequestParam @Min(0) int page,
      @RequestParam @Min(1) @Max(50) int size
  ) { ... }
}
Q3
Giải thích chính xác điều gì xảy ra bên dưới khi Spring xử lý request POST /orders với body JSON invalid — từ lúc request đến khi trả response 400. Nhắc đến đúng class Java tham gia.

Luồng cụ thể theo thứ tự:

  1. DispatcherServlet nhận request, tìm handler method khớp với POST /orders.
  2. RequestResponseBodyMethodProcessor (một HandlerMethodArgumentResolver) được chọn để resolve parameter @Valid @RequestBody OrderRequest req. Jackson deserialize JSON body thành OrderRequest object.
  3. RequestResponseBodyMethodProcessor phát hiện annotation @Valid trên parameter — gọi DataBinder.validate() với OrderRequest object vừa bind.
  4. Hibernate Validator (implementation của Jakarta Bean Validation) đọc annotation constraint trên field, check từng field. Với JSON invalid, có ít nhất một constraint bị vi phạm.
  5. Hibernate Validator trả danh sách ConstraintViolation. DataBinder gom thành BindingResult chứa list FieldError.
  6. RequestResponseBodyMethodProcessor thấy BindingResult có error nên ném MethodArgumentNotValidException (chứa BindingResult bên trong).
  7. Spring tìm @ExceptionHandler(MethodArgumentNotValidException.class) trong @RestControllerAdvice. Handler đọc ex.getBindingResult().getFieldErrors(), build ProblemDetail 400, trả response JSON.

Method body OrderController.create() không bao giờ được gọi — exception ném tại bước 6, trước khi Spring forward vào method.

Q4
Sự khác biệt thực tế giữa @Valid (Jakarta) và @Validated (Spring) là gì? Khi nào dùng cái nào?

Khác nhau về nguồn gốc và cơ chế:

@Valid là annotation của Jakarta Bean Validation (spec chuẩn). Tác dụng: trigger DataBinder.validate() trên parameter, hoặc đánh dấu field cần cascade. Không hỗ trợ validation group.

@Validated là annotation của Spring Framework. Đặt ở class-level để Spring tạo AOP proxy (CGLIB) bao quanh class. Proxy intercept mọi method call, gọi MethodValidationInterceptor trước khi chuyển tiếp. Hỗ trợ validation group — @Validated(Create.class).

Exception khác nhau: @Valid trên @RequestBodyMethodArgumentNotValidException; @Validated AOP → ConstraintViolationException. Cần handler riêng cho mỗi loại trong @RestControllerAdvice.

Quy tắc thực tế:

  • @Valid trên @RequestBody parameter — trigger validation cho JSON body.
  • @Valid trên field trong DTO — cascade vào nested object.
  • @Validated trên controller class — bật method-level validation, cần cho constraint trên @RequestParam/@PathVariable.
  • @Validated(Group.class) trên parameter — khi cần validation group (POST vs PUT rule khác nhau).

Kết quả: nhiều code dùng cả hai cùng lúc — @Validated trên class để bật infrastructure, @Valid trên parameter để trigger/cascade.

Q5
DTO ShipmentRequest dưới có field packages là list nested object. Constraint nào trên PackageItem sẽ được validate và constraint nào bị skip? Tại sao? Sửa để validate đúng.
public record ShipmentRequest(
  @NotBlank String trackingNumber,
  @Future LocalDate estimatedArrival,
  @Size(min = 1, max = 50) List<PackageItem> packages
) {}

public record PackageItem(
  @NotBlank String description,
  @Positive double weightKg,
  @Min(1) int quantity
) {}

Câu trả lời: Không constraint nào trên PackageItem được validate.

ShipmentRequest@Size(min=1, max=50) trên packages — constraint này chỉ check kích thước của list (số phần tử từ 1 đến 50). Nó không đi vào từng PackageItem để check constraint bên trong.

Để Hibernate Validator cascade vào từng phần tử của list và validate @NotBlank description, @Positive weightKg, @Min(1) quantity, cần thêm @Valid trên field packages:

public record ShipmentRequest(
  @NotBlank String trackingNumber,
  @Future LocalDate estimatedArrival,
  @Size(min = 1, max = 50)
  @Valid                          // them @Valid de cascade
  List<PackageItem> packages
) {}

Với @Valid thêm vào, Hibernate Validator sẽ: (1) check @Size(min=1,max=50) trên list — đúng số phần tử, và (2) với mỗi phần tử PackageItem trong list, check @NotBlank, @Positive, @Min(1).

Hậu quả của thiếu @Valid: list có thể chứa package với description="", weightKg=-5 — qua validation mà không bị bắt. Data corrupt đi vào DB hoặc downstream service.

Nguyên tắc nhớ: @Size/@NotEmpty trên collection chỉ check collection itself (length). @Valid thêm vào mới activate cascade vào từng element.

Bài tiếp theo: Validation groups, custom constraint & i18n

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên