天天看點

jpa分組分頁查詢 傳回總數錯誤解決問題描述原因分析:解決方案:

問題描述

jpa分組分頁查詢之後,傳回page分頁資料錯誤解決方案`

例如:

Specification<User> specification = new Specification<User>() {
    @Override
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
        List<Predicate> predicates = new ArrayList<Predicate>() {
            {
                String id = serachVM.getId();
                criteriaQuery.groupBy(root.get("id"));//避免同個供方出現多次
            }
        };
        return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
    }
};
Page<User> page= Repository.findAll(specification , pageable);

           

原因分析:

此時,分組之後的page總數量為分組的後的每一個count(*)的累加結果,得到的結果比實際數量多,導緻前端顯示出現空頁面,而我們想要的是每一組的資料,總數應該是分了多少組才對,不符合我們的要求。

那麼問題出在哪裡呢?我們看到JPA的findAll辦法

public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) {
     TypedQuery<T> query = this.getQuery(spec, pageable);
     return (Page)(isUnpaged(pageable) ? new PageImpl(query.getResultList()) : this.readPage(query, this.getDomainClass(), pageable, spec));
 }
           

已知為分頁查詢,是以來到readPage

protected <S extends T> Page<S> readPage(TypedQuery<S> query, Class<S> domainClass, Pageable pageable, @Nullable Specification<S> spec) {
    if (pageable.isPaged()) {
        query.setFirstResult((int)pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());
    }

    return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> {
        return executeCountQuery(this.getCountQuery(spec, domainClass));
    });
}
           

前面是一些簡單的指派,再看到executeCountQuery這個方法(重點)

private static long executeCountQuery(TypedQuery<Long> query) {
    Assert.notNull(query, "TypedQuery must not be null!");
    List<Long> totals = query.getResultList();
    long total = 0L;

    Long element;
    for(Iterator var4 = totals.iterator(); var4.hasNext(); total += element == null ? 0L : element) {
        element = (Long)var4.next();
    }

    return total;
}
           

我們可以看到 query.getResultList().size()這個才是我們的資料結果,但是jpa把他進行進行疊代累加導緻資料總數是分組前的總數。是以我們隻需要對此方法進行重寫即可。

解決方案:

建立一個JPASpecialCountErrorHandler抽象類
/**
 * 特殊情況下數量統計異常處理(分頁+分組,切僅用于此種情況)
 * 此類将傳回正确的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;
        }
    }
}
           
将處理改查詢的service實作類繼承JPASpecialCountErrorHandler,并重寫getEm,注入EntityManager ,例如:
public class userServiceImpl extends JPASpecialCountErrorHandler implements userService{
	@PersistenceContext
    private EntityManager entityManager;
    @Override
    protected EntityManager getEm() {
        return entityManager;
    }
}
           
這個使用,讓我們對資料進行查詢的時候,對傳回的page進行二次封裝即可得到正常數量(注意此方法僅用于對資料的分組查詢後處理),該類編寫在繼承JPASpecialCountErrorHandler 的impl類,如上面所訴的userServiceImpl 。
Specification<User> specification = new Specification<User>() {
   @Override
   public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
       List<Predicate> predicates = new ArrayList<Predicate>() {
           {
               String id = serachVM.getId();
               criteriaQuery.groupBy(root.get("id"));//避免同個供方出現多次
           }
       };
       return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
   }
};
Page<User> page= Repository.findAll(specification , pageable);
//pageable為請求的分頁參數 specification為jpa建構查詢條件傳回的實體
//page1中的資料就是最終分組後的正常結果
Page<User>page1=getPage(page.getContent(),pageable,getCount(specification ,User.class));