`

使用spring boot jpa+分组+分页后,count页数不对的解决办法

阅读更多

1.错误案例重现

首先来看博主想做什么:博主想group一张表,且对分组后的条目进行分页

Page<Specification> page = repository.findAll(MatrixModel.createSpecification(model),
 MatrixModel.createSort(pageNum, pageSize, model.getSort()));

ok,主要还是用的repository的finalAll方法,鉴于本人是java鉴定的OOP拥护者,所以很少写sql,那么我其中的一个方法MatrixModel.createSpecification(model),其实就是创建了一堆spec,后面的createSort不用看就知道了创建了pageAble对象,符合findAll方法的参数需求。

 

楼主在确认参数没有任何问题的情况下,发现了这样一个现象:


这是第一页的数据,发现分组之后居然有1662个Elements,这显然是不对的。

Then,当我把单页的条目数扩展到555之后(至于为什么是这个数,看到后面就知道了)



 OK当页条目数是554,总条目数也是554,相当于我一页取完了所有的分组之后的对象。

那么在任何调用都没有问题的情况下,为什么会出现total计算的偏差呢?下面是分析过程。

2.分析sql

博主的当页数据是没有问题的,所以楼主重点想看JPA中的count语句。我按照上面顺序复现了并且打印了hql

顺序如下

2.1 单页数目为10

Hibernate: select specificat0_.id as id1_27_, specificat0_.create_time as create_t2_27_, specificat0_.update_time as update_t3_27_, specificat0_.head_img as head_img4_27_, specificat0_.intro as intro5_27_, specificat0_.name as name6_27_, specificat0_.specifications_id as specific7_27_, specificat0_.specifications_name as specific8_27_, specificat0_.status as status9_27_, specificat0_.weight as weight10_27_ from _specification specificat0_ where specificat0_.status=0 group by specificat0_.specifications_id order by specificat0_.weight desc limit ?

 

Hibernate: select count(specificat0_.id) as col_0_0_ from _specification specificat0_ where specificat0_.status=0 group by specificat0_.specifications_id

 

2.2 单页数目为555(刚好大于分组后所有元素的总和)

Hibernate: select specificat0_.id as id1_27_, specificat0_.create_time as create_t2_27_, specificat0_.update_time as update_t3_27_, specificat0_.head_img as head_img4_27_, specificat0_.intro as intro5_27_, specificat0_.name as name6_27_, specificat0_.specifications_id as specific7_27_, specificat0_.specifications_name as specific8_27_, specificat0_.status as status9_27_, specificat0_.weight as weight10_27_ from _specification specificat0_ where specificat0_.status=0 group by specificat0_.specifications_id order by specificat0_.weight desc limit ?

 

2.2 比较结果

敲他大爷,为什么第一种结果回去多查询一次Hibernate: select count(specificat0_.id) as col_0_0_ from _specification specificat0_ where specificat0_.status=0 group by specificat0_.specifications_id

 

而且这样的查询是sql中典型的group + count(*)的错误查询



 

如图所示,这样会查询出每个group之后的条目数。而我们需要的是总的group数目

所以正确查询应该嵌套查询:



 如图所示554 才应该是group之后的条目数。

所以我有疑问:难道TM的SPRING JPA 没有对group的判定?(敲他吗,还真没有)

 

3.不说了,看源码

先去找到findAll方法的实现

 继续前进



 

ok,来到spring JPA实现,可以看到那句return是一个重要的action,已知我们的pageale不为null,那么继续

进入方法readPage


 

getPage一定干了一些坏事,进去瞅瞅(还有个匿名内部类的实现哟,等会比较重要)



 仔细检查了逻辑,没有任何问题(主要是对查询出来的当页数量和设定的页面数量做比较,这是常规操作,spring也不会犯这样的问题),最后的猫腻在哪里呢?看到那个get方法没有,结果前面的情况二,总数的计算在这里有问题---》这里回到上面那个匿名内部内的实现,total是采用的:SimpleJpaRepository.executeCountQuery(SimpleJpaRepository.this.getCountQuery(spec, domainClass)).longValue();

 

咋们去看看这个executeCountQuery方法:

 


 

破案了,曹,totals是我们需要的总条目数554,但是spring把所有的数目累加了,相当于分组之前的总数了,不知道这算不算issue,但是目前是不符合我们的要求的。

 

4.解决方案

我的构想是能拿到那个条目数,并且能完美使用specRepo(OOP),所以不能使用sql来实现。

 

package com.swb.dreamweaver.dao;


import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.Collections;
import java.util.List;

/**
 * 特殊情况下数量统计异常处理(分页+分组,切仅用于此种情况)
 * 此类将返回正确的count数量
 * Author: Liao Ke
 */
public abstract class JPASpecialCountErrorHandler {

    /**
     * 获取EntityManager实现
     * @return
     */
    protected abstract EntityManager getEm();


    /**
     * 获取分页数据
     * @param content 当前页面条目数
     * @param pageable 分页信息
     * @param count 真实总数
     * @return
     */
    protected <S> PageImpl getPage(List<S> content, Pageable pageable,int count){
       return new PageImpl(content, pageable, (long)count);
    }

    /**
     * 获取正确的count计数
     * @param spec
     * @param domainClass
     * @param <S>
     * @return
     */
    protected <S> int getCount(org.springframework.data.jpa.domain.Specification<S> spec, Class<S> domainClass) {
        return getCountQuery(spec, domainClass).getResultList().size();
    }

    /**
     * 来自源码,获取TypedQuery
     * org\springframework\data\jpa\repository\support\SimpleJpaRepository.class
     * @param spec
     * @param domainClass
     * @param <S>
     * @return
     */
    protected <S> TypedQuery<Long> getCountQuery(org.springframework.data.jpa.domain.Specification<S> spec, Class<S> domainClass) {
        CriteriaBuilder builder = getEm().getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);
        Root<S> root = this.applySpecificationToCriteria(spec, domainClass, (CriteriaQuery<S>) query);
        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            query.select(builder.count(root));
        }
        query.orderBy(Collections.emptyList());
        return getEm().createQuery(query);
    }

    /**
     * 来自源码,翻译spec
     * org\springframework\data\jpa\repository\support\SimpleJpaRepository.class
     * @param spec
     * @param domainClass
     * @param query
     * @param <S>
     * @return
     */
    private <S> Root<S> applySpecificationToCriteria(org.springframework.data.jpa.domain.Specification<S> spec, Class<S> domainClass, CriteriaQuery<S> query) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        Assert.notNull(query, "CriteriaQuery must not be null!");
        Root<S> root = query.from(domainClass);
        if (spec == null) {
            return root;
        } else {
            CriteriaBuilder builder = getEm().getCriteriaBuilder();
            Predicate predicate = spec.toPredicate(root, query, builder);
            if (predicate != null) {
                query.where(predicate);
            }
            return root;
        }
    }
}

写了个抽象类,然后仿写了源码。直接使用使用list.size()可以获取到我想要的分组后的group条目数(开心)。

 

 

最后附上使用的代码:

1.继承上面的抽象类

public class SpecificationService extends JPASpecialCountErrorHandler

2.注入em

@PersistenceContext
private EntityManager entityManager;


3.重写了getEm方法

@Override
protected EntityManager getEm() {
    return entityManager;
}


 4.简单的group判断及对pageView的重写

@Override
public <S extends EntityCURD> Code.ViewPage<S> gets(MatrixModel model, Integer pageNum, Integer pageSize) {
    Page<Specification> page = repository.findAll(MatrixModel.createSpecification(model), MatrixModel.createSort(pageNum, pageSize, model.getSort()));
//JPA ISSUE -- 分组+分页,总数显示不正确
if (model.getGroup() != null && model.getGroup().size() != 0) {
        return (Code.ViewPage<S>) code.JpaPageToViewPage(getPage(page.getContent(), MatrixModel.createSort(pageNum, pageSize, model.getSort()), getCount(MatrixModel.createSpecification(model), Specification.class)));
}
    return (Code.ViewPage<S>) code.JpaPageToViewPage(page);
}

 

OK 结果确认:

"last": false, "totalElements": 554, "totalPages": 51, "number": 0, "size": 11, "first": true, "numberOfElements": 11

没有问题

 

 

 

  • 大小: 9.1 KB
  • 大小: 14.2 KB
  • 大小: 19.8 KB
  • 大小: 24.1 KB
  • 大小: 13 KB
  • 大小: 12.3 KB
  • 大小: 16.4 KB
  • 大小: 24.2 KB
  • 大小: 15.5 KB
  • 大小: 4.1 KB
  • 大小: 6.2 KB
2
0
分享到:
评论
1 楼 CodeMonkey_Xiong 2019-07-01  
困扰了好久的问题,终于在这里找到了答案! 
回来感谢下博主,多谢你的分享!
佩服你探索问题的精神!!!![color=indigo][/color]

相关推荐

Global site tag (gtag.js) - Google Analytics