問題描述
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));