自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

PageHelper 分頁為什么會失效,含原理分析

數(shù)據(jù)庫 其他數(shù)據(jù)庫
構造 PageInfo 的時候,判斷 List 類型,如果類型是 Page ,也就是我們說的生效的情況,那就能正常的返回分頁信息。如果單純就是個Collection,則分頁信息就按照傳入的這個集合給你返回,這就是為什么在分頁不生效的時候,返回的total就是你傳入的 List 的size。

大家好,我是風箏

作為一個 Java 程序員,想必一定對 MyBatis 非常熟悉,尤其在國內來看,只要是 Java 項目并且涉及到數(shù)據(jù)庫操作,絕大多數(shù)都會使用 MyBatis,或者是 MyBatis 的各個變種。

那在查詢數(shù)據(jù)庫的場景中,分頁是無法避免的,不管前端是按鈕翻頁還是下拉加載,對應到數(shù)據(jù)庫上都是一樣的,都是利用數(shù)據(jù)庫的條數(shù)限制,例如 MySQL 中的 Limit。

而在完成分頁需求時,不知道有多少同學是自己實現(xiàn)的,還有多少同學使用 PageHelper 。剛開始自學 Java 的時候,我都是古法手工擼 SQL 語句,在 Mapper 層傳分頁參數(shù),然后在 SQL 中分頁。直到后來我發(fā)現(xiàn)了 PageHelper ,害,早直到有這家伙,還自己寫啥呀,交給它就完事兒了。

后來的很多項目中都使用它,有從 Spring Boot 最基礎的腳手架從 0 搭建的項目,也有直接使用的成熟腳手架,例如若依,都在使用 PageHelper,從此分頁就變得異常簡單了。

前幾天,有個工作不就的 Java 小哥問我說問題,說是 PageHelper 本來好好的,結果加了幾行代碼,分頁數(shù)據(jù)都失效了。

當他還沒有亮出代碼的時候,我基本上已經猜到問題原因了。倒不是我厲害,恰恰相反,因為我之前很菜的時候也碰到過類似的問題,而且不止一次。也就是菜了一次,沒有吸取教訓,又菜了第二次。直到我研究了一下 PageHelper 的原理,之后才沒有出現(xiàn)類似的問題。

失效原因分析及解決

當小哥給我發(fā)來代碼后,死去的以及開始攻擊我,基本就是當初我寫的代碼的格式,不光是我,我 Google 了一下,出現(xiàn)問題的基本都是這么用的。

我簡化了一下這個邏輯:

  • 設置分頁參數(shù) PageHelper.startPage(1,10);
  • 通過一個 Mapper 查詢出結果集;
  • 通過上一步的結果集構造 PageInfo
  • 這時候,構造出的 PageInfo 是沒問題的。

如果你的業(yè)務比較單純,這樣也就沒問題了,但是有些情況下不是這樣的。

public PageInfo<DataDetailVo> search(String keyword) {
 List<DataDetailVo> voList = new ArrayList<>();
 // 1.設置分頁,第1頁,10條
 PageHelper.startPage(1,10);
 // 2.查詢結果集
 List<DataVo> dataVos = xxxMapper.searchDataList(keyword);

 // 3.通過上一步的結果集構造 PageInfo
 PageInfo<DataVo> pageSuccess = new PageInfo<>(dataVos);
 // 結果是對的
 log.info("pageSuccess:" + JSON.toJSONString(pageSuccess));

 // 4.真實情況,還要對結果集進行加工,將結果集轉變了類型
 for (DataVo dataVo : dataVos) {
  DataDetailVo vo = new DataDetailVo();
  BeanUtils.copyProperties(dataVo, vo);
  voList.add(vo);
 }

 // 5.這時候,通過新的結果集構造 PageInfo,分頁信息就是錯誤的
 PageInfo<DataDetailVo> pageFail = new PageInfo<>(voList);
 log.info("pageFail:" + JSON.toJSONString(pageFail));
 
 return pageFail;
}
  • 真實情況,還要對結果集進行加工,將結果集轉變了類型;
  • 這時候,通過新的結果集構造 PageInfo,分頁信息就是錯誤的

很多時候會像第4步那樣,對初始結果集進行進一步再加工,而這些加工的數(shù)據(jù)沒辦法通過 SQL 直接獲取到,或者用 SQL 獲取代價太大。

甚至有時候會像上面的代碼那樣,從數(shù)據(jù)庫查詢出來的實體類型和實際返回給調方的實體類型都不一樣。有的同學說,難道就不能在 mapper 層直接返回需要類型嗎?當然可以,不過很多時候不可能都這么完美。

