背景
JPA specification查詢使用Pageable進行分頁查詢盐类,返回的Page對象會查詢數據的總數用于計算總頁數。這種方式就存在弊端,用戶每點擊一頁都會執(zhí)行一次總數查詢在跳。在數據庫數據過多的情況下枪萄,這種查詢方式通常會伴隨著慢查詢。
優(yōu)化方案
通常過大的數據就沒必要去計算總頁數猫妙。對于用戶來說去查詢最后一頁的數據并沒有多大意義瓷翻。用戶可以查詢采用精確查詢,比如根據手機號碼查詢指定用戶的信息割坠。前端分頁可以采用向前翻和向后翻的方式齐帚。后端只需要根據LIMIT查詢的結果,返回給前端是否存在下一頁數據即可韭脊。
通過查看源碼童谒,我們發(fā)現(xiàn)JPA并沒有提供傳遞Pageable對象,返回List集合的方法沪羔。接下來我們考慮一下是不是有辦法把Specification的總數查詢給禁用掉饥伊,然后實現(xiàn)原生查詢 SELECT * FROM xxx LIMIT N, M的功能。
public interface JpaSpecificationExecutor<T> {
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
}
JPA源碼分析
首先看一下JPA是怎么實現(xiàn)總數查詢的蔫饰,查看SimpleJpaRepository的findAll實現(xiàn)琅豆。
/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
*
* @param spec
* @param pageable
* @return
*/
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
// 根據復雜條件獲取TypedQuery
TypedQuery<T> query = getQuery(spec, pageable);
// pageable為空則執(zhí)行查詢獲取查詢結果
// pageable不為空則執(zhí)行readPage
return pageable == null ? new PageImpl<T>(query.getResultList())
: readPage(query, getDomainClass(), pageable, spec);
}
這部分的邏輯很簡單,主要是根據復雜條件查詢參數獲取TypedQuery對象篓吁。最后根據是否指定分頁參數來決定返回的結果茫因。由于我們這里關注的是分頁情況下的總數查詢,那么接著看readPage方法杖剪。
/**
* Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and
* {@link Specification}.
*
* @param query must not be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param spec can be {@literal null}.
* @param pageable can be {@literal null}.
* @return
*/
protected <S extends T> Page<S> readPage(TypedQuery<S> query, final Class<S> domainClass, Pageable pageable,
final Specification<S> spec) {
// 設置LIMIT參數
query.setFirstResult(pageable.getOffset());
// 設置每頁數量
query.setMaxResults(pageable.getPageSize());
// 分別執(zhí)行數據查詢和總數查詢冻押,封裝成Page對象
return PageableExecutionUtils.getPage(query.getResultList(), pageable, new TotalSupplier() {
@Override
public long get() {
return executeCountQuery(getCountQuery(spec, domainClass));
}
});
}
此時,我們已經發(fā)現(xiàn)了JPA默認查詢總數的具體地方盛嘿,那么假如我們不需要這個總數查詢洛巢,只需要將該方法通過子類重寫就可以了。以下為具體的代碼實現(xiàn)次兆。
/**
* 分頁查詢稿茉,不查詢總數
**/
@Repository
public class CriteriaNoCountRepository {
/**
* 注入JPA實體管理器用于自定義Repository的初始化
*/
@PersistenceContext
protected EntityManager entityManager;
public <T, ID extends Serializable> Page<T> findAll(Specification<T> spec, Pageable pageable, Class<T> clazz){
// 創(chuàng)建對象
SimpleJpaNoCountRepository<T, ID> noCountRepository = new SimpleJpaNoCountRepository<T, ID>(clazz, entityManager);
// 執(zhí)行查詢方法
return noCountRepository.findAll(spec, pageable);
}
/**
* 創(chuàng)建一個內部類 繼承SimpleJpaRepository,重寫readPage方法
*/
public static class SimpleJpaNoCountRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {
public SimpleJpaNoCountRepository(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
}
/**
* 不對總數據進行查詢
* 根據查詢的結果估算總數用于算出hasNext hasPrevious
*/
@Override
protected <S extends T> Page<S> readPage(TypedQuery<S> query, Class<S> domainClass, Pageable pageable, Specification<S> spec) {
query.setFirstResult(pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
// 分頁總數查詢
List<S> content = query.getResultList();
// 分頁查詢結果小于查詢數 page-> hasNext = false
if (content.size() < pageable.getPageSize()) {
// 查詢結果小于需求數芥炭,說明沒有下一頁
return new PageImpl<S>(content, pageable, (pageable.getPageNumber() + 1) * pageable.getPageSize());
}
// page -> hasNext = true;
return new PageImpl<S>(content, pageable, (pageable.getPageNumber() + 1) * pageable.getPageSize() + 1);
}
}
}
這里的總數計算公式是怎么來的呢漓库?我們這里最終的目的是為了返回正確的hasNext和hasPrevious字段,由于PageImpl類未提供set方法讓我們設置這兩個字段园蝠,而是通過Pageable和count兩個字段算出渺蒿,因此我們需要通過計算一個假的count傳遞進去。
@Override
public boolean hasNext() {
return getNumber() + 1 < getTotalPages();
}
我們接著看一下PageImpl的實現(xiàn)砰琢,可以看到hasNext()方法判斷邏輯很簡單蘸嘶,就是當前頁小于總頁數良瞧,就存在下一頁。
因此我們總數只需要根據當前頁數進行計算
hasNext = true -> count = (pageNumber + 1) * pageSize + 1
hasNext = false -> count = (pageNumber + 1) * pageSize
補充說明
這里計算hasNext的邏輯是根據結果數量和預期每頁數量做的比較训唱。如果查詢小于每頁需要的數量則認為沒有下一頁褥蚯。當總數量是每頁數量的整數倍,并且翻到最后一頁的時候况增,這個判斷邏輯可能會出現(xiàn)誤判赞庶。舉個例子,數據庫中有10條數據澳骤,我們進行分頁查詢每頁查詢兩條歧强,當查詢到第五頁的時候,由于此時返回的也是2條數據为肮,那么根據剛剛的邏輯摊册,hasNext=true。然而再去進行查詢颊艳,已經查詢不到數據了茅特。