Spring Boot JPA仅更新实体指定字段触发@NotNull校验异常如何解决
问题根因
- 你直接将前端传入的未赋值的游离态Company对象存入数据库,未赋值字段默认为null,不仅会触发
@NotNull校验,就算去掉校验也会直接覆盖数据库原有字段为null,本身存在逻辑问题。 - 实体上的
@NotNull默认属于Default校验组,新增、更新操作都会触发,不符合更新时无需校验未修改字段的需求。
方案一:校验分组 + 存量实体字段合并(最稳妥,推荐)
步骤1:定义校验分组接口
区分新增和更新场景的校验规则:
// 新增场景校验组 public interface CreateGroup {} // 更新场景校验组 public interface UpdateGroup {}
步骤2:修改实体校验注解的分组配置
仅将新增时必填的校验规则绑定到CreateGroup,如果字段更新时也需要校验格式(比如邮箱、密码长度),则同时绑定两个分组:
@Entity @Table (name = "companies") @Data @ToString(exclude = "perfumes") @AllArgsConstructor @NoArgsConstructor @Builder public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @NotNull(groups = CreateGroup.class) private String name; @NotNull(groups = CreateGroup.class) @Email(message = "请输入合法的邮箱地址", groups = {CreateGroup.class, UpdateGroup.class}) private String email; @NotNull(groups = CreateGroup.class) @Size(min = 4, max = 14, message = "密码长度需在4-14位之间", groups = {CreateGroup.class, UpdateGroup.class}) private String password; @NotNull(groups = CreateGroup.class) @Enumerated(EnumType.STRING) private Country country; @Singular @OneToMany(mappedBy = "company", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private List<Perfume> perfumes = new ArrayList<>(); }
步骤3:Controller指定更新场景的校验分组
更新接口用@Validated指定仅触发UpdateGroup分组的校验:
@PutMapping("/updateCompany/{id}") @ResponseStatus(HttpStatus.ACCEPTED) public String updateCompany(@PathVariable int id, @RequestBody @Validated(UpdateGroup.class) Company company) throws DoesNotExistException { return admin.updateCompany(id,company); }
步骤4:Service层先查存量实体再合并字段
不要直接保存前端传入的对象,先从数据库查询持久态的存量实体,将前端传入的非空字段覆盖到存量实体上,再保存存量实体,避免原有字段被覆盖为null:
@Override public String updateCompany(int id, Company company) throws DoesNotExistException { Company existingCompany = companyRepository.findById(id) .orElseThrow(() -> new DoesNotExistException(id)); // 逐个判断非空字段覆盖 Optional.ofNullable(company.getName()).ifPresent(existingCompany::setName); Optional.ofNullable(company.getEmail()).ifPresent(existingCompany::setEmail); Optional.ofNullable(company.getPassword()).ifPresent(existingCompany::setPassword); Optional.ofNullable(company.getCountry()).ifPresent(existingCompany::setCountry); companyRepository.saveAndFlush(existingCompany); return existingCompany.getName() + " 更新成功"; }
如果实体字段较多,不想逐个写判断,可以用工具类批量复制非空字段:
// 工具类:获取对象所有值为null的属性名 public static String[] getNullPropertyNames (Object source) { final BeanWrapper src = new BeanWrapperImpl(source); java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); Set<String> nullNames = new HashSet<>(); for(java.beans.PropertyDescriptor pd : pds) { if (src.getPropertyValue(pd.getName()) == null) { nullNames.add(pd.getName()); } } return nullNames.toArray(new String[0]); } // Service层直接调用即可完成非空字段复制 BeanUtils.copyProperties(company, existingCompany, getNullPropertyNames(company));
方案二:专用更新DTO(更安全,生产环境推荐)
不要直接用实体类接收前端参数,单独定义更新场景的DTO,仅保留允许用户修改的字段,避免恶意修改id、关联数据等敏感字段:
@Data public class UpdateCompanyDTO { private String name; @Email(message = "请输入合法的邮箱地址") private String email; @Size(min = 4, max = 14, message = "密码长度需在4-14位之间") private String password; private Country country; }
Controller接收DTO后,后续合并字段、保存的逻辑和方案一一致即可。
方案三:自定义更新语句(适合简单固定的更新场景)
如果更新场景非常固定,也可以直接在Repository层写自定义更新语句,跳过实体校验逻辑:
@Repository public interface CompanyRepository extends JpaRepository<Company, Integer> { @Modifying @Query("update Company c set c.name = :name where c.id = :id") int updateNameById(Integer id, String name); }
内容的提问来源于stack exchange,提问作者Shai Ace
相关产品推荐
相关产品推荐

