分享自研實現的多資料來源(支援同DB不同表、跨DB表、記憶體資料、外部系統資料等)分頁查詢工具類實現原理及使用

夢在旅途發表於2022-03-13

思考:

提起分頁查詢,想必任何一個開發人員(不論是新手還是老手)都能快速編碼實現,實現原理再簡單不過,無非就是寫一條SELECT查詢的SQL語句,ORDER BY分頁排序的欄位, 再結合limit (頁碼-1),每頁記錄數,這樣即可返回指定頁碼的分頁記錄,類似SQL如下所示:

select * from table where 查詢條件 order by id limit 100,100; -- 這裡假設是第2頁(limit 第1個值從0開始),每頁100條

那如果是想將多張表的記錄合併一起進行分頁查詢,我們又該如何實現呢?我估計稍微有點經驗的開發人員可能會立馬舉一反三,想到了通過UNION 多張表的方式來實現分頁查詢,類似SQL如下所示:

select * from
(select id,col1,col2,col3 from table1 where 查詢條件
union all
select id,cola as col1,colb as col2,colc as col3 from table2 where 查詢條件
) as t
order by t.id limit 100,100; -- 這裡假設是第2頁(limit 第1個值從0開始),每頁100條

這樣實現有沒有問題呢?我覺得如果是UNION的多張小表(即:資料量相對較小)那麼這樣實現成本最低最有效果,肯定是OK的,但如果UNION的多張表是一些大表(即:資料量相對較大,如:100W+)且有些表的查詢條件中的查詢欄位不一定是有索引的,那麼就會存在嚴重的查詢效能問題,另外如果UNION的表過多,即使不都是大表也仍然存在查詢效能問題,而且查詢效能隨著UNION的表的數量增加查詢效能而降低,導致無法擴充套件。

​ 這裡有人可能會說,分頁查詢一般都是單表或JOIN多表的結果集,即使UNION多張表也不會太多,為何要考慮擴充套件?我只能說,一切皆有可能,誰也沒有規定分頁查詢只能單表或限定在幾張表內,如果產品經理提出需要將多個功能模組(對於開發人員來說:可能是多張表)的資料合併分頁查詢展示,那我們也必需實現,斷然不能因為“實現不了 或 實現有難度 或 存在效能問題”就拒絕此類需求,因為產品經理提出的需求肯定有他的背景及業務價值,作為開發人員,且想做為一個優秀的開發人員,那麼“有求必應”是必備的工作態度,豪不誇張的張,沒有實現不了的產品需求,就看實現的成本(包含時間成本、人力成本、物質成本等)是否與產品需求的價值相匹配,如果成本與價值基本相符(或說投入與產出後的效果),那麼即使再難實現也必定是可以實現的。扯得有點遠了,還是回到上面所描述的關於多張表分頁查詢的問題,UNION多張表確實可以解決一些相對簡單的多表分頁的問題,但如果多張表的資料欄位結構、記錄數不相同(即:欄位名不同、一對多、單行水平欄位、垂直多行欄位),甚至不僅僅是多張表,有可能是跨系統、跨DB的多張表或是動態計算的結果,這些情況下,UNION SQL的方式肯定是滿足不了了,必需要有其它的解決辦法,我認為最好的實現方式有兩種:一種是想辦法將多查詢來源(因為不僅限於表)的記錄全部彙總到一張總表上,然後使用傳統的單表分頁查詢SQL即可(正如一開始所舉例的SQL),另一種就是本文重點介紹的,支援多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)

多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)介紹

多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)的使用前提條件是:多個查詢來源(不僅限於表)必需是有順序的,即:先查第1個來源,查完後再查下一個來源,依此類推,直至查完所有來源,分頁結束,如:表1,表2,表3,先分頁查表1,查完後再查表2,查完後最後查表3。

多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)的使用效果:多個查詢來源(不僅限於表)能夠正常記錄總頁數,總記錄數,能夠支援正常連續分頁,跳轉分頁,且只要不是最後1頁,則每頁的記錄數均為設定的頁大小(即:pageSize,滿頁),若上一個查詢來源的記錄數不足頁大小則會自動切換下一個查詢來源來補足1頁大小的記錄,否則最後1頁才有可能剩餘記錄不足1頁大小的情況(即:與傳統單表分頁查詢效果一致),整體對前端,對使用者無差異感知。

