JPA分页查询总数计算错误问题
问题背景:
上一篇博客(JPA排序中的两个问题)中有提到一个场景:对象A中包含了Set<B> objBs这样一个成员,要求查询A时按A中包含的B数量排序,当时的解决方案是count一下B,然后order by这个count,详见上一篇博客。
上面的方法确实解决了排序的问题,但是经过多次测试发现,当按照objBs排序、查询第一页时,数据正确,但是total不对,实际上我有11条数据,但是total返回的是17。
数据库中:
问题定位:
代码调用的是JPA的默认实现SimpleJpaRepository中的
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
TypedQuery<T> query = getQuery(spec, pageable);
return pageable == null ? new PageImpl<T>(query.getResultList())
: readPage(query, getDomainClass(), pageable, spec);
}
过程可以自己看源码,这里列出出现问题的核心代码:
private static Long executeCountQuery(TypedQuery<Long> query) {
Assert.notNull(query, "TypedQuery must not be null!");
List<Long> totals = query.getResultList();
Long total = 0L;
for (Long element : totals) {
total += element == null ? 0 : element;
}
return total;
}
咱们来看这个逻辑:根据getResultList查询出的结果,将里面的元素累加后作为总数返回。
问题就在这里!
通过打印sql语句,发现,当用usedCount(即一开始提到的B对象的个数)作为排序条件时,系统根据查询的目标表自动生成query的语句为:
select
count(filterrule0_.id) as col_0_0_
from
filter_rule filterrule0_
left join
v_filter_rule_used filterrule1_
on filterrule0_.id=filterrule1_.filter_rule_id
where
1=1
group by
filterrule0_.id
查询结果为:
加起来是不是正好17?
但是以其他字段来排序时没有这个问题,来看看此时计算count的查询语句:
select
count(filterrule0_.id) as col_0_0_
from
filter_rule filterrule0_
where
1=1
查询结果为:
此时累加是没问题的。
解决方法:
不知道这算不算spring的bug,当然也可以通过写sql语句来解决,不过本人作为OOP的忠实拥护者,既然用了JPA又岂能接受sql语句?
使用的方案就是写一个JPA的实现类继承SimpleJpaRepository来重写findAll方法。
step 1:写一个interface继承JPA,并加注解@NoRepositoryBean:
@NoRepositoryBean
public interface JpaRepositoryExt<T, ID extends Serializable> extends JpaRepository<T, Serializable>, JpaSpecificationExecutor<T> {
}
step2:写一个实现类来实现step1的接口,并继承SimpleJpaRepository:
public class BaspJpaRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, Serializable>
implements JpaRepositoryExt<T, Serializable>
里面实现findAll,然后修改自己要修改的部分,这里就是前面提到的计数方法,思路是:想办法获取到本次查询是否有join,如果没有,那就按原来的方式累加计数,如果有,就取resultList的size()即可。
判断是否有join,经过多次debug发现,用usedCount作为排序字段时,getCountQuery这个方法里面,调用Root<S> root = applySpecificationToCriteria后,能从root里获取到一个getJoins(),这里会有join的表,所以在这里可以做下记录,定义一个成员来保存这个结果,后面计数的时候去判断:
protected <S extends T> TypedQuery<Long> getCountQuery(Specification<S> spec, Class<S> domainClass) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
if (CollectionUtils.isNotEmpty(root.getJoins())) {
this.hasJoin = true;
}
else {
this.hasJoin = false;
}
后面省略...
计数方法executeCountQuery里的代码段:
List<Long> totals = query.getResultList();
if (hasJoin) {
return null == totals ? 0 : (long)totals.size();
}
Long total = 0L;
for (Long element : totals) {
total += element == null ? 0 : element;
}
return total;
step3:编写一个BeanFactory来继承JpaRepositoryFactoryBean,重新实现获取目标实现类的方法即可,这里代码太长只贴一部分主要的::
public class BaspRepositoryFactoryBean<R extends JpaRepository<T, I>, T,
I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {
@Override
protected Object getTargetRepository(RepositoryInformation information) {
return new BaspJpaRepositoryImpl<T, I>((Class<T>) information.getDomainType(), em);
}
//设置具体的实现类的class
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return BaspJpaRepositoryImpl.class;
}
}
step4:spring启动类前面修改一下,添加一个注解:
@EnableJpaRepositories(repositoryFactoryBeanClass = BaspRepositoryFactoryBean.class)
step5:业务继承step1定义的repository接口,这跟平时写一个继承JpaRepository的接口没有区别,然后在业务中autowired这个接口即可:
public interface XXJpaRepositoryExt
extends JpaRepositoryExt<XX, String>, JpaSpecificationExecutor<XX> {
}
上一篇: VSCode—搭建python的编译环境
下一篇: JPA 5.映射关联关系