問題原因

原因很明顯,稍有經驗的同學可能已經看出來了,就是因為第5步構造 PageInfo 時使用了一個新的 List,才導致分頁失效的。

這只是表現(xiàn)出來的原因,但是 Mapper 查出來的是一個 List(dataVos),經過加工的也是 List(voList),怎么就一個正常,一個不正常呢,難道這兩個 List 有什么不一樣的嗎?還是 PageHelper 只認第一個 List?

下面介紹原理的時候再說這個問題。

解決方式

解決這個問題也很簡單。

方式1:不要加工了嘛,mapper 返回啥,就直接給調用方返回啥。

也不是不可以,你要是產品經理+老板的話,可以直接改需求,讓需求來適應代碼,但是基本上行不通;

方式2:前面也說了,直接讓 mapper 返回最終返回給調用方的類型,不要在加工的時候生成新的 List 了。

這種也可以,但是改動可能比較大,因為有的 Mapper 層的方法是供很多其他方法調用的,Mapper 層基本上只需要返回最通用的類型。不能為了某個方法調用方,而讓其他調用方也做出改變。

當然了,你可以為這種特殊的需求新加一個 Mapper 方法,只是比較麻煩而已。

方式3:在構造 PageInfo 的時候稍加修改就可以了

只需要將原本構造錯誤的 PageInfo

PageInfo<DataDetailVo> pageFail = new PageInfo<>(voList);
log.info("pageFail:" + JSON.toJSONString(pageFail));

改為下面這樣既可,還是用 Mapper 層返回的dataVos 集合來構造 PageInfo,只不過稍后將加工后的新的List 賦值給 PageInfo 的 list 屬性即可。

PageInfo pageSuccess2 = new PageInfo<>(dataVos);
pageSuccess2.setList(voList);

原理分析

前面查找原因的時候提到這樣一個問題:Mapper 查出來的是一個 List(dataVos),經過加工的也是 List(voList),怎么就一個正常,一個不正常呢,難道這兩個 List 有什么不一樣的嗎?

我們就順著這個問題思考就可以了,我先說結論,這倆 List 確實不一樣,確切的說,Mapper 查出來的那個 List 是被 PageHelper 包裝后的List,再確切的說是 PageHelper 里的 Page 對象。

PageHelper.startPage(1,10);
// 2.查詢結果集
List<DataVo> dataVos = xxxMapper.searchDataList(keyword);

通過調試代碼可以看出來,dataVos 就是一個披著 List 外衣的 Page 對象,你可以直接在這個對象上調用 Page 中的方法,比如 getTotal(),可以直接返回數(shù)量的。

圖片圖片

而你自己加工后的集合,就真的是個單純的 ArrayList 了,所以在使用 PageInfo 構造分頁對象的時候,是絕對不可能獲取到真實的分頁參數(shù)的,比如總條數(shù)、總頁數(shù)等。

簡要概括一下這個過程,不過多解釋源碼,整個流程大致如下。

圖片圖片

通過 ThreadLocal 存儲 Page 初始參數(shù)

首先通過代碼 PageHelper.startPage(1,10);設置分頁參數(shù),這個過程很簡單,就是初始化 Page 對象,然后存到 ThreadLocal 中。

關于 ThreadLocal 可以參考我之前的一篇文章 我還是不懂 ThreadLocal,不要沒標題迷惑,看完就懂了。

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
 Page<E> page = new Page<E>(pageNum, pageSize, count);
 page.setReasonable(reasonable);
 page.setPageSizeZero(pageSizeZero);
 //當已經執(zhí)行過orderBy的時候
 Page<E> oldPage = getLocalPage();
 if (oldPage != null && oldPage.isOrderByOnly()) {
  page.setOrderBy(oldPage.getOrderBy());
 }
 setLocalPage(page);
 return page;
}

其中 setLocalPage(page)就是像 ThreadLocal 中存 Page 對象,一會兒還有地方用到它。

利用 MyBatis 攔截器機制

然后就是利用了 MyBatis 的攔截器機制,攔截器主要做兩件事,第一件就是在查詢數(shù)據(jù)集合前先count一下,把數(shù)量查出來。第二件就是將查詢出來的數(shù)據(jù)集包裝成 Page 對象,當然了 Page 是繼承自 ArrayList 的,要不然它也不能偽裝的這么好。

在 PageHelper 源碼中有 PageInterceptor.java這個攔截器,主要是里面的 intercept 方法。這里面就是實現(xiàn)核心邏輯的主戰(zhàn)場。