多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)的實現原理與機制:

  1. 先通過彙總計算每個查詢來源的總記錄數,然後根據每個查詢來源的總記錄數精確計算出分頁區間佔比情況(即:pageRange),分頁區間的關鍵資訊有:開始區間頁碼、結束區間頁碼、所屬查詢來源、開始頁實際記錄數、結束頁記錄數(注意:結束頁記錄數是累加的,目的是便於計算下一個查詢來源的分頁區間),最後得出真實的總頁數、總記錄數;(對應程式碼的方法:getTotalCountResult),下面通過一個表格來展示分頁區間的計算情況:

    假設:pageSize:每頁2條

    如下每一單元格代表一行記錄數,單元格中的數字表示分頁數字,不同顏色區分不同的查詢來源

  2. 分頁查詢時,根據前端使用者選擇的查詢頁碼、查詢來源(這個首次不傳則為預設0,後面若跨查詢來源則會由後端返回給前端,前端儲存)、分頁大小、分頁區間(這個由後端計算後返回給前端儲存)等入參資訊(MultiSourcePagination),先由頁碼獲得分頁區間物件列表(不足1頁跨多查詢來源時會有多個查詢來源,否則一般都只會命中一個分頁區間),選擇第1個分頁區間物件,若這個分頁區間的查詢來源與當前請求的查詢來源相同說明是正常的分頁,則執行正常分頁邏輯;若不相同時說明存在跳頁情況,則再判斷當前查詢的頁碼是否為這個分頁區間對應的的開始頁碼,若是說明無需分隔點,則僅需切換查詢來源及設定查詢來源的分頁超始頁碼後執行正常分頁邏輯,否則說明跳頁且當前查詢的頁碼在這個查詢來源的第2頁及後面的分頁區間內(含最末頁)或分頁區間開始頁存在跨多個查詢來源(即:多個查詢來源補足1頁記錄,如:表1佔10條,表2佔10條,頁大小為20條),此時就需要先根據分頁區間的開始頁記錄數及查詢條件查出對應的補頁記錄資訊,然後獲取結果的最後一條記錄作為這個查詢來源的分頁過濾條件(注意:若查詢補頁記錄後的資料來源與當前原請求的分頁區間的資料來源不相同時,則說明資料有變化(資料條數變少或沒有,導致切換下一個查詢來源),此時應重新彙總計算分頁資訊,以便再翻頁時能準確查詢到資料),最後執行正常分頁邏輯(對應程式碼的方法:getPageQueryResult)

  3. 正常分頁邏輯(對應程式碼的方法:doPageQueryFromMultiSource):根據請求的查詢來源索引從已設定的多資料來源分頁查詢回撥方法列表中選擇對應的分頁查詢回撥方法引用,執行分頁查詢回撥方法獲得分頁的結果,若結果記錄滿足頁大小(即:實際記錄數=頁大小pageSize)則正常返回即可,否則需判斷是否為最後一個查詢來源,若是則說明已到最大頁碼,直接返回當前剩餘記錄即可,無需補充分頁記錄的情況,除外則說明查詢的結果為空或記錄數不滿1頁大小,需要跨查詢來源進行補頁查詢(即:缺少幾條記錄就查多少記錄補全1頁大小,如:頁大小20,表1查詢出8條,不足1頁還差12條,則切換查表2查詢12條補全1頁),注意可能存在跨多個查詢來源才補全1頁大小的情況,最後在返回分頁結果時,需將補頁記錄的最後一條記錄設定為查詢來源的分頁過濾條件(querySourceFilterStart)、當前請求頁碼設定為這個查詢來源的分頁起始頁碼(即:已佔用的頁碼,querySourcePageStart)一併返回給前端即可。後續翻頁時前端除了更改頁碼外還需將上述分頁區間資訊、分頁過濾條件、分頁起始頁碼等回傳給後端,以避免每次都要後端重新計算 影響查詢效能或因分頁入參資訊不全不準導致分頁結果不正確的情況;

    下面通過表格圖來展示幾種情況下的多資料來源的分頁情況

    其中:pageLimitStart=(this.page【請求的頁碼】 - this.querySourcePageStart【起始頁碼】 - 1) * this.pageSize【頁大小】;

    第一種情況:無論是正常分頁(即:連續分頁)或是跳頁分頁(即:隨機頁碼翻頁)均不存在補頁情況(即:同1頁中包含多個查詢來源的資料),最為簡單,每個查詢來源均正常分頁查詢即可(limit pageLimitStart,pageSize),跳頁時僅需確認查詢來源、分頁起始頁碼即可;

第二種情況:無論是正常分頁(即:連續分頁)或是跳頁分頁(即:隨機頁碼翻頁)均需要補頁情況,由於涉及補頁的情況,故跳頁時也分兩種情況,如果在已執行過的查詢來源的分頁區間中進行跳頁(情形1),那麼僅需確定查詢來源、分頁起始頁碼即可,而如果從一個已執行過的查詢來源跳到未執行過的查詢來源(情形2),那麼此時因為存在補頁故必需先查詢這個查詢來源的分頁區間起始頁補頁記錄資訊從而確定分隔過濾條件及分頁起始頁碼;

​ 第三種情況:與上面第二種情況一下,無論是正常分頁(即:連續分頁)或是跳頁分頁(即:隨機頁碼翻頁)均需要補頁情況,但補頁涉及多個查詢來源;

總之:不論哪種情況,如果某個查詢來源不足1頁大小時,必需由另一個或多個查詢來源的記錄補全1頁,一旦存在補頁,那麼補頁的最後查詢來源後面的頁碼記錄均需要排除掉補頁的記錄(這也就是為什麼跳頁時,需要先查分頁區間的起始頁的補頁記錄並確認分隔點過濾條件的目的),即:需確認分隔過濾條件;

多資料來源分頁查詢工具類(MultiSourcePageQueryBuilder)程式碼快速上手示例指南:

示例1:(這裡採用的是純記憶體模擬資料,其實也說明了支援不同型別的查詢來源,不論是DB的表或記憶體中的集合物件 、甚至是調外部系統的介面,只要能符合分頁的出入參欄位即可,混合也是可以的)

@RunWith(JUnit4.class)
public class MultiSourcePageQueryBuilderTests {

