复习:
使用版本:3.4.5
1.简介
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录
2. 背景介绍
MyBatis是支持普通SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手共设置以及结果集的检索,MyBatis使用简单的XML或者注解用于配置和原始映射,将接口和Java的POJOs(Plain Ordinary Java Objects,普通的 Java对象)映射成数据库中的记录。
每个MyBatis应用程序主要利用SqlSessionFactory实例操作数据库,而SqlSessionFactory实例可以通过SqlSessionFactoryBuilder获得。SqlSessionFactoryBuilder可以从一个xml配置文件或者一个预定义的配置类实例获得。
用xml文件构建SqlSessionFactory实例是非常简单的事情,推荐在这个配置中使用类路径资源(classpath resource),但你可以使用任何Reader实例,包括用文件路径或者f://开头的url创建实例。MyBatis有一个实用类(Resources),它有很多方法,可以方便地从类路径以及其他位置加载资源。
3.框架特点
- 简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。
- 灵活:mybatis不会对应用程序或者数据库的现有设计强加任何影响。 sql写在xml里,便于统一管理和优化。通过sql基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多。
- 解除sql与程序代码的耦合:通过提供DAL层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
- 提供映射标签,支持对象与数据库的orm字段关系映射
- 提供对象关系映射标签,支持对象关系组建维护
- 提供xml标签,支持编写动态sql
4. 功能架构
Mybatis的功能架构分为三层:
- API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
- 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
- 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
5. 体系结构
6. 使用传统的MyBatis API
通过调用MyBatis中SqlSession对象的方法从而达到与数据库交互的方式,有一些类似DBUtils的操作!
<!-- namespace="dao接口路径"
还可以连dao接口都不定义,只定义此mapper文件,则此处的namespace可以随意写一个“zhj”,
执行方式sqlSession.selectList("zhj.queryAll")
sqlSession.delete("zhj.deleteUser",1);
基于DAO接口的开发中,底层依然是如上api
-->
<mapper namespace="zhj">
...
<select id="queryAll" resultType="User">
select id,name,gender,create_time as createTime
from tt2
</select>
<!-- 删除用户 -->
<delete id="deleteUser" parameterType="int">
delete from tt2
where id=#{id}
</delete>
...
7.MyBatis类型解析
第一:mybatis自动处理java.util.Date
第二:mybatis自动识别1:0为true/false。数据库中存1/0,java中直接收true/false
第三:parameterType 和 resultType 解析
<-- parameterType类型:jdk8中基本类型+String :double,float,int,short,boolean,long,date,string --> <select id="queryOne" parameterType="int" resultType="User"> select id,name,gender,create_time as createTime from tt2 where id=#{id} </select>
原理: 1>会类型字符转小写,TypeAliasRegistry初始化构造方法中注册了很多类型,先从中检查
2>如果如上没有,则用Resources.classForName(string)通过反射得到类型(核心原理是 Class.forName()类加载)
8.MyBatis事务控制
8.1 起点 :获取SqlSession对象
// ================ 起点 ===============
sqlSessionFactory.openSession();
8.2 DefaultSqlSessionFactory
// 类:DefaultSqlSessionFactory = SqlSessionFactory的实际类型
public SqlSession openSession() {
// ================ openSession()方法实现 ================
// Executor 隔离级别 自动提交
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获得数据库相关配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// ============== 获得事务对象,(此处需要重点关注,继续跟进 ================
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 返回构建好的SqlSession,实际类型为DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
8.3 JdbcTransactionFactory
//类:JdbcTransactionFactory
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
// =============== 构建事务对象,实际类型:JdbcTransaction ==============
return new JdbcTransaction(ds, level, autoCommit);
}
8.4 JdbcTransaction
//类:JdbcTransaction,事务对象
// 构造方法:以上传来的各项参数:【数据源+事务隔离级别+是否自动提交事务】
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
// 在自己的属性中 保持了如上3项参数
dataSource = ds;
level = desiredLevel;
autoCommmit = desiredAutoCommit;
}
// 事务对象中,会打开一个 Connection,其中设置了如上的参数
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
// 1.通过数据源,获得一个Connection对象
connection = dataSource.getConnection();
if (level != null) {
// 2.设置事务隔离级别
connection.setTransactionIsolation(level.getLevel());
}
// 3.设置事务的自动提交属性,默认为false,即mybatis中默认是不自动提交事务的
setDesiredAutoCommit(autoCommmit);
}
// 事务对象中设置 自动提交属性
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
....
connection.setAutoCommit(desiredAutoCommit);
....
}
// 事务对象中 提交事务
public void commit() throws SQLException {
....
connection.commit();
....
}
//事务对象中 回滚事务
public void rollback() throws SQLException {
...
connection.rollback();
....
}
//事务对象中 关闭资源
public void close() throws SQLException {
...
connection.close();
}
综上,即,在创建sqlSession时,便会创建一个Transaction对象。
Transaction对象负责创建Connection对象,并借之实现事务管理:事务属性设置(隔离级别,自动提交)
事务行为控制(提交,回滚)
9.开发规范
- mapper接口的全限定名要和mapper映射文件的namespace的值相同。
- mapper接口的方法名称要和mapper映射文件中的statement的id相同;
- mapper接口的方法参数只能有一个,且类型要和mapper映射文件中statement的parameterType的值保持一致。
- mapper接口的返回值类型要和mapper映射文件中statement的resultType值或resultMap中的type值保持一致;
10. 方法参数细节
如果DAO方法中只有一个参数,则在mapper文件中【#{随意}】:如下的
#{abc},#{def},#{hig}
如是。
public List<User> xx(Integer id);
public List<User> xx(String name);
public List<User> xx(Date createTime);
<select id="xx" parameterType="int" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{abc}
</select>
<select id="xx" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name=#{def}
</select>
<select id="xx" parameterType="date" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where create_time=#{hig}
</select>
当方法中有多个参数时,不能再向如上那样随意,且也不能如常规课程中直接使用参数名;有两种解决方案:
1> 使用
#{arg0} #{arg1} #{arg2}
2> 使用
@Param
注解
public List<User> test1(@Param("id")Integer id, @Param("name") String name,@Param("gender") Boolean gender);
public List<User> test2(Integer id, String name,Boolean gender);
<select id="test1" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{id} and name=#{name} and gender=#{gender}
</select>
<select id="test2" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{arg0} and name=#{arg1} and gender=#{arg2}
</select>
11. # 和 $ 区分
//如果用$,则必须用 @Param注解,否则${name}会认为是要从参数中取名为name的属性
public List<User> test3(@Param("name") String a);
<!-- 注意${} 就是在做字符拼接,所以此处用了【‘${name}’】而不是【${name}】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name='"+name+"'";//此处是要加单引号的
注意:sql拼接时,有sql注入的风险
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = '${name}'
</select>
<!-- 注意#{} 就是在做占位符,所以此处用了【#{name}】而不是【’#{name}‘】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name=?“;//此处是不加单引号的
...
prepareStatement.executeQuery(sql,name);//占位符赋值
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = #{name}
</select>
必须使用$场景:
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
order by id ${name}
</select>
<select id="test4" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from ${tn}
where name = #{name}
</select>
<!-- 用在列名上亦可 -->
userDAO.test3("desc");
userDAO.test3("asc");
userDAO.test4("t_user");
userDAO.test4("t_admin");
12. Statement 选型
发生在初始加载过程中,每个MappedStatement中存储自己的 StatementType
MyBatis中,除非明确设置
statementType="STATEMENT"
否则默认都是用 PreparedStatement可参见 类:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode()
{ //在mapper标签中获取’statementType‘属性值,如果没有设置则默认取’StatementType.PREPARED.toString()‘ StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); }
<select id="test3" parameterType="string" resultType="com.zhj.domain.User" statementType="STATEMENT">
select id,name,gender,create_time as createTime
from tt2
where name = '${name}'
</select>
真正执行一个MappedStatement时:
SimpleExecutor中:
//doQuery() 或 doUpdate() 方法中
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler
resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 通过当前MappedStatement=ms 中的StatementType属性,创建一个StatementHandler
// 会创建一个RoutingStatementHandler,其构造方法中会根据ms的StatementType构建:
// SimpleStatementHandler 或 PreparedStatementHandler.他们各自负责创建一种Statement
// 如下hanlder属性中存储的是一个RoutingStatementHandler对象,可实际工作时,是其内部的
// SimpleStatementHandler 或 PreparedStatementHandler完成执行。
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds,
resultHandler, boundSql);
// 此方法中的handler会负责创建Statement,即,决定用哪种Statement
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
}....
13. 关联关系映射多种方式
//namespace = com.zhj.dao.UserDAO
<resultMap id="person_passport" type="Person">
<id property="id" column="pid"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<!-- 常规配置
<association property="passport" javaType="Passport">
<id property="id" column="passid"/>
<result property="note" column="note"/>
<result property="createTime" column="create_time"/>
</association>-->
<!-- 通过column+select,column作为select的输入参数,select中使用的查询可以来自其他ns
<association property="passport" column="passid" select="com.zhj.dao.UserDAO2.queryOne"/> -->
<!-- 嵌套resultMap,和常规配置类似,不过此种abc可以复用 -->
<association property="passport" resultMap="abc"/>
</resultMap>
<resultMap id="abc" type="Passport">
<id property="id" column="passid"/>
<result property="note" column="note"/>
<result property="createTime" column="create_time"/>
</resultMap>
<!-- 自动映射 -->
<resultMap id="person_passport" type="Person" autoMapping="true">
<id property="id" column="pid"/>
<!-- 自动映射 -->
<association property="passport" javaType="Passport" autoMapping="true">
<id property="id" column="passid"/>
</association>
</resultMap>
14. 细节参数
1. 连接池
连接池 |
---|
!![]() |
<!-- 默认是如上所示开启的,可以通过以下配置关闭二级缓存 --> <settings> <setting name="cacheEnabled" value="false"/> </settings>
<dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.user}" /> <property name="password" value="${jdbc.password}" /> <property name="poolMaximumIdleConnections" value="9"/> 增设参数 </dataSource>
2. 延迟加载细节
15. 缓存细节
1. 一级缓存
一级缓存 |
---|
2. 二级缓存开关
二级缓存开关 和 存储位置 |
---|
3. 二级缓存存储位置
二级缓存,存于SqlSessionFactory中 |
---|
4.二级缓存存储结构
二级缓存存储结构 |
---|
5. 二级缓存生效时间
二级缓存需要提交事务,才会生效。
原因:用于全局共享的数据,还是在操作数据的事务结束后再生效为好。
实现:见下图:
二级缓存细节 |
---|
注意:
查询时,数据存入一级缓存中。
如果sqlsession在commit或close后,数据存入进入二级缓存。
如果sqlsession执行rollback,则数据不会进入二级缓存。
6. 二级缓存-cache-ref
和关系属性相关
注意如果
<collection>
中没有使用select
关联查询,则不存在此问题。
<mapper namespace="com.zhj.dao.UserDAO">
<cache/>
<resultMap id="user_orders" type="User">
<id property="id" column="uid"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="registTime" column="registTime"/>
<collection property="orders" select="com.zhj.dao.OrderDAO.queryOrderOfUser" column="id" fetchType="eager"/>
</resultMap>
<select id="queryOne" parameterType="int" resultMap="user_orders">
select id,name,gender,regist_time as registTime
from t_user
where id=#{id}
</select>
</mapper>
<mapper namespace="com.zhj.dao.OrderDAO">
<!-- 使用cache-ref 则OrderDAO的缓存数据,会存放于com.zhj.dao.UserDAO分支下,
与UserDAO的缓存数据存储于同一个Perpetual对象中
-->
<cache-ref namespace="com.zhj.dao.UserDAO"/>
<select id="queryOrderOfUser" parameterType="int" resultType="Order">
select id as oid,note,price,create_time as createTime
from t_order
where user_id = #{userId}
</select>
</mapper>
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);//User 和 关系属性Order 都被缓存
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //有二级缓存数据可用 (OrderDAO中必须有 <cache>或<cache-ref>)
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);
//数据改动,此时会清空User缓存,并清空Order缓存(因为order中是 <cache-ref>,如果是<cache>则此处只会清空User缓存)
userDAO.insertUser(new User(null,"new_user",true,new Date()));
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //重新查询数据库,<cache-ref namespace="A"/> 会由于A的改动而被清空缓存
16. idea设置sql方言
设置方言后,mapper文件中的sql语句就不会报黄啦
17. bind
<bind name="name_pattern" value="'%'+name+'%'"/>
创建一个变量,并绑定在当前上下文
<select id="queryUsers" parameterType="User" resultType="User">
<bind name="name_pattern" value="'%'+name+'%'"/>
SELECT id,name,gender,regist_time
FROM t_user2
<trim prefix="where" prefixOverrides="and|or">
<if test="name != null">
name like #{name_pattern}
</if>
<if test="id>=10">
OR id>#{id}
</if>
<if test="gender == false">
AND gender=#{gender}
</if>
</trim>
</select>
18. Foreach-Map(了解)
public int insertUsers(Map<String,List<User>> users);
<insert id="insertUsers">
insert into t_user2 (name,gender,regist_time) values
<foreach collection="userMaps" item="user" index="ind" close="" open="" separator=",">
(#{user.name},#{user.gender},#{user.registTime,jdbcType=DATE})
</foreach>
</insert>
HashMap<String, List<User>> userMaps = new HashMap<>();
userMaps.put("users",users);
userDAO.insertUsers(userMaps);
19. 配置文件加载
SqlSessionFactoryBuilder().
build(reader);
|-- XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);//封装解析器
return build(parser.
parse()
);//解析配置,返回一个SqlSessionFactory.则SSF是由mybatis的全部配置支撑的。 |-- XMLConfigBuilder .
parse()
//解析配置文件 |-- XMLConfigBuilder .
parseConfiguration
(parser.evalNode("/configuration")); //解析配置,从configuration标签开始,解析整个配置文件
//其中加载配置个各个节点都是用了单独的方法
//加载properties标签,加载typealias标签等,可以在每个加载方法中看到标签解析的细节。
//比如properties标签加载时,会解析 resource属性 或 url属性,以及会如何判断用户用的是哪个。
20. 映射文件加载
XMLConfigBuilder .
parseConfiguration
(parser.evalNode("/configuration"));//其中有一项是加载mapper文件 |-- XMLConfigBuilder .
mapperElement
(root.evalNode(“mappers”)); //加载mapper文件 |-- XMLMapperBuilder.
buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
//其中会加载所有的 select,update,delete,insert标签, 返回
List<XNode>
,然后遍历该List,执行下一步 |-- XMLStatementBuilder.
parseStatementNode()
//每遍历一个XNode执行一次该方法,解析出一个MappedStatement //其中会解析上一步加载的每个标签(
XNode
),和其中的属性(resultTeype,id,paremeterType…), //其中还有很重要的一步,是决定当前mapstatement使用的Statement类型。
//解析完毕后,有一步:
|-- parseStatementNode()#
builderAssistant.addMappedStatement(XNode中的所有属性 id,resultTpe,...)
|-- MapperBuilderAssistant.
addMappedStatement()
|-- 其中会根据传入的(id,resultType,…)构建一个
MappedStatement.Builder statementBuilder
然后: |--
statementBuilder.build()
返回一个MappedStatement |--
configuration.addMappedStatement(statement);
//将MappedStatement 存入Configuration对象
21. 动态sql解析
在映射文件加载过程中,还会有sql语句的解析,主要是针对,动态sql的解析
|-- XMLStatementBuilder.
parseStatementNode()
中: |--
langDriver.createSqlSource(configuration, context, parameterTypeClass);
|-- 如上方法实现中:
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
|-- XMLScriptBuilder.
parseScriptNode()
|-- 如上方法中:
List contents = parseDynamicTags(context);//解析动态sql
if (isDynamic) {// 如果有 $ 或 动态标签
sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); //其中解析 #{ } }
//得到一个SqlSource,其中存储了 : sql语句;sql中的参数名(#{xx})
//还持有当前SqlSessionFactory中的configuration对象( 缓存信息,延迟加载信息等配置都可以在其中获)
【parseDynamicTags方法中调用nodeHandlers()//获取所有的动态sql标签处理的 handler,每种动态标签都对应一种handler
handler会解析动态标签】
22. SqlSession执行
UserDAO userDAO = sqlSession.getMapper(UserDAO.class);
|-- userDAO.queryOne(1); //mapper类型:MapperProxy,基于jdk动态代理构建
|-- MapperProxy.
invoke
//其中获得的MapperMethod对象( 其中封装了:当前方法接口,当前方法对象, Configuration)
|-
MapperMethod.execute()
//其中会真正执行对应方法,执行的真正实现者是sqlSession.xxxx();[insert,udate,delete,selectOne,…]
|- 在传统API中调用
Excutor(CachingExcutor=>SimpleExcutor)
|-
PrepareStatementHandler
=> 获取PrepareStatement.execute()
23. PageHelper
1. 使用过程
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>RELEASE</version>
</dependency>
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
<!-- spring等价配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注意其他配置 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor"></bean>
</array>
</property>
</bean>
//使用:
PageHelper.startPage(2,3);// 第2页,每页3条数据
PageHelper.orderBy("regist_time");//可以选择设置排序
List<User> users = mapper.queryAllUsers();//PageHelper后的第一个查询语句,会被PageHelp增强处理(可观测mysql日志)
for (User user : users) {// users中已经是分页数据
System.out.println(user);
}
//包装一个PageInfo,其中会持有所有分页会用到的信息:当前页号,每页多少条,共多少页,是否为第一页/最后一页,是否有下一页等。
PageInfo<User> pageInfo=new PageInfo<User>(users);
PageInfo对象 概览 |
---|
注意:如果是多表查询,则会有如下效果:
select t_user.id,name,gender,regist_time,
t_order.id orderId,price,note,create_time
from t_user JOIN t_order
ON t_user.id = t_order.user_id LIMIT 2, 2
#是对大表做了分页,此时数据可能不完整,比如用户有10个订单,却只能查到2个,或部分。
2. 重要提示
PageHelper.startPage
方法重要提示只有紧跟在
PageHelper.startPage
方法后的第一个Mybatis的**查询(Select)**方法会被分页。请不要配置多个分页插件
请不要在系统中配置多个分页插件(使用Spring时,
mybatis-config.xml
和Spring<bean>
配置方式,请选择其中一种,不要同时配置多个分页插件)!分页插件不支持带有
for update
语句的分页对于带有
for update
的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。分页插件不支持嵌套结果映射
由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。