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 bepresent 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