    @Test
    public void testPageQuery() {

        //構建3張虛擬表的記錄(假設現在有3張表)
        final List<ATable> table1 = new ArrayList<>();
        table1.add(new ATable(1, "zs", new Timestamp(System.currentTimeMillis()), 202112));
        table1.add(new ATable(2, "zs2", new Timestamp(System.currentTimeMillis()), 202110));
        table1.add(new ATable(3, "zs3", new Timestamp(System.currentTimeMillis()), 202201));
        table1.add(new ATable(4, "zs4", new Timestamp(System.currentTimeMillis()), 202202));
        table1.add(new ATable(5, "zs5", new Timestamp(System.currentTimeMillis()), 202203));


        final List<ATable> table2 = new ArrayList<>();
        table2.add(new ATable(1, "ls", new Timestamp(System.currentTimeMillis()), 202111));
        table2.add(new ATable(2, "ls2", new Timestamp(System.currentTimeMillis()), 202112));
        table2.add(new ATable(3, "ls3", new Timestamp(System.currentTimeMillis()), 202202));
        table2.add(new ATable(4, "ls4", new Timestamp(System.currentTimeMillis()), 202202));
        table2.add(new ATable(5, "ls5", new Timestamp(System.currentTimeMillis()), 202203));

        final List<ATable> table3 = new ArrayList<>();
        table3.add(new ATable(11, "ww", new Timestamp(System.currentTimeMillis()), 202111));
        table3.add(new ATable(22, "ww2", new Timestamp(System.currentTimeMillis()), 202112));
        table3.add(new ATable(33, "ww3", new Timestamp(System.currentTimeMillis()), 202203));
        table3.add(new ATable(44, "ww4", new Timestamp(System.currentTimeMillis()), 202202));
        table3.add(new ATable(55, "ww5", new Timestamp(System.currentTimeMillis()), 202203));


        MultiSourcePageQueryBuilder<ATable,ATable> pageQueryBuilder = new MultiSourcePageQueryBuilder<>();
        pageQueryBuilder.addCountQuerySources(pagination -> {
            //這裡僅為演示,現實是查表1 SQL COUNT
            return table1.stream().count();
        }).addCountQuerySources(pagination -> {
            //這裡僅為演示,現實是查表2 SQL COUNT
            return table2.stream().count();
        }).addCountQuerySources(pagination -> {
            //這裡僅為演示,現實是查表3 SQL COUNT
            return table3.stream().count();

            //如果COUNT與實際分頁分開,則可以在不同的地方按需進行組合,但注意:若同時存在addCountQuerySources、 addPageQuerySources,則他們必需配對(即:count與pageQuery的集合索引一致)
        }).addPageQuerySources(pagination -> {
            //這裡僅為演示,現實是查表1 分頁SQL(基於limit分頁)
            return doPageQuery(pagination, table1);

        }).addPageQuerySources(pagination -> {
            //這裡僅為演示,現實是查表2 分頁SQL(基於limit分頁)
            return doPageQuery(pagination, table2);
        }).addPageQuerySources(pagination -> {
            //這裡僅為演示,現實是查表3 分頁SQL(基於limit分頁)
            return doPageQuery(pagination, table3);
        });

        MultiSourcePagination<ATable,ATable> pagination = new MultiSourcePagination<>();
        pagination.setPageSize(7);
        pagination.setPage(1);
        pagination.setQueryCriteria(new GenericBO<ATable>());
        MultiSourcePagination<ATable,ATable> paginationResult = pageQueryBuilder.getTotalCountResult(pagination);
        System.out.println("total result:" + JsonUtils.deserializer(paginationResult));


        while (true) {
            paginationResult = pageQueryBuilder.getPageQueryResult(pagination);
            if (paginationResult == null || CollectionUtils.isEmpty(paginationResult.getRows())) {
                break;
            }
            System.out.printf("page:%d, list:%s, %n", paginationResult.getPage(), JsonUtils.deserializer(paginationResult));
            //因為是模擬測試,每次的結果必需清除掉
            paginationResult.setRows(null);
            paginationResult.setPage(paginationResult.getPage() + 1);//模擬跳頁
        }

        System.out.printf("page end:%d %n", paginationResult.getPage());

        Assert.assertEquals(3,paginationResult.getPageTotal());

    }

    private List<ATable> doPageQuery(MultiSourcePagination<ATable,ATable> pagination, List<ATable> tableX) {
        if (pagination.getLimitRowCount() > 0) {
            //補充分頁(無分隔點)
            return tableX.stream()
                    .sorted((o1, o2) -> (o1.inMonth >= o2.inMonth && o1.id > o2.id) ? 1 : ((o1.inMonth == o2.inMonth && o1.id == o2.id) ? 0 : -1))
                    .limit(pagination.getLimitRowCount()).collect(Collectors.toList());
        } else if (pagination.getQuerySourceFilterStart() != null) {
            //正常分頁(有分隔點)
            return tableX.stream()
                    .filter(t -> t.id > pagination.getQuerySourceFilterStart().getId() && t.inMonth >= pagination.getQuerySourceFilterStart().getInMonth())
                    .sorted((o1, o2) -> (o1.inMonth >= o2.inMonth && o1.id > o2.id) ? 1 : ((o1.inMonth == o2.inMonth && o1.id == o2.id) ? 0 : -1))
                    .skip(pagination.getPageLimitStart()).limit(pagination.getPageSize()).collect(Collectors.toList());
        } else {
            //正常分頁
            return tableX.stream()
                    .sorted((o1, o2) -> (o1.inMonth >= o2.inMonth && o1.id > o2.id) ? 1 : ((o1.inMonth == o2.inMonth && o1.id == o2.id) ? 0 : -1))
                    .skip(pagination.getPageLimitStart()).limit(pagination.getPageSize()).collect(Collectors.toList());
        }
    }


    private static class ATable {
        private int id;
        private String name;
        private Timestamp updateDate;
        private long inMonth;

        public ATable(int id, String name, Timestamp updateDate, long inMonth) {
            this.id = id;
            this.name = name;
            this.updateDate = updateDate;
            this.inMonth = inMonth;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Timestamp getUpdateDate() {
            return updateDate;
        }

        public void setUpdateDate(Timestamp updateDate) {
            this.updateDate = updateDate;
        }

        public long getInMonth() {
            return inMonth;
        }

        public void setInMonth(long inMonth) {
            this.inMonth = inMonth;
        }
    }
}

示例2:(mybatis spring boot)

相關mapper xml的SQL定義片段:

<!-- AMapper.xml sql:-->

    <select id="selectCount" resultType="long" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select count(1) from tableA a inner join tableB b on a.b_id=b.id
        inner join tableC c on b.id=c.b_id
        where a.enabled_flag=1 and b.enabled_flag=1 and c.enabled_flag=1
        and b.in_month=#{queryCriteria.vo.inMonth} and c.un_receive_fee_amount>0
    </select>

