JPA Specification禁用總數查詢

背景

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。然而再去進行查詢颊艳,已經查詢不到數據了茅特。

參考資料

https://stackoverflow.com/questions/26738199/how-to-disable-count-when-specification-and-pageable-are-used-together

https://gist.github.com/tcollins/0ebd1dfa78028ecdef0b

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者棋枕。
  • 序言:七十年代末白修,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子重斑,更是在濱河造成了極大的恐慌兵睛,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窥浪,死亡現(xiàn)場離奇詭異祖很,居然都是意外死亡,警方通過查閱死者的電腦和手機漾脂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門突琳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人符相,你說我怎么就攤上這事〈懒眨” “怎么了啊终?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長傲须。 經常有香客問我蓝牲,道長,這世上最難降的妖魔是什么泰讽? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任例衍,我火速辦了婚禮昔期,結果婚禮上,老公的妹妹穿的比我還像新娘佛玄。我一直安慰自己硼一,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布梦抢。 她就那樣靜靜地躺著般贼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奥吩。 梳的紋絲不亂的頭發(fā)上哼蛆,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音霞赫,去河邊找鬼腮介。 笑死,一個胖子當著我的面吹牛端衰,可吹牛的內容都是我干的叠洗。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼靴迫,長吁一口氣:“原來是場噩夢啊……” “哼惕味!你這毒婦竟也來了?” 一聲冷哼從身側響起玉锌,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤名挥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后主守,有當地人在樹林里發(fā)現(xiàn)了一具尸體禀倔,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年参淫,在試婚紗的時候發(fā)現(xiàn)自己被綠了救湖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡涎才,死狀恐怖鞋既,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情耍铜,我是刑警寧澤邑闺,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站棕兼,受9級特大地震影響陡舅,放射性物質發(fā)生泄漏。R本人自食惡果不足惜伴挚,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一靶衍、第九天 我趴在偏房一處隱蔽的房頂上張望灾炭。 院中可真熱鬧,春花似錦颅眶、人聲如沸蜈出。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掏缎。三九已至,卻和暖如春煤杀,著一層夾襖步出監(jiān)牢的瞬間眷蜈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人间校。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像忌怎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子酪夷,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350