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

Spring Data JPA+Hibernate6:更新不存在记录时save()为何执行插入?

问题场景与需求

实体与Repository定义

Test实体代码:

@Entity
@Table(name = "TEST")
@lombok.Data
public class Test { 
    
    @Id
    @jakarta.persistence.GeneratedValue(generator = "test_id_gen_key")
    @org.hibernate.annotations.GenericGenerator(name="test_id_gen_key", type=org.hibernate.id.IdentityGenerator.class)
    @Column(name="id", nullable = false)
    private Integer id;
    
    @Column(name="name", nullable = true, length = 10)
    private String name;
    
}

对应的TestRepository:

@Repository
public interface TestRepository extends JpaRepository<Test, Integer> {

}

当前问题

在Service中执行以下更新操作时:

var test = new Test(10, "any name");  // id=10在数据库中不存在 
testRepository.save(test);

Hibernate会执行merge()逻辑:先根据id查询数据库,无结果则插入新记录。但期望当更新的实体不存在时抛出异常,而非自动插入。

查看CrudRepository#save方法的注释:

/**

  • Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
  • entity instance completely.
  • @param entity must not be {@literal null}.
  • @return the saved entity; will never be {@literal null}.
  • @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
  • @throws OptimisticLockingFailureException when the entity uses optimistic locking and has a version attribute with
  • a different value from that found in the persistence store. **Also thrown if the entity is assumed to be
    
  • present but does not exist in the database**.
    

*/
S save(S entity);

但该注释中提到的异常仅在实体带有@Version乐观锁字段时才会触发,当前实体无该字段,因此save方法不会抛出异常。

尝试过的方案(存在缺陷)

曾扩展SimpleJpaRepository实现自定义update方法,调用Hibernate的Session#update():

@SuppressWarnings("deprecation")
@Override
public T update(T entity) {     
    session().update(entity);
    return entity;
}

但Session#update()已过时,并非最优方案。

最优处理方式

方案1:Service层先校验存在性,再更新

这是最直观且符合Spring Data规范的方式:先通过findById查询实体,不存在则抛出异常,存在则更新属性后保存。

// 在TestService中
public Test updateTest(Integer id, String newName) {
    Test existingTest = testRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Test实体不存在,id: " + id));
    existingTest.setName(newName);
    return testRepository.save(existingTest);
}

优点:逻辑清晰,易于维护,完全符合需求,无过时API风险。

方案2:自定义Repository方法,使用JPQL更新并校验影响行数

在TestRepository中定义自定义更新方法,通过JPQL执行更新,返回受影响的行数,若行数为0则抛出异常:

@Repository
public interface TestRepository extends JpaRepository<Test, Integer> {
    @Modifying
    @Query("update Test t set t.name = :name where t.id = :id")
    int updateNameById(@Param("id") Integer id, @Param("name") String name);
}

然后在Service中调用:

public void updateTestName(Integer id, String newName) {
    int affectedRows = testRepository.updateNameById(id, newName);
    if (affectedRows == 0) {
        throw new EntityNotFoundException("Test实体不存在,id: " + id);
    }
}

优点:无需查询实体直接执行更新,性能更优,适合只更新部分字段的场景。

方案3:使用getReferenceById触发异常

Spring Data的getReferenceById方法会返回实体的懒加载代理,当后续对代理进行操作时,若实体不存在则会抛出EntityNotFoundException:

public Test updateTest(Integer id, String newName) {
    Test existingTest = testRepository.getReferenceById(id);
    existingTest.setName(newName); // 此处若实体不存在会抛出异常
    return testRepository.save(existingTest);
}

注意:部分JPA实现中,getReferenceById不会立即查询数据库,而是在首次访问实体属性时才触发查询并校验存在性。

内容的提问来源于stack exchange,提问作者neo xu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 01:53:10