    <select id="pageLimitQuery" resultType="cn.zuowenjun.model.AUnPaidInfo" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select b.in_month as 'inMonth',b.id as 'bizRowId',a.bill_number as 'bizNumber',c.un_receive_fee_amount as 'unPaidAmount','M' as 'bizType'
       from tableA a inner join tableB b on a.b_id=b.id
        inner join tableC c on b.id=c.b_id
        where a.enabled_flag=1 and b.enabled_flag=1 and c.enabled_flag=1
        and b.in_month=#{queryCriteria.vo.inMonth} and c.un_receive_fee_amount>0

        <if test="querySourceFilterStart!=null">
            <![CDATA[
               and b.id > #{querySourceFilterStart.bizRowId} and b.in_month>=#{querySourceFilterStart.inMonth}
           ]]>
        </if>
        order by b.in_month,b.id asc
        <choose>
            <when test="limitRowCount>0">
                limit #{limitRowCount}
            </when>
            <otherwise>
                limit #{pageLimitStart},#{pageSize}
            </otherwise>
        </choose>

    </select>

<!-- BMapper.xml sql:-->

<select id="selectCount" resultType="long" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select count(1)
        from tableAA a inner join tableBB b on a.b_id=b.id
        inner join tableCC c on b.id=c.b_id
        where a.enabled_flag=1 and b.enabled_flag=1 and c.enabled_flag=1
        and b.in_month=#{queryCriteria.vo.inMonth} and c.un_receive_fee_amount>0
    </select>

    <select id="pageLimitQuery" resultType="cn.zuowenjun.model.AUnPaidInfo" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select b.in_month as 'inMonth',b.id as 'bizRowId',a.waybill_number as 'bizNumber',c.un_receive_fee_amount as 'unPaidAmount','P' as 'bizType'
         from tableAA a inner join tableBB b on a.b_id=b.id
        inner join tableCC c on b.id=c.b_id
        where a.enabled_flag=1 and b.enabled_flag=1 and c.enabled_flag=1
        and b.in_month=#{queryCriteria.vo.inMonth} and c.un_receive_fee_amount>0

        <if test="querySourceFilterStart!=null">
            <![CDATA[
               and b.id > #{querySourceFilterStart.bizRowId} and b.in_month>=#{querySourceFilterStart.inMonth}
           ]]>
        </if>
        order by b.in_month,b.id asc
        <choose>
            <when test="limitRowCount>0">
                limit #{limitRowCount}
            </when>
            <otherwise>
                limit #{pageLimitStart},#{pageSize}
            </otherwise>
        </choose>

    </select>


<!-- CMapper.xml sql:-->

    <select id="selectCount" resultType="long" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select count(1) from tableC where uncollect_amount>0 and enabled_flag=1 and in_month=#{queryCriteria.vo.inMonth}
    </select>

    <select id="pageLimitQuery" resultType="cn.zuowenjun.model.AUnPaidInfo" parameterType="cn.zuowenjun.model.MultiSourcePagination">
        select in_month as 'inMonth',id as 'bizRowId',bill_number as 'bizNumber',uncollect_amount as 'unPaidAmount','O' as 'bizType'
        from tableC where uncollect_amount>0 and enabled_flag=1 and in_month=#{queryCriteria.vo.inMonth}
        <if test="querySourceFilterStart!=null">
            <![CDATA[
               and id > #{querySourceFilterStart.bizRowId} and in_month>=#{querySourceFilterStart.inMonth}
           ]]>
        </if>
        order by in_month,id asc
        <choose>
            <when test="limitRowCount>0">
                limit #{limitRowCount}
            </when>
            <otherwise>
                limit #{pageLimitStart},#{pageSize}
            </otherwise>
        </choose>

    </select>

JAVA程式碼片段:


//前置工作:

// AMapper.java、BMapper.java、CMapper.java 3個mapper 介面類中均定義如下兩個方法
	//計算總記錄數(命名不固定)
	Long selectCount(MultiSourcePagination<AUnPaidInfo,AUnPaidInfo> pagination);
    //分頁查詢(命名不固定)
	List<AUnPaidInfo> pageLimitQuery(MultiSourcePagination<AUnPaidInfo,AUnPaidInfo> pagination);


//對應的AService.java 、BService.java、CService.java 均定一個如下示例的獲取上述mapper的方法,當然也可以照上面的mapper方法在對應的Service類中定義對應的方法,內部仍然直接調mapper介面類的方法(實際是mapper proxy的代理方法)
	private AMapper aMapper=null;
    public BillMonthlyService(@Autowired AMapper aMapper) {
        this.aMapper=aMapper;
    }

    public BillMonthlyMapper getMapper() {
        return aMapper;
    }

//真正的分頁使用:(這裡使用介面)
@RestController
public class TestController {
    @Autowired
    private AService aService;
    @Autowired
    private BService bService;
    @Autowired
    private CService cService;

    @ApiOperation("測試多資料來源分頁查詢")
    @RequestMapping(value = "/test/pageQueryUnPaids",method = RequestMethod.POST)
    public MultiSourcePagination<AUnPaidInfo,AUnPaidInfo> pageQueryUnPaids(@RequestBody MultiSourcePagination<AUnPaidInfo,AUnPaidInfo> request){

        if (request==null || request.getQueryCriteria()==null || request.getQueryCriteria().getVo()==null){
            throw new RuntimeException("入參不能為空!");
        }

        MultiSourcePageQueryBuilder<AUnPaidInfo,AUnPaidInfo> pageQueryBuilder=new MultiSourcePageQueryBuilder<>();
       //addCountQuerySources、addPageQuerySources 是支援鏈式呼叫,為了便於
        pageQueryBuilder.addCountQuerySources(r->aService.getMapper().selectCount(r))
                .addPageQuerySources(r->aService.getMapper().pageLimitQuery(r))

                .addCountQuerySources(r->bService.getMapper().selectCount(r))
                .addPageQuerySources(r->bService.getMapper().pageLimitQuery(r))

                .addCountQuerySources(r->cService.getMapper().selectCount(r))
                .addPageQuerySources(r->cService.getMapper().pageLimitQuery(r));

        return pageQueryBuilder.getPageQueryResult(request);
    }
}


//出參資訊物件(這裡也作為入參對明)
public class AUnPaidInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    private String inMonth;
    private String bizType;
    private String bizNumber;
    private Double unPaidAmount;
    private Long bizRowId;