public Object intercept(Invocation invocation) throws Throwable {
 try {
  // ...
  List resultList;
  //調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
  if (!dialect.skip(ms, parameter, rowBounds)) {
   //判斷是否需要進行 count 查詢
   if (dialect.beforeCount(ms, parameter, rowBounds)) {
    //查詢總數(shù)
    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
    //處理查詢總數(shù),返回 true 時繼續(xù)分頁查詢,false 時直接返回
    if (!dialect.afterCount(count, parameter, rowBounds)) {
     //當查詢總數(shù)為 0 時,直接返回空的結果
     return dialect.afterPage(new ArrayList(), parameter, rowBounds);
    }
   }
   resultList = ExecutorUtil.pageQuery(dialect, executor,
     ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
  } else {
   //rowBounds用參數(shù)值,不使用分頁插件處理時,仍然支持默認的內存分頁
   resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
  }
  // 處理加工后的結果集
  return dialect.afterPage(resultList, parameter, rowBounds);
 }
}

先判斷是否需要進行分頁,如果不需要,直接返回結果。也就是這行代碼,你可以點進去看一下 skip 這個方法,就是獲取 ThreadLocal 中的Page對象,看是不是存在,是不是有分頁參數(shù),有的話就是需要分頁,沒有就直接按照正常的查詢走了。

if (!dialect.skip(ms, parameter, rowBounds))

如果需要分頁的話,先查詢一下數(shù)量。

Long count = count(executor, ms, parameter, rowBounds, null, boundSql);

然后根據(jù)分頁參數(shù),查詢分頁結果集。

resultList = ExecutorUtil.pageQuery(dialect, executor,
     ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);

之后對結果集加工并返回。

return dialect.afterPage(resultList, parameter, rowBounds);

最終加工成 Page 的方法,看到沒,還是先從 ThreadLocal中拿,然后將原始結果集放進去。

public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
 Page page = getLocalPage();
 if (page == null) {
  return pageList;
 }
 page.addAll(pageList);
 if (!page.isCount()) {
  page.setTotal(-1);
 } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
  page.setTotal(pageList.size());
 } else if (page.isOrderByOnly()) {
  page.setTotal(pageList.size());
 }
 return page;
}

最后 PageInfo 構造

最后,構造 PageInfo 的時候,判斷 List 類型,如果類型是 Page ,也就是我們說的生效的情況,那就能正常的返回分頁信息。如果單純就是個Collection,則分頁信息就按照傳入的這個集合給你返回,這就是為什么在分頁不生效的時候,返回的total就是你傳入的 List 的size。

public PageInfo(List<? extends T> list, int navigatePages) {
        super(list);
        if (list instanceof Page) {
            Page page = (Page) list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();

            this.pages = page.getPages();
            this.size = page.size();
            //由于結果是>startRow的,所以實際的需要+1
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                //計算實際的endRow(最后一頁的時候特殊)
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) {
            this.pageNum = 1;
            this.pageSize = list.size();

            this.pages = this.pageSize > 0 ? 1 : 0;
            this.size = list.size();
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }
        if (list instanceof Collection) {
            calcByNavigatePages(navigatePages);
        }
    }

怎么樣,學廢了嗎?

責任編輯:武曉燕 來源: 古時的風箏
相關推薦

2022-04-13 20:53:15

Spring事務管理

2019-11-25 16:05:20

MybatisPageHelpeJava

2022-09-20 22:27:08

事務失效public 修飾

2020-12-11 08:02:16

索引MySQL存儲

2023-01-17 09:13:08

Mybatis后端框架

2022-03-02 10:11:41

索引場景數(shù)據(jù)庫

2020-04-27 07:13:37

Nginx底層進程

2018-05-09 09:55:36

數(shù)據(jù)分析

2020-10-27 14:15:42

SpringBoot

2025-03-10 07:10:00

2023-03-22 09:10:18

IT文檔語言

2014-03-05 14:58:00

蘋果CarPlayiOS

2015-12-07 10:49:43

卸載App用戶體驗

2022-05-11 08:22:54

IO負載NFSOS

2021-01-25 07:14:53

Cloud DevOps云計算

2012-08-17 10:01:07

云計算

2020-03-30 15:05:46

Kafka消息數(shù)據(jù)

2012-03-26 10:26:43

openstackeucalyptus

2012-05-02 10:08:51

桌面Linux微軟

2021-07-09 09:24:06

NanoID UUID軟件開發(fā)
點贊
收藏

51CTO技術棧公眾號