分享自研实现的多数据源(支持同DB不同表、跨DB表、内存数据、外部系统数据等)分页查询工具类实现原理及使用

WenJun.Zuo ITer

思考:

提起分页查询,想必任何一个开发人员(不论是新手还是老手)都能快速编码实现,实现原理再简单不过,无非就是写一条SELECT查询的SQL语句,ORDER BY分页排序的字段, 再结合limit (页码-1),每页记录数,这样即可返回指定页码的分页记录,类似SQL如下所示:

1
select * from table where 查询条件 order by id limit 100,100; -- 这里假设是第2页(limit 第1个值从0开始),每页100条

那如果是想将多张表的记录合并一起进行分页查询,我们又该如何实现呢?我估计稍微有点经验的开发人员可能会立马举一反三,想到了通过UNION 多张表的方式来实现分页查询,类似SQL如下所示:

1
2
3
4
5
6
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条

    如下每一单元格代表一行记录数,单元格中的数字表示分页数字,不同颜色区分不同的查询来源

  1. 分页查询时,根据前端用户选择的查询页码、查询来源(这个首次不传则为默认0,后面若跨查询来源则会由后端返回给前端,前端保存)、分页大小、分页区间(这个由后端计算后返回给前端保存)等入参信息(MultiSourcePagination),先由页码获得分页区间对象列表(不足1页跨多查询来源时会有多个查询来源,否则一般都只会命中一个分页区间),选择第1个分页区间对象,若这个分页区间的查询来源与当前请求的查询来源相同说明是正常的分页,则执行正常分页逻辑;若不相同时说明存在跳页情况,则再判断当前查询的页码是否为这个分页区间对应的的开始页码,若是说明无需分隔点,则仅需切换查询来源及设定查询来源的分页超始页码后执行正常分页逻辑,否则说明跳页且当前查询的页码在这个查询来源的第2页及后面的分页区间内(含最末页)或分页区间开始页存在跨多个查询来源(即:多个查询来源补足1页记录,如:表1占10条,表2占10条,页大小为20条),此时就需要先根据分页区间的开始页记录数及查询条件查出对应的补页记录信息,然后获取结果的最后一条记录作为这个查询来源的分页过滤条件(注意:若查询补页记录后的数据源与当前原请求的分页区间的数据源不相同时,则说明数据有变化(数据条数变少或没有,导致切换下一个查询来源),此时应重新汇总计算分页信息,以便再翻页时能准确查询到数据),最后执行正常分页逻辑(对应代码的方法:getPageQueryResult)

  2. 正常分页逻辑(对应代码的方法: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的表或内存中的集合对象 、甚至是调外部系统的接口,只要能符合分页的出入参字段即可,混合也是可以的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@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定义片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!-- 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代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

//前置工作:

// 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 源代码,其实理解了多数据源分页的原理后写代码还是很简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
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

if (paginationRequest.getPage()>paginationRequest.getPageTotal()){
//如果页码超过总页数,则直接返回空
paginationRequest.setRows(null);
return paginationRequest;
}

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页(或为空),但已是最后的查询来源(即:最后一张表),则说明已到最大页码,直接返回当前剩余记录即可,无需补充分页记录的情况
//此时不用总页数与页码判断,是考虑数据本身就在动态变化,按查询的实际结果即可
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版本的多数据源分页查询工具类,个人觉得还是比较适用的,如果大家觉得也能帮助到你,可以点赞支持一下哈!

  • 标题: 分享自研实现的多数据源(支持同DB不同表、跨DB表、内存数据、外部系统数据等)分页查询工具类实现原理及使用
  • 作者: WenJun.Zuo
  • 创建于 : 2024-02-24 12:05:10
  • 更新于 : 2024-02-24 12:05:10
  • 链接: https://www.zuowenjun.cn//2024/02/24/java-multi-source-page-query-builder/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论