    public String getInMonth() {
        return inMonth;
    }

    public void setInMonth(String inMonth) {
        this.inMonth = inMonth;
    }

    public String getBizType() {
        return bizType;
    }

    public void setBizType(String bizType) {
        this.bizType = bizType;
    }

    public String getBizNumber() {
        return bizNumber;
    }

    public void setBizNumber(String bizNumber) {
        this.bizNumber = bizNumber;
    }

    public Double getUnPaidAmount() {
        return unPaidAmount;
    }

    public void setUnPaidAmount(Double unPaidAmount) {
        this.unPaidAmount = unPaidAmount;
    }

    public Long getBizRowId() {
        return bizRowId;
    }

    public void setBizRowId(Long bizRowId) {
        this.bizRowId = bizRowId;
    }
}

好了,最後就貼出MultiSourcePageQueryBuilder 原始碼,其實理解了多資料來源分頁的原理後寫程式碼還是很簡單的。

public class MultiSourcePageQueryBuilder<T,B> {
    private final List<Function<MultiSourcePagination<T,B>, Long>> countQuerySourceFuncList = new ArrayList<>();
    private final List<Function<MultiSourcePagination<T,B>, List<T>>> pageQuerySourceFuncList = new ArrayList<>();

    /**
     * 新增需要進行多查詢來源(即:多表)計算總記錄數的回撥方法,同時支援一次性寫多個也可以鏈式新增
     *
     * @param countQuerySourceFuncArr count SQL對應的service方法,SQL類似如下:
     *                                <pre>
     *                                 select count(1) from table where 查詢條件
     *                                               </pre>
     * @return
     */
    @SafeVarargs
    public final MultiSourcePageQueryBuilder<T,B> addCountQuerySources(Function<MultiSourcePagination<T,B>, Long>... countQuerySourceFuncArr) {
        Assert.notEmpty(countQuerySourceFuncArr, "請指定需要計算總記錄數的回撥方法【每個查詢來源1個方法】!");
        this.countQuerySourceFuncList.addAll(Arrays.asList(countQuerySourceFuncArr));
        return this;
    }

    /**
     * 新增需要進行多查詢來源(即:多表)分頁查詢的回撥方法,同時支援一次性寫多個也可以鏈式新增
     *
     * @param pageQuerySourceFuncArr 分頁查詢(含補充查詢) SQL對應的service方法,SQL類似如下:(假設按in_month,id排序分頁)
     *                               <pre>
     *                                  <select id="pageLimitQuery">
     *                                     select * from tableX where enabled_flag=1 and 查詢條件...
     *                                     <if test="querySourceFilterStart!=null">
     *                                         <![CDATA[
     *                                         and id > #{querySourceFilterStart.id} and in_month>=#{querySourceFilterStart.inMonth}
     *                                         ]]>
     *                                     </if>
     *                                     order by in_month,id asc
     *                                     <choose>
     *                                         <when test="limitRowCount>0">
     *                                             limit #{limitRowCount}
     *                                         </when>
     *                                         <otherwise>
     *                                             limit #{pageLimitStart},#{pageSize}
     *                                         </otherwise>
     *                                     </choose>
     *                                  </select>
     *                                 </pre>
     * @return
     */
    @SafeVarargs
    public final MultiSourcePageQueryBuilder<T,B> addPageQuerySources(Function<MultiSourcePagination<T,B>, List<T>>... pageQuerySourceFuncArr) {
        this.pageQuerySourceFuncList.addAll(Arrays.asList(pageQuerySourceFuncArr));
        return this;
    }


