使用JPA时,报错Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
出现问题的场景参考文章:SpringBoot系列教程JPA之delete使用姿势详解
在两个线程中的异步方法里,都调用delete方法删除同一条数据,而这条数据在库中是存在的,大概率会出现该异常。
原因 JPA在delete某条数据时,实际上是按照条件先select这条数据,然后再delete这条数据by主键id。即先根据条件find出该条数据对应的主键id,然后deleteById删除该条数据。
源码中,JPA实际上调用SimpleJpaRepository中的deleteById()方法。
@Transactionalpublic void deleteById(ID id) {Assert.notNull(id, ID_MUST_NOT_BE_NULL);delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));}@Transactionalpublic void delete(T entity) {Assert.notNull(entity, "The entity must not be null!");em.remove(em.contains(entity) ? entity : em.merge(entity));}
推测出现该问题的原因是两个线程都delete,根据JPA的原理,相当于四条语句,两条查询两条删除。而JPA是用不同的队列存不同的操作,当两个线程均先执行select然后再delete时,第二个delete删除时,原本查出来一条数据,应该删除这条数据,但实际上删除了0条数据,因为该条数据已经被第一个delete删除了,导致预期和实际不匹配,会抛出该异常:Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1。
如下图所示,若是两个线程先select完了,再delete就会报错,如果两个线程按顺序一个先删一个后删,就不会有错误。
本来想在执行delete后,flush一下,但是并没有用。即使flush,如图第一种情况还是回报错。
解决方法使用原生sql语句执行,不使用jpa就可以了。
@Transactional@Modifying@Query(value = "delete from user where name =:name ",nativeQuery = true)void deleteByName(@Param("name") String name);