You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot JPA仅更新实体指定字段触发@NotNull校验异常如何解决

问题根因

  1. 你直接将前端传入的未赋值的游离态Company对象存入数据库,未赋值字段默认为null,不仅会触发@NotNull校验,就算去掉校验也会直接覆盖数据库原有字段为null,本身存在逻辑问题。
  2. 实体上的@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 22:36:04