欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JPA分页查询总数计算错误问题

程序员文章站 2024-03-13 17:53:33
...

问题背景:

    上一篇博客(JPA排序中的两个问题)中有提到一个场景:对象A中包含了Set<B> objBs这样一个成员,要求查询A时按A中包含的B数量排序,当时的解决方案是count一下B,然后order by这个count,详见上一篇博客。

    上面的方法确实解决了排序的问题,但是经过多次测试发现,当按照objBs排序、查询第一页时,数据正确,但是total不对,实际上我有11条数据,但是total返回的是17。

JPA分页查询总数计算错误问题

数据库中:

JPA分页查询总数计算错误问题

问题定位:

代码调用的是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

查询结果为:

JPA分页查询总数计算错误问题

加起来是不是正好17?

但是以其他字段来排序时没有这个问题,来看看此时计算count的查询语句:

select
        count(filterrule0_.id) as col_0_0_ 
    from
        filter_rule filterrule0_ 
    where
        1=1

查询结果为:

JPA分页查询总数计算错误问题

此时累加是没问题的。

 

解决方法:

    不知道这算不算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> {
}

 

相关标签: JPA