Mybatis 逻辑与物理分页
前言
什么是物理分页和逻辑分页?
一.逻辑分页
1.逻辑分页的第一种方式,利用ResultSet的滚动分页。步骤如下:
a.根据条件sql查询数据库。
b.得到ResultSet的结果集,由于ResultSet带有游标,因此可以使用其next()方法来指向下一条记录。
c.利用next()方法,得到分页所需的结果集。
这种分页方式依靠的是对结果集的算法来分页,因此通常被称为“逻辑分页”。
2.逻辑分页的第二种方式
利用Scrollable ResultSets(可滚动结果集合)来快速定位到某个游标所指定的记录行,所使用的是ResultSet的absolute()方法。
虽然和第一种方式区别不大,单效率比ResultSet滚动要好,但是absolute()方法并不是所有jdbc驱动都支持。
可用如下代码测试当前jdbc驱动是否支持可滚动结果集:
int type = rs.getType();
if (type == ResultSet.TYPE_SCROLL_INSENSITIVE || type == ResultSet.TYPE_SCROLL_SENSITIVE)
System.out.println("Result set is scrollable");
else
System.out.println("Result set is not scrollable");
二.物理分页
利用数据库本身的一些特性来分页。即:利用了数据库对sql语法的优化,提高分页性能。
1.针对Oracle数据库
步骤如下:
a.根据所使用的数据库特性来组织sql进行分页。
b.每次跳转页面的sql查询都不相同。
通用的sql分页方式,“限制行数结果集的倒序”分页,步骤如下:
(1).取得符合条件的所有结果集中可以唯一标识的Key值(通常是主键),并正向排序。
(2).利用数据库提供的特殊方法进行“最大结果集”的限制(在Oracle中使用rownum, sql server中使用top, mysql中使用limit...),
该“最大结果集”指包含当前所处页的所有记录数,“最大结果集”应该只包含惟一的Key值
(3).对步骤(2)中的“最大结果集”进行逆序,并取得“显示当前页显示数量的结果集”,该结果集中只包含惟一的Key值。
(4).通过步骤(3)中所取得的Key值取得显示数据,该显示数据就是当前页应该显示的数据。
2.针对MySQL数据库
在MySQL数据库中offset关键字的意思是"越过",而limit关键字的意思是“限制”,利用这两者结合可轻松分页。
(1)取得符合条件的结果集,包含全字段。
(2)利用offset关键字越过一段结果集(被越过的结果集就是"(当前页 - 1) * 一页显示数")。
(3)利用limit关键字限制取得一段结果集(被限制取得的结果集就是一页显示数)
分页结论:
1.物理分页速度上并不一定快于逻辑分页,逻辑分页速度上也并不一定快于物理分页。
2.物理分页总是优于逻辑分页:没有必要将属于数据库端的压力加诸到应用端来,就算速度上存在优势,
然而其它性能上的优点足以弥补这个缺点。
3.在分页工作前,有必要了解使用数据库本身的一些sql语句特点更好的分页。
代码链接:http://www.cnblogs.com/wllyy189/articles/1237478.html
1.Mybatis 逻辑分页
Mybatis 实现了逻辑分页,直接传入RowBounds 实例即可实现。
public List<User> findUserByPage() {
// TODO Auto-generated method stub
SqlSession sqlSession = ssf.openSession();
int offSet = 0;
int limit = 2;
User user = new User();
user.setName("qing");
RowBounds rowRounds = new RowBounds(offSet, limit);
List<User> listUsers = sqlSession.selectList("select UserByPage", user, rowRounds);
return listUsers;
}
Mapper.xml:
<select id="selectUserByPage" resultType="test_ibatis.User" resultSetType="SCROLL_SENSITIVE"parameterType="test_ibatis.User">
select * from t_user where name like '%'||#{name}||'%'
</select>
在配置文件中指定resultSetType即可。
2. Mybatis物理分页
Mybatis自带的分页方法只是逻辑分页,如果数据量很大,内存一定会溢出,在不修改mybatis源代码的情况下,应该怎么使mybatis支持物理分页呢?
网上推荐了三种实现物理分页的方法:
代码的情况下,应该怎么使mybatis支持物理分页呢?
网上推荐了三种实现物理分页的方法:
1)直接在配置文件中使用limit 子句。Select * from t_user limit 1,3
select * from users limit #{start}, #{end}
2) 扩展mybatis的拦截器,自己重新构造查询子句。在下面的例子中也主要使用改方式实现。
http://www.cnblogs.com/jcli/archive/2011/08/09/2132222.html
3)参考hibernate的Dialect的实现方式,在mybatis也用此方式实现。
http://blog.sina.com.cn/s/blog_6853c03b0100ooe9.html
iBatis分页查询的性能问题分析
http://hi.baidu.com/lvzhnan/blog/item/1d788f9812630d046e068ca9.html具体内容将在下一讲中详细说明。
3. 分页实现步骤
主要分为两步,第一步是定义每个数据库自己的sql拼接statement。在该实例中,首先定义了一个基本的方言(Dialect)类。然后实现了Oracle和Mysql两个数据库。第二步是定义Mybatis拦截器。
4.具体实现
在dialect类中主要是定义一个抽象的方法,用于其他子类的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
publicabstractclass Dialect {
publicstaticenum Type {
MYSQL, ORACLE
}
publicabstract String getLimitString(String sql,
int
skipResults,
int
maxResults);
}
|
Mysql使用limit实现分页:
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
|
/**
* 得到分页的SQL
*
* @param offset
* 偏移量
* @param limit
* 位置
* @return分页SQL
*/
publicstatic String getLimitString(String querySelect,
int
offset,
int
limit) {
querySelect = getLineSql(querySelect);
String sql = querySelect.replaceAll(
"[^\\s,]+\\."
,
""
) +
" limit "
+ offset +
" ,"
+ limit;
return
sql;
}
|
Oralce使用两层查询实现分页:
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
|
public
String getLimitString(String sql,
int
offset,
int
limit) {
sql = sql.trim();
boolean
isForUpdate =
false
;
if
(sql.toLowerCase().endsWith(
" for update"
)) {
sql = sql.substring(
0
, sql.length() -
11
);
isForUpdate =
true
;
}
StringBuffer pagingSelect =
new
StringBuffer(sql.length() +
100
);
pagingSelect
.<span style=
"color: #ff0000;"
><strong>append(
"select * from ( select row_.*, rownum rownum_ from ( "
); </strong></span>
pagingSelect.append(sql);
pagingSelect.append(
" ) row_ ) where rownum_ > "
+ offset
+
" and rownum_ <= "
+ (offset + limit));
if
(isForUpdate) {
pagingSelect.append(
" for update"
);
}
return
pagingSelect.toString();
}
|
拦截器中重要的代码部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
Object intercept(Invocation invocation)
throws
Throwable {
Dialect dialect =
null
;
switch
(databaseType) {
caseMYSQL:
dialect =
new
MysqlDialect();
}
|
5. XML文件配置
a. 在spring配置文件中加载该拦截器
<beanid="paginationInterceptor"class="page.PaginationInterceptor">
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="myDataSource" />
<property name="plugins">
<array>
<ref bean="paginationInterceptor" />
</array>
</property>
<property name="configLocation" value="WebContent/WEB-INF/conf/SqlMapConfig.xml"/>
</bean>
b:SqlConfig.xml
在mybatis的配置文件中指定数据库的类型
<properties>
<property name="dialect" value="mysql"/>
</properties>
6.推荐文章
http://blog.csdn.net/firecoder/article/details/6069434
拦截器里面最重要的三条语句:
metaStatementHandler.setValue("delegate.boundSql.sql", dialect.getLimitString(originalSql, rowBounds.getOffset(), rowBounds.getLimit()) ); metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET ); metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT );
执行的sql语句,现在新设置的sql语句是物理分页的,所以现在不再需要mybatis进行额外的操作了,所以把rowBounds的偏移量恢复为初始值(offet:0,limit:Integer.max) 为了指定数据库版本,在mybatis全局配置文件设置dialect值
Q&A 常见问题:
1.最近有朋友用mybatis和spring整合的时候如果按照下列方式发现dialect属性不能设置成功:
1 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 2 <property name="dataSource" ref="dataSource"/> 3 <property name="typeAliasesPackage" value="com.***.web.domain"/> 4 <property name="plugins"> 5 <array> 6 <ref bean="paginationInterceptor"/> 7 </array> 8 </property> 9 <property name="configurationProperties"> 10 <props> 11 <prop key="dialect">mysql</prop> 12 </props> 13 </property> 14 </bean> 这个问题是org.mybatis.spring.SqlSessionFactoryBean这个代码里有个bug (244行,或者不是bug,是作者不想这么做法),如果感兴趣可以看下源码。配置文件做下如下修改:
1 <ref bean="paginationInterceptor"/> 2 </array> 3 </property> 4<!-- 这里不要,注释掉 5 <property name="configurationProperties"> 6 <props> 7 <prop key="dialect">mysql</prop> 8 </props> 9 </property> 10 --> 11<!-- 加上这个属性 --> 12 <property name="configLocation" value="classpath:Mybatis_Configuration.xml"/> 13 </bean> Mybatis_Configuration.xml的配置如下:
1<?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 <configuration> 7 <properties> 8 <property name="dialect" value="oracle"/> 9 </properties> 10</configuration>
2. delegate.rowBounds.offset 是在哪定义的?为什么要设置这个属性呢?
我们调用分页的API一般是这样的:
List l = session.selectList("users.selectUsers", user,new
RowBounds(2, 1));
mybatis默认情况下全部查询出来后再截取当前页的记录。可是如果我们自己写了个分页插件的情况下采用了物理分页,查询出来的记录就是当前页的内容。所以现在要告诉mybatis不再对结果进行加工,所以要重置为不分页的设置:
metaStatementHandler.setValue(
"delegate.rowBounds.offset"
, RowBounds.NO_ROW_OFFSET );
metaStatementHandler.setValue(
"delegate.rowBounds.limit"
, RowBounds.NO_ROW_LIMIT );
这两条语句就是用反射机制设置,相当于:
delegate.getRowBounds.setLimit(RowBounds.NO_ROW_LIMIT);
delegate.getRowBounds.setOffset(RowBounds.NO_ROW_OFFSET);