Spring Boot中DTO应选用值对象还是Bean Validation?
Spring Boot中DTO应选用值对象还是Bean Validation?
我之前做项目的时候也碰到过一模一样的困扰——重复的Bean Validation注解写得人头疼,后期改个规则还要找遍所有用到的DTO,简直是维护噩梦。你想改用值对象的思路太对了,至于你担心的“值对象不能为null”的问题,其实不用被这个刻板定义框死,咱们来捋清楚:
首先,值对象的核心本质是“确保自身存在时是有效的”,而不是“强制必须存在”。传统说法里的“值对象不能为null”,更多是指值对象内部的状态不能无效,而不是说DTO里的Value Object引用必须非空。在你的Patch更新场景里,null完全可以表示“不需要修改这个字段”,和值对象的核心原则不冲突。
接下来给你具体的落地方案,保证简洁又符合最佳实践:
第一步:把校验逻辑封装到值对象里
把每个字段的校验规则都集中到对应的Value Record中,以后改规则只需要动这一处:
// Name值对象:确保只要传入就符合校验规则 public record Name( @NotBlank(message = "The name is a required field.") @Length(max = 255, message = "The name cannot be longer than 255 characters.") @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.") String value ) {} // 银行账号值对象 public record BankAccountNumber( @NotBlank(message = "The giro account is a required field.") @Pattern(regexp = "^555-[0-9]{3}-[0-9]{8}-[0-9]{2}$", message = "Bank account number must be in the format 555-YYY-ZZZZZZZZ-WW") @Size(min = 19, max = 19, message = "Bank account number must be exactly 19 characters long") String value ) {} // Fax值对象:本身允许空值,所以校验规则里已经包含了可选逻辑 public record Fax( @Pattern(regexp = "^(\\d{3}/\\d{3}-\\d{3})?$", message = "Fax number must be in the format XXX/YYY-ZZZ (e.g., 123/456-789)") String value ) {}
第二步:用值对象重构你的DTO
现在DTO就变得异常简洁,而且逻辑清晰:
// 创建请求:核心字段必须非null,确保创建时数据完整有效 public record BankCreateRequest( @NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax ) {} // 更新请求:所有字段都可以为null,表示不更新该字段;如果传入值,值对象会自动校验有效性 public record BankUpdateRequest( Name name, BankAccountNumber bankAccountNumber, Fax fax ) {}
为什么这个方案可行?
- 完全符合DRY原则:所有校验逻辑只写一次,避免了重复代码,维护成本骤降;
- 数据有效性有保障:只要值对象被实例化(也就是DTO里的字段不为null),就一定是符合规则的;
- 适配Patch场景:null语义明确,就是“不修改该字段”,和业务逻辑完美匹配。
当然也有其他替代方案,比如用Validation Groups区分创建和更新的校验规则,但还是会存在注解重复的问题;或者用工具类做转换,但复杂度比这个方案高多了。你想的这个值对象方案已经是最简洁高效的了,完全不用担心违反最佳实践。
备注:内容来源于stack exchange,提问作者aleks
相关产品推荐
相关产品推荐