    /**
     * 獲取最終合併計算的總記錄數、總頁數結果資訊
     *
     * @param paginationRequest
     * @return
     */
    public final MultiSourcePagination<T,B> getTotalCountResult(MultiSourcePagination<T,B> paginationRequest) {
        Assert.notEmpty(countQuerySourceFuncList, "請指定需要計算總記錄數的回撥方法【每個查詢來源1個方法】!");
        paginationRequest.setPageRanges(new ArrayList<>());
        paginationRequest.setRowTotal(0);
        paginationRequest.setPageTotal(0);
        for (int i = 0; i < countQuerySourceFuncList.size(); i++) {
            Function<MultiSourcePagination<T,B>, Long> countQuerySourceFunc = countQuerySourceFuncList.get(i);
            MultiSourcePagination.SourcePageRange sourcePageRange = null;
            int rowTotalCount = countQuerySourceFunc.apply(paginationRequest).intValue();
            if (rowTotalCount == 0) {
                continue;
            }

            if (CollectionUtils.isEmpty(paginationRequest.getPageRanges())) {
                //如果是第1個有記錄的查詢來源,即開始
                if (rowTotalCount <= paginationRequest.getPageSize()) {
                    //如果總記錄數不足1頁,直接設定頁區間範圍
                    sourcePageRange = new MultiSourcePagination.SourcePageRange(1, 1, i, rowTotalCount, rowTotalCount);
                } else {
                    //否則正常計算總頁數及剩餘頁的記錄數
                    int pageTotal = (rowTotalCount / paginationRequest.getPageSize()) + (rowTotalCount % paginationRequest.getPageSize() > 0 ? 1 : 0);
                    int remainEndPageSize = rowTotalCount - (rowTotalCount / paginationRequest.getPageSize()) * paginationRequest.getPageSize();
                    sourcePageRange = new MultiSourcePagination.SourcePageRange(1, 1 + pageTotal - 1, i, paginationRequest.getPageSize(), remainEndPageSize>0?remainEndPageSize:paginationRequest.getPageSize());
                }
            } else {
                //獲取上一個查詢來源的分頁區間資訊
                MultiSourcePagination.SourcePageRange preSourcePageRange = paginationRequest.getPageRanges().get(paginationRequest.getPageRanges().size() - 1);
                //補頁記錄
                int mergeSize = paginationRequest.getPageSize() - preSourcePageRange.getEndPageSize();
                //剩餘可分頁記錄(減去補頁記錄)
                int remainSize = rowTotalCount - mergeSize;
                //整數頁數
                int fullPageCount =0;
                //餘頁記錄數(不足1頁的記錄)
                int remainEndPageSize=0;
                //總頁數
                int pageTotal=0;
                //開始頁的實際條數(如果有補頁,則實際補頁記錄為開始頁的條數,否則記錄數超過1頁,則為頁大小,否則實際記錄數【不足1頁】)
                int beginPageSize = mergeSize > 0 && remainSize > 0 ? mergeSize : (mergeSize == 0 && remainSize >= paginationRequest.getPageSize() ? paginationRequest.getPageSize() : rowTotalCount);
                if (remainSize > 0) {
                    fullPageCount = remainSize / paginationRequest.getPageSize();
                    remainEndPageSize = remainSize - fullPageCount * paginationRequest.getPageSize();
                    pageTotal = fullPageCount + (remainEndPageSize > 0 ? 1 : 0);
                } else {
                    //如果剩餘可分頁記錄數<=0,則說明無法補完或剛好僅補完1頁,則當頁即為最後頁
                    remainEndPageSize = remainSize < 0 ? preSourcePageRange.getEndPageSize() + rowTotalCount : paginationRequest.getPageSize();
                    //無法補完或剛好僅補完1頁時,則此時第1頁的有效記錄數則為實際的記錄
                    beginPageSize = rowTotalCount;
                }
                //開始頁碼
                int beginPage = preSourcePageRange.getEndPage() + 1;
                if (mergeSize > 0) {
                    //如果有補頁記錄,則開始頁與上一個查詢來源結束頁有交集,需設定為上一個查詢來源的結束頁碼
                    beginPage = preSourcePageRange.getEndPage();
                    //有補頁,實際總頁數也得加1
                    pageTotal+=1;
                }

                sourcePageRange = new MultiSourcePagination.SourcePageRange(beginPage, beginPage + pageTotal - 1, i, beginPageSize, remainEndPageSize>0?remainEndPageSize:paginationRequest.getPageSize());
            }

            paginationRequest.setRowTotal(paginationRequest.getRowTotal() + rowTotalCount);
            paginationRequest.getPageRanges().add(sourcePageRange);
        }

        if (paginationRequest.getRowTotal() > 0) {
            //如果有記錄,則總頁數=最後一個查詢來源的頁區間的結束頁碼
            paginationRequest.setPageTotal(paginationRequest.getPageRanges().get(paginationRequest.getPageRanges().size()-1).getEndPage());
        }

        return paginationRequest;
    }

    /**
     * 獲取最終合併分頁的結果資訊
     *
     * @param paginationRequest
     * @return
     */
    public final MultiSourcePagination<T,B> getPageQueryResult(MultiSourcePagination<T,B> paginationRequest) {
        Assert.notEmpty(pageQuerySourceFuncList, "未設定分頁查詢回撥方法,請先通過addPageQuerySources方法進行設定!");
        Assert.notNull(paginationRequest, "查詢條件不能為空!");
        if (paginationRequest.isCount() || paginationRequest.getPageTotal()<=0) {
            //如果需要彙總計算總記錄數、總頁數(含之前沒有彙總計算過),則需先進行彙總計算
            getTotalCountResult(paginationRequest);
        }

        //begin 這個程式碼塊主要是根據當前頁碼確定對應的查詢來源的分頁區間,根據分頁查詢決定如何切換查詢來源及分隔點資訊
        List<MultiSourcePagination.SourcePageRange> currentSourcePageRanges = getCurrentSourcePageRanges(paginationRequest);
        if (!CollectionUtils.isEmpty(currentSourcePageRanges)) {
            //如果查出多個分頁區間,則說明當前頁碼在開始頁或結束頁交集中(若無交集,只會有1條),此時取頁交集頁中的第1查詢來源;若只有1個分頁區間,則正常分頁即可
            MultiSourcePagination.SourcePageRange currentSourcePageRange=currentSourcePageRanges.get(0);
            if (currentSourcePageRange != null && currentSourcePageRange.getSource() != paginationRequest.getQuerySource()) {
                paginationRequest.setQuerySourceFilterStart(null);
                //說明有跳轉頁碼,且跨查詢來源,則需要先根據對應的查詢來源查所在的分頁區間的開始頁
                if (paginationRequest.getPage() == currentSourcePageRange.getBeginPage() || currentSourcePageRange.getBeginPageSize() == paginationRequest.getPageSize()) {
                    //如果是切換查詢來源,但剛好是這個查詢來源分頁區間的第1頁 或這個查詢來源開始頁是整頁(即:不存在補頁),則僅切換查詢來源即可,因為分頁查詢中會正常查詢,不足1頁也會自動切換查詢來源
                    paginationRequest.setQuerySource(currentSourcePageRange.getSource());
                    paginationRequest.setQuerySourcePageStart(currentSourcePageRange.getBeginPage() - (currentSourcePageRange.getBeginPageSize() == paginationRequest.getPageSize() ? 1 : 0));
                } else {
                    //如果是切換查詢來源,且頁碼在這個查詢來源的第2頁及後面的分頁區間內(含最末頁)或1頁跨多個查詢來源,則必需先查詢這個來源的分頁區間的開始頁碼資料,以便確定跨來源的分隔點
                    queryBeginPageBySource(paginationRequest, currentSourcePageRange);
                }
            }
        }
        // end

        return doPageQueryFromMultiSource(paginationRequest);
    }

