分页查询作为数据库交互最常用的几种操作之一,在日常开发中是非常常见的,比如前段请求需要一个分页的列表,往往有两种方式,一是把所有的数据都给到前段,前段分页。另外一种方式是前端通过传分页信息给后端,后端查询时进行分页,并将相应页的数据返给前端。第一种方式如果数据规模比较小的情况下可以使用,如果数据量较大,对内存、网络传输的消耗都是非常大的,所以实际开发中一般很少使用。第二种方式是后端进行分页,后端分页的实现又可以分为逻辑分页和物理分页,逻辑分页就是在进行数据库查询时一次性将数据查出来,然后将相应页的数据挑出返回,物理分页就是通过在查询时就查询相应的页的数据(比如直接在mysql查询语句添加limit)。很明显逻辑分页跟第一种前端分页的方式有着相同的弊端。
之前写了好几篇关于Mybatis的文章了,其实mybatis原生也是支持分页的,但为了与数据库语法解耦,实现的是逻辑分页,首先将所有结果查询出来,然后通过计算offset和limit,只返回部分结果,操作在内存中进行,所以也叫内存分页,Mybatis逻辑分页是通过RowBounds实现的。而物理分页一般是通过为sql添加limit实现的,具体可以通过拦截器在对其后的第一个执行sql进行拦截,并自动拼接上分页的sql语句,也可以直接改造文件添加limit的方式实现。本文会分别介绍一下RowBounds逻辑分页、拦截器物理分页、改造这三种分页方式的使用方法。
1. 逻辑分页——RowBounds
通过RowBounds类可以实现Mybatis逻辑分页,原理是首先将所有结果查询出来,然后通过计算offset和limit,只返回部分结果,操作在内存中进行,所以也叫内存分页。弊端很明显,当数据量比较大的时候,肯定是不行的,所以一般不会去使用RowBounds进行分页查询,这里仅展示一下RowBounds用法。Mybatis Generator原生支持RowBounds查询,生成的Mapper接口中存在一个方法selectByExampleWithRowbounds就是通过RowBounds进行分页查询。
1.1 项目结构
|
|
|
+---src
| +---main
| | +---java
| | | \---com
| | | \---zhuoli
| | | \---service
| | | \---springboot
| | | \---mybatis
| | | \---rowbounds
| | | |
| | | |
| | | +---controller
| | | |
| | | |
| | | +---repository
| | | | +---conf
| | | | |
| | | | |
| | | | +---mapper
| | | | |
| | | | |
| | | | +---model
| | | | |
| | | | |
| | | | |
| | | | \---service
| | | | |
| | | | |
| | | | \---impl
| | | |
| | | |
| | | \---service
| | | |
| | | |
| | | \---impl
| | |
| | |
| | \---resources
| | |
| | |
| | +---autogen
| | | generatorConfig_zhuoli.xml
| | |
| | \---base
| | \---com
| | \---zhuoli
| | \---service
| | \---springboot
| | \---mybatis
| | \---rowbounds
| | \---repository
| | \---mapper
| |
| |
| \---test
| \---java
1.2
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
<modelVersion>4.0.0</modelVersion>
<groupId></groupId>
<artifactId>springboot-08-mybatis-rowbounds</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- Spring Boot 启动父依赖 -->
<parent>
<groupId></groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.</version>
</parent>
<build>
<plugins>
<plugin>
<groupId></groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<!--如果不配置configuration节点,配置文件名字必须为-->
<configuration>
<!--可以自定义generatorConfig文件名-->
<configurationFile>src/main/resources/autogen/generatorConfig_zhuoli.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
<plugin>
<groupId></groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Exclude Spring Boot's Default Logging -->
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- /artifact//lombok -->
<dependency>
<groupId></groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
1.3 数据源配置
@Configuration
@MapperScan(basePackages = "", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
@Value("${}")
private String url;
@Value("${}")
private String user;
@Value("${}")
private String password;
@Value("${}")
private String driverClass;
@Bean(name = "dataSource")
public DataSource dataSource() {
PooledDataSource dataSource = new PooledDataSource();
(driverClass);
(url);
(user);
(password);
return dataSource;
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
(dataSource);
/*设置mapper文件位置*/
(new PathMatchingResourcePatternResolver()
.getResources("classpath:base/com/zhuoli/service/springboot/mybatis/rowbounds/repository/mapper/*.xml"));
/*设置打印sql*/
configuration = new ();
();
(configuration);
return ();
}
}
为了展示RowBounds为逻辑分页,特地设置将sql控制台打印。
1.4 Repository定义
@Repository
@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private UserMapper userMapper;
@Override
public List<User> getUserByRowBounds(String userName, String description, RowBounds rowBounds) {
UserExample example = new UserExample();
/*动态sql,userName和description不为null,则作为查询条件查询*/
criteria = ();
if (!(userName)) {
("%" + userName + "%");
}
if (!(description)) {
(description);
}
return (example, rowBounds);
}
}
1.5 Service层调用
@Service
@AllArgsConstructor
public class UserControllerServiceImpl implements UserControllerService {
private UserRepository userRepository;
@Override
public List<User> getByRowBounds(String userName, String description, Integer pageNum, Integer pageSize) {
RowBounds rowBounds = new RowBounds((pageNum - 1) * pageSize, pageSize);
return (userName, description, rowBounds);
}
}
1.6 控制台信息
Creating a new SqlSession
SqlSession [@5cdf4b86] was not registered for synchronization because synchronization is not active
JDBC Connection [.JDBC4Connection@373e86a1] will not be managed by Spring
==> Preparing: select id, user_name, description, is_deleted from user WHERE ( user_name like ? )
==> Parameters: %zhuoli%(String)
<== Columns: id, user_name, description, is_deleted
<== Row: 6, zhuoli, zhuoli is a programer, 0
<== Row: 7, zhuoli1, zhuoli1 is a programer, 0
Closing non transactional SqlSession [@5cdf4b86]
sql查询时并没有添加limit,也验证了之前讲的RowBounds分页原理是首先将所有结果查询出来,然后通过计算offset和limit,只返回部分结果,是一种逻辑分页。至于Mybatis RowBounds分页原理,请查看Mybatis逻辑分页原理解析RowBounds,写的挺明白的。
2. 物理分页——直接为sql添加limit
如果可以在查询时直接在sql中指定limit,name肯定是只查询相应页的数据。所以就有一种直观的现象,比如使用mybatis,如果可以在中添加limit属性,那生成的sql肯定是可以直接查询到相应页的数据的。结合之前使用的Mybatis Generator,可以这样实现:首先在生成的XxxExample中加入两个属性limit和offset,同时加上set和get方法,然后在中在通过selectByExample查询时,添加limit,大概就是这种样子:
/*XxxExample*/
private Integer limit;
private Integer offset;
public void setLimit(Integer limit) {
= limit;
}
public Integer getLimit() {
return limit;
}
public void setOffset(Integer offset) {
= offset;
}
public Integer getOffset() {
return offset;
}
/**/
<select parameterType="" resultMap="BaseResultMap">
...
<if test="limit != null">
<if test="offset != null">
limit ${offset}, ${limit}
</if>
<if test="offset == null">
limit ${limit}
</if>
</if>
</select>
其实手动去加工作量也不大,但是如果表比较多,添加起来还是有一定工作量的。而且加入下次表结构变更,重新通过Mybatis Generator生成的话,这些信息也要重新加入。为了避免这些麻烦,有大神写了一个Mybatis Generator插件MySQLLimitPlugin,可以在Mybatis Generator生成文件的时候自动生成上述信息,本片文章就使用MySQLLimitPlugin插件进行生成。
2.1
项目结构跟RowBounds一致,这里不单独放出来了,首先来看一下配置。为了使用MySQLLimitPlugin插件,这里要声明MySQLLimitPlugin仓库地址,并为Mybaits Generator添加MySQLLimitPlugin依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
<modelVersion>4.0.0</modelVersion>
<groupId></groupId>
<artifactId>springboot-08-mybatis-limitplugin</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- Spring Boot 启动父依赖 -->
<parent>
<groupId></groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.</version>
</parent>
<!--声明MysqlLimitPlugin maven仓库地址-->
<pluginRepositories>
<pluginRepository>
<id>mybatis-generator-limit-plugin-mvn-repo</id>
<url>/wucao/mybatis-generator-limit-plugin/mvn-repo/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId></groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<!--如果不配置configuration节点,配置文件名字必须为-->
<configuration>
<!--可以自定义generatorConfig文件名-->
<configurationFile>src/main/resources/autogen/generatorConfig_zhuoli.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<!--为MybatisGenerator添加MySQLLimitPlugin,为生成的Example类添加limit和offset属性,为生成的文件selctByExample添加Limit-->
<dependencies>
<dependency>
<groupId></groupId>
<artifactId>mybatis-generator-plugin</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId></groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Exclude Spring Boot's Default Logging -->
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- /artifact//lombok -->
<dependency>
<groupId></groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.2 Mybatis Generator配置文件添加MySQLLimitPlugin依赖
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-////DTD MyBatis Generator Configuration 1.0//EN"
"/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--注意:本地需要有mysql-connector-java-5.1.-->
<classPathEntry location="D:\\mysql-connector-java-5.1."/>
<context targetRuntime="MyBatis3">
<!-- 省略 -->
<plugin type=""/>
<plugin type=""/>
<!--添加MySQLLimitPlugin,为生成的Example类添加limit和offset属性,为生成的文件selctByExample添加Limit-->
<plugin type=""/>
<!-- 省略 -->
</context>
</generatorConfiguration>
2.3 Repository定义
数据源定义跟RowBounds数据源定义一致,这里不单独放出来了,直接看一下limit在respository的使用:
@Repository
@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private UserMapper userMapper;
@Override
public List<User> getUserByExampleLimit(String userName, String description, Integer pageNum, Integer pageSize) {
UserExample example = new UserExample();
/*动态sql,userName和description不为null,则作为查询条件查询*/
criteria = ();
if (!(userName)) {
("%" + userName + "%");
}
if (!(description)) {
(description);
}
((pageNum - 1) * pageSize);
(pageSize);
return (example);
}
}
2.4 Service层调用
@Service
@AllArgsConstructor
public class UserControllerServiceImpl implements UserControllerService {
private UserRepository userRepository;
@Override
public List<User> getUserByExampleLimit(String userName, String description, Integer pageNum, Integer pageSize) {
return (userName, description, pageNum, pageSize);
}
}
2.5 控制台信息
Creating a new SqlSession
SqlSession [@706f956b] was not registered for synchronization because synchronization is not active
JDBC Connection [.JDBC4Connection@7234b5ae] will not be managed by Spring
==> Preparing: select id, user_name, description, is_deleted from user WHERE ( user_name like ? ) limit 0, 10
==> Parameters: %zhuoli%(String)
<== Columns: id, user_name, description, is_deleted
<== Row: 6, zhuoli, zhuoli is a programer, 0
<== Row: 7, zhuoli1, zhuoli1 is a programer, 0
<== Row: 8, zhuoli2, zhuoli2 is a programer, 0
<== Total: 3
Closing non transactional SqlSession [@706f956b]
可以看到sql查询时,limit参数是固定的,也就是说通过Example成功将limit参数添加到生成的sql中,这种方式的分页是一种物理分页,有些情况也是必须要这么做的。比如我之前做过一个使用Zebra进行分库分表的项目,在使用拦截器进行分页时,并不起作用,原因不明,到最后只好通过这种方式实现分库分表的分页查询。至于原因,一直没来及查清楚,回头有时间的话,我会用一篇文章讲述。
3. 物理分页——拦截器PageHelper
PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件,在配置了PageHelper的page number和size,调用完startPage后,它会通过PageInterceptor对其后的第一个执行sql进行拦截,比如List<User> list = (),这里原本的sql可能是 select * from users,它会自动拼接上分页的sql语句,比如mysql环境的话,就是拼接上limit语句,随后执行,最后的结果,可以通过PageInfo和Page进行获取。
3.1
项目结构跟RowBounds一致,这里不单独放出来了,首先来看一下配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
<modelVersion>4.0.0</modelVersion>
<groupId></groupId>
<artifactId>springboot-08-mybatis-pagehelper</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- Spring Boot 启动父依赖 -->
<parent>
<groupId></groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.</version>
</parent>
<build>
<plugins>
<plugin>
<groupId></groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<!--如果不配置configuration节点,配置文件名字必须为-->
<configuration>
<!--可以自定义generatorConfig文件名-->
<configurationFile>src/main/resources/autogen/generatorConfig_zhuoli.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
<plugin>
<groupId></groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Exclude Spring Boot's Default Logging -->
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--pagehelper -->
<dependency>
<groupId></groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!-- /artifact//lombok -->
<dependency>
<groupId></groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
3.2 Repository定义
@Repository
@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private UserMapper userMapper;
@Override
public List<User> getUserByCondition(String userName, String description) {
UserExample example = new UserExample();
/*动态sql,userName和description不为null,则作为查询条件查询*/
criteria = ();
if (!(userName)) {
("%" + userName + "%");
}
if (!(description)) {
(description);
}
return (example);
}
}
可以看到,respository层没有任何分页相关的信息,使用Rowbounds要传入一个RowBounds参数,使用MySQLLimitPlugin要把pageNum和pageSize作为参数传入。所以可以很明显看到一个好处是,使用PageHelper是非侵入的,假如respository层有N个查询方法,在做分页时,不用改造respository层代码,使方法的通用性更高。
3.3 service层调用
@Service
@AllArgsConstructor
public class UserControllerServiceImpl implements UserControllerService {
private UserRepository userRepository;
@Override
public PageInfo<User> getByCondition(String userName, String description, Integer pageNum, Integer pageSize) {
//分页
(pageNum, pageSize);
List<User> queryResult = (userName, description);
return new PageInfo<>(queryResult);
}
}
3.4 控制台信息
SqlSession [@2057e3b5] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [SQL_CACHE]: 0.0
JDBC Connection [.JDBC4Connection@487a7b05] will not be managed by Spring
==> Preparing: SELECT count(0) FROM user WHERE (user_name LIKE ?)
==> Parameters: %zhuoli%(String)
<== Columns: count(0)
<== Row: 3
<== Total: 1
==> Preparing: select id, user_name, description, is_deleted from user WHERE ( user_name like ? ) LIMIT ?, ?
==> Parameters: %zhuoli%(String), 2(Integer), 2(Integer)
<== Columns: id, user_name, description, is_deleted
<== Row: 8, zhuoli2, zhuoli2 is a programer, 0
<== Total: 1
Closing non transactional SqlSession [@2057e3b5]
3.5 PageInfo输出
{
"total": 3,
"list": [
{
"id": 8,
"userName": "zhuoli2",
"description": "zhuoli2 is a programer",
"isDeleted": 0
}
],
"pageNum": 2,
"pageSize": 2,
"size": 1,
"startRow": 3,
"endRow": 3,
"pages": 2,
"prePage": 1,
"nextPage": 0,
"isFirstPage": false,
"isLastPage": true,
"hasPreviousPage": true,
"hasNextPage": false,
"navigatePages": 8,
"navigatepageNums": [
1,
2
],
"navigateFirstPage": 1,
"navigateLastPage": 2,
"firstPage": 1,
"lastPage": 2
}
可以到,分页相关的基本信息都拿到了,可以说是非常方便的。
示例代码:
码云 – 卓立 – Mybatis使用RowBounds分页
码云 – 卓立 – Mybatis使用MySQLLimitPlugin分页
码云 – 卓立 – Mybatis使用PageHelper分页
参考链接:
- Mybatis逻辑分页原理解析RowBounds
- Mybatis最入门—分页查询(逻辑分页与SQL语句分页)
- MyBatis Generator实现MySQL分页插件
- Mybatis3.技术内幕(二十):PageHelper分页插件源码及原理剖析