    private void queryBeginPageBySource(MultiSourcePagination<T,B> paginationRequest, MultiSourcePagination.SourcePageRange sourcePageRange) {
        MultiSourcePagination<T,B> newPagination = new MultiSourcePagination<>();
        newPagination.setPageRanges(paginationRequest.getPageRanges());
        newPagination.setLimitRowCount(sourcePageRange.getBeginPageSize());
        newPagination.setPageSize(sourcePageRange.getBeginPageSize());
        newPagination.setQuerySource(sourcePageRange.getSource());
        newPagination.setQueryCriteria(paginationRequest.getQueryCriteria());

        //獲取當前查詢來源的分頁區間的起始頁資訊(僅補頁時需要),以便獲得分頁的條件過濾起點、頁碼起點等
        //類似執行SQL:select * from table2 where 查詢條件 order by 分頁排序欄位 limit #{LimitRowCount}
        MultiSourcePagination<T,B> paginationResponse = doPageQueryFromMultiSource(newPagination);

        paginationRequest.setQuerySource(paginationResponse.getQuerySource());
        paginationRequest.setQuerySourcePageStart(sourcePageRange.getBeginPage() - (sourcePageRange.getBeginPageSize() == paginationRequest.getPageSize() ? 1 : 0));

        if (CollectionUtils.isEmpty(paginationResponse.getRows())){
            return;
        }

        //回填:資料來源、頁碼起點(setQuerySource\setQuerySourcePageStart)、條件過濾起點(setQuerySourceFilterStart),以確保在這個查詢來源內的跳轉分頁查詢正常 【即:確定補頁的最後1條記錄資訊,以便後面的分頁帶上這個分隔條件,排除補頁的記錄】
        paginationRequest.setQuerySourceFilterStart(paginationResponse.getRows().get(paginationResponse.getRows().size()-1));

        if (paginationRequest.getQuerySource() != sourcePageRange.getSource() && !CollectionUtils.isEmpty(countQuerySourceFuncList)) {
            //如果查詢來源的分頁區間的首頁資料來源與原分頁區間的資料來源不相同,說明資料有變化(資料條數變少或沒有,導致切換下一個查詢來源),則此時應重新彙總計算分頁資訊
            getTotalCountResult(paginationRequest);
            List<MultiSourcePagination.SourcePageRange> currentSourcePageRanges = getCurrentSourcePageRanges(paginationRequest);
            if (CollectionUtils.isEmpty(currentSourcePageRanges)){
                //正常一定會匹配到,若匹配不到,說明記錄數變少了,少到小於當前頁碼的記錄,這種則正常返回
                return;
            }

            paginationRequest.setQuerySourcePageStart(currentSourcePageRanges.get(0).getBeginPage() - (currentSourcePageRanges.get(0).getBeginPageSize() == paginationRequest.getPageSize() ? 1 : 0));
        }
    }


    /**
     * 執行具體的多查詢來源的合併分頁邏輯
     *
     * @param paginationRequest
     * @return
     */
    private MultiSourcePagination<T,B> doPageQueryFromMultiSource(MultiSourcePagination<T,B> paginationRequest) {
        if (paginationRequest.getQuerySource() + 1 > pageQuerySourceFuncList.size()) {
            //如果查詢來源索引值超過設定的分頁查詢來源回撥方法集合,則說明入參不正確,直接返回
            return paginationRequest;
        }

        Function<MultiSourcePagination<T,B>, List<T>> currentPageQueryFunc = pageQuerySourceFuncList.get(paginationRequest.getQuerySource());
        List<T> pagedList = currentPageQueryFunc.apply(paginationRequest);

        if (!CollectionUtils.isEmpty(pagedList)) {
            if (CollectionUtils.isEmpty(paginationRequest.getRows())) {
                //如果不存在記錄,則直接設定結果記錄
                paginationRequest.setRows(pagedList);
            } else {
                //如果已存在記錄,說明此處為補充查詢,則合併結果記錄
                paginationRequest.getRows().addAll(pagedList);
            }
            if (paginationRequest.getRows().size() >= paginationRequest.getPageSize()) {
                //查詢結果(含補充的)滿1頁,則正常返回
                return paginationRequest;
            }
        }

        if (paginationRequest.getQuerySource() + 1 >= pageQuerySourceFuncList.size()) {
            //查詢結果不滿1頁(或為空),但已是最後的查詢來源(即:最後一張表),則說明已到最大頁碼,直接返回當前剩餘記錄即可,無需補充分頁記錄的情況
            //此時不用總頁數與頁碼判斷,是考慮資料本身就在動態變化,按查詢的實際結果即可
            paginationRequest.setRows(pagedList);
            return paginationRequest;
        }

        //除外,則說明查詢的結果為空或記錄數不滿1頁,則需要跨查詢來源(即:切換到另一個表進行查詢,補充分頁記錄)
        paginationRequest.setQuerySource(paginationRequest.getQuerySource() + 1);
        paginationRequest.setQuerySourceFilterStart(null);

        if (!CollectionUtils.isEmpty(pagedList)) {
            //若不滿1頁,則限制補充查詢剩餘記錄數(注意可能多個查詢來源合併補充1頁,故這裡是rows.size而不是pagedList.size)
            int offsetCount = paginationRequest.getPageSize() - paginationRequest.getRows().size();
            paginationRequest.setLimitRowCount(offsetCount);
        } else {
            //若查詢為空,則直接需要查詢完整的1頁
            paginationRequest.setLimitRowCount(paginationRequest.getPageSize());
        }

        //補充查詢下一個查詢來源(即:切換到下一個表進行補充查詢,如SQL:select * from table where 查詢條件 order by in_month,id limit #{limitRowCount})
        MultiSourcePagination<T,B> paginationResponse = doPageQueryFromMultiSource(paginationRequest);
        if (!CollectionUtils.isEmpty(paginationResponse.getRows())) {
            //設定下一頁查詢的分隔點-查詢過濾條件(實際下一頁的查詢來源的SQL查詢條件應加上querySourceLimitStart物件中的關鍵欄位,如SQL:where id>#{querySourceLimitStart.id} and in_month>=#{querySourceLimitStart.inMonth})
            paginationResponse.setQuerySourceFilterStart(paginationResponse.getRows().get(paginationResponse.getRows().size() - 1));
            //設定下一頁查詢的分隔點-已佔用頁碼(實際下一頁的查詢來源的SQL頁碼應為:page-querySourcePageStart,如SQL:order by page-querySourcePageStart,pageSize )
            paginationResponse.setQuerySourcePageStart(paginationRequest.getPage());
        }
        //補充查詢完成後,將LimitRowCount還原預設值,以便下一次分頁請求時,可以正常進行分頁處理
        paginationResponse.setLimitRowCount(0);

        return paginationResponse;
    }

    private List<MultiSourcePagination.SourcePageRange> getCurrentSourcePageRanges(MultiSourcePagination<T,B> paginationRequest) {
        int page = paginationRequest.getPage();

        if (CollectionUtils.isEmpty(paginationRequest.getPageRanges())) {
            return null;
        }

        List<MultiSourcePagination.SourcePageRange> pageRanges = paginationRequest.getPageRanges().stream().filter(p -> p.getBeginPage() <= page && page <= p.getEndPage())
                .sorted(Comparator.comparingInt(MultiSourcePagination.SourcePageRange::getSource)).collect(Collectors.toList());

        return pageRanges;
    }


}


public class MultiSourcePagination<T,B> {
    //如下是本分頁欄位
    private int page = 1;
    private int pageSize;
    private List<T> rows;
    private int rowTotal;
    private int pageTotal;
    @JsonIgnore
    private boolean count=false;

    //如下是多資料來源分頁所需欄位
    /**
     * 當前查詢來源索引(來源表索引,0,1,2...,預設為0)
     */
    private int querySource = 0;

    /**
     * 查詢來源【即:多表】頁碼分佈區間資訊,以便快速根據page定位到對應的查詢來源
     */
    private List<SourcePageRange> pageRanges;

    /**
     * 查詢來源條件過濾起點(當存在跨查詢來源【即:跨表】補滿一頁記錄時則記錄當前頁最後的關鍵過濾條件物件資訊)
     */
    private T querySourceFilterStart;

    /**
     * 查詢來源的頁碼起點(當存在跨查詢來源【即:跨表】分頁時就記錄當前頁碼,預設為0)
     */
    private int querySourcePageStart = 0;

    /**
     * 限制行返回的記錄數(即:limit N,僅在補充分頁時有值)
     */
    @JsonIgnore
    private int limitRowCount = 0;

    /**
     * 查詢條件
     */
    private B queryCriteria;

    public MultiSourcePagination() {
        //預設分頁過程中不彙總計算總記錄數、總頁數,以提高查詢效能,若有需要允許顯式設定為true
        this.count=false;
    }

    //省略getter、setter方法...

    /**
     * 獲取計算後的實際SQL limit start數值(當跨查詢來源【即:跨表】翻頁時,此值=page-querySourcePageStart,若還未發生跨查詢來源翻頁時,此值=page,因為querySourcePageStart=0【僅跨查詢來源時才有值】)
     *
     * @return 實際SQL limit start數值
     */
    @JsonIgnore
    public int getPageLimitStart() {

        if (this.page - this.querySourcePageStart - 1 <= 0) {
            return 0;
        }

        return (this.page - this.querySourcePageStart - 1) * this.pageSize;
    }

    public List<SourcePageRange> getPageRanges() {
        return pageRanges;
    }

    public void setPageRanges(List<SourcePageRange> pageRanges) {
        this.pageRanges = pageRanges;
    }

    /**
     * 查詢來源分頁區間資訊(即:每個查詢來源【即:表】實際對應的頁碼)
     */
    public static class SourcePageRange {
        /**
         * 開始頁碼
         */
        private final int beginPage;
        /**
         * 結束頁碼
         */
        private final int endPage;
        /**
         * 查詢來源索引
         */
        private final int source;

        /**
         * 開始頁實際記錄數
         */
        private final int beginPageSize;

        /**
         * 結束頁實際記錄數
         */
        private final int endPageSize;

        public SourcePageRange(int beginPage, int endPage, int source, int beginPageSize, int endPageSize) {
            this.beginPage = beginPage;
            this.endPage = endPage;
            this.source = source;
            this.beginPageSize = beginPageSize;
            this.endPageSize = endPageSize;
        }

        public int getBeginPage() {
            return beginPage;
        }

        public int getEndPage() {
            return endPage;
        }

        public int getSource() {
            return source;
        }

        public int getBeginPageSize() {
            return beginPageSize;
        }

        public int getEndPageSize() {
            return endPageSize;
        }
    }
}

提示:.NET語言也可以參考上述JAVA程式碼轉為實現對應的C# 或VB.NET版本的多資料來源分頁查詢工具類,個人覺得還是比較適用的,如果大家覺得也能幫助到你,可以點贊支援一下哈!

相關文章