告别繁琐的手写分页SQL,轻松实现高性能分页功能!
目录
🔧 配置分页插件
MyBatis-Plus的分页功能需要先配置才能使用:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
⚠️ 注意:DbType要选择你使用的数据库类型,如MySQL、Oracle等。不同数据库的分页语法不同,插件会自动适配。
📚 基本分页查询
使用BaseMapper
// 1. 创建Page对象,指定当前页和每页记录数
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
// 2. 执行分页查询
Page<User> userPage = userMapper.selectPage(page, null);
// 3. 获取分页结果
List<User> users = userPage.getRecords(); // 当前页数据
long total = userPage.getTotal(); // 总记录数
long pages = userPage.getPages(); // 总页数
long current = userPage.getCurrent(); // 当前页
long size = userPage.getSize(); // 每页记录数
使用IService
// 使用IService接口的page方法
Page<User> page = new Page<>(1, 10);
Page<User> userPage = userService.page(page, null);
💡 提示:Page对象既是输入参数又是输出结果,查询完成后会自动填充总记录数、总页数等信息。
🔍 带条件的分页查询
结合条件构造器,实现带条件的分页查询:
// 创建Page对象
Page<User> page = new Page<>(1, 10);
// 创建条件构造器
LambdaQueryWrapper<User> query = Wrappers.<User>lambdaQuery();
query.gt(User::getAge, 20)
.like(User::getName, "张");
// 执行分页查询
Page<User> userPage = userMapper.selectPage(page, query);
🔄 自定义分页查询
对于复杂的分页查询(如多表关联),可以自定义分页方法:
1. 定义Mapper方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页查询
*/
IPage<UserVO> selectUserPage(Page<UserVO> page, @Param("age") Integer age);
}
2. 编写XML映射
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectUserPage" resultType="com.example.vo.UserVO">
SELECT u.id, u.name, u.age, u.email, d.name as deptName
FROM user u
LEFT JOIN dept d ON u.dept_id = d.id
<where>
<if test="age != null">
AND u.age > #{age}
</if>
</where>
</select>
</mapper>
3. 使用自定义分页
// 创建Page对象
Page<UserVO> page = new Page<>(1, 10);
// 执行自定义分页查询
IPage<UserVO> userPage = userMapper.selectUserPage(page, 20);
// 获取分页结果
List<UserVO> users = userPage.getRecords();
⚙️ 高级配置
溢出总页数处理
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置请求的页面大于最大页后操作
// true: 返回首页数据, false: 返回空数据
paginationInterceptor.setOverflow(false);
单页限制条数
// 设置最大单页限制条数,防止大查询
paginationInterceptor.setMaxLimit(100L);
优化Count查询
// 开启count查询优化,只针对部分 left join
paginationInterceptor.setOptimizeJoin(true);
🖥️ 前端分页应用
在实际项目中,我们通常需要将分页结果返回给前端:
@Data
public class PageResult<T> {
private long current; // 当前页码
private long size; // 每页记录数
private long total; // 总记录数
private long pages; // 总页数
private List<T> records; // 数据列表
// 转换方法
public static <T> PageResult<T> convert(Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setCurrent(page.getCurrent());
result.setSize(page.getSize());
result.setTotal(page.getTotal());
result.setPages(page.getPages());
result.setRecords(page.getRecords());
return result;
}
}
Controller中使用:
@GetMapping("/users")
public PageResult<User> getUserPage(
@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "10") long size,
@RequestParam(required = false) String name) {
Page<User> page = new Page<>(current, size);
LambdaQueryWrapper<User> query = Wrappers.<User>lambdaQuery();
if (StringUtils.isNotBlank(name)) {
query.like(User::getName, name);
}
Page<User> userPage = userService.page(page, query);
return PageResult.convert(userPage);
}
🎨 前端表单实现示例
⚠️ 请注意: 下面的代码是一个 前端Vue组件的示例,用于演示如何与后端API交互。它 不能 直接在Markdown预览中作为交互式表单运行。您需要将此代码集成到您的Vue.js项目中,并安装相关依赖(如
axios
)才能看到实际效果。
下面是一个使用Vue实现的带搜索和分页功能的表单示例:
<template>
<div class="user-container">
<!-- 搜索表单 -->
<div class="search-form">
<form @submit.prevent="handleSearch">
<div class="form-item">
<label for="name">姓名:</label>
<input type="text" id="name" v-model="searchForm.name" placeholder="请输入姓名关键字">
</div>
<div class="form-item">
<label for="minAge">最小年龄:</label>
<input type="number" id="minAge" v-model="searchForm.minAge" placeholder="最小年龄">
</div>
<div class="form-item">
<label for="maxAge">最大年龄:</label>
<input type="number" id="maxAge" v-model="searchForm.maxAge" placeholder="最大年龄">
</div>
<div class="form-item">
<label for="gender">性别:</label>
<select id="gender" v-model="searchForm.gender">
<option value="">全部</option>
<option value="1">男</option>
<option value="2">女</option>
</select>
</div>
<div class="form-buttons">
<button type="submit" class="btn-search">搜索</button>
<button type="button" class="btn-reset" @click="resetForm">重置</button>
</div>
</form>
</div>
<!-- 数据表格 -->
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
<th>邮箱</th>
<th>部门</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in userList" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>{{ user.gender === 1 ? '男' : '女' }}</td>
<td>{{ user.email }}</td>
<td>{{ user.deptName }}</td>
<td>
<button @click="viewUser(user)">查看</button>
<button @click="editUser(user)">编辑</button>
</td>
</tr>
<tr v-if="userList.length === 0">
<td colspan="7" class="no-data">暂无数据</td>
</tr>
</tbody>
</table>
<!-- 分页组件 -->
<div class="pagination">
<button :disabled="pagination.current === 1" @click="changePage(1)">首页</button>
<button :disabled="pagination.current === 1" @click="changePage(pagination.current - 1)">上一页</button>
<span class="page-info">
{{ pagination.current }} / {{ pagination.pages }} 页,共 {{ pagination.total }} 条
</span>
<button :disabled="pagination.current >= pagination.pages" @click="changePage(pagination.current + 1)">下一页</button>
<button :disabled="pagination.current >= pagination.pages" @click="changePage(pagination.pages)">末页</button>
<select v-model="pagination.size" @change="handleSizeChange">
<option :value="10">10条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
<option :value="100">100条/页</option>
</select>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
// 搜索表单
searchForm: {
name: '',
minAge: '',
maxAge: '',
gender: ''
},
// 用户列表
userList: [],
// 分页信息
pagination: {
current: 1,
size: 10,
total: 0,
pages: 0
}
};
},
created() {
this.fetchUserList();
},
methods: {
// 获取用户列表
async fetchUserList() {
try {
const params = {
current: this.pagination.current,
size: this.pagination.size,
...this.searchForm
};
const response = await axios.get('/api/users/search', { params });
const result = response.data;
this.userList = result.records;
this.pagination.current = result.current;
this.pagination.size = result.size;
this.pagination.total = result.total;
this.pagination.pages = result.pages;
} catch (error) {
console.error('获取用户列表失败', error);
this.$message.error('获取用户列表失败');
}
},
// 搜索
handleSearch() {
this.pagination.current = 1; // 重置到第一页
this.fetchUserList();
},
// 重置表单
resetForm() {
this.searchForm = {
name: '',
minAge: '',
maxAge: '',
gender: ''
};
this.handleSearch();
},
// 切换页码
changePage(page) {
this.pagination.current = page;
this.fetchUserList();
},
// 修改每页条数
handleSizeChange() {
this.pagination.current = 1; // 重置到第一页
this.fetchUserList();
},
// 查看用户
viewUser(user) {
// 实现查看用户详情的逻辑
console.log('查看用户', user);
},
// 编辑用户
editUser(user) {
// 实现编辑用户的逻辑
console.log('编辑用户', user);
}
}
};
</script>
<style scoped>
.user-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.search-form {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.search-form form {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
}
.form-item {
display: flex;
flex-direction: column;
min-width: 200px;
}
.form-item label {
margin-bottom: 5px;
font-weight: bold;
}
.form-item input, .form-item select {
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.form-buttons {
display: flex;
gap: 10px;
}
.btn-search, .btn-reset {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-search {
background-color: #409eff;
color: white;
}
.btn-reset {
background-color: #909399;
color: white;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.data-table th, .data-table td {
padding: 12px 8px;
text-align: left;
border-bottom: 1px solid #ebeef5;
}
.data-table th {
background-color: #f5f7fa;
color: #606266;
}
.data-table button {
padding: 4px 8px;
margin-right: 5px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.data-table button:last-child {
background-color: #67c23a;
}
.no-data {
text-align: center;
color: #909399;
padding: 20px 0;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.pagination button {
padding: 6px 12px;
border: 1px solid #dcdfe6;
background-color: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.pagination select {
padding: 6px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.page-info {
color: #606266;
}
</style>
🔄 前后端交互流程
-
初始化加载:页面创建时调用
fetchUserList
方法,获取第一页数据 -
条件搜索:用户填写搜索表单并提交,前端将搜索条件与分页参数一起发送到后端
-
分页切换:用户点击分页按钮,更新当前页码并重新请求数据
-
每页条数:用户修改每页显示条数,重置到第一页并重新请求数据
这个前端实现与我们之前展示的后端Controller完美配合,通过RESTful API进行数据交互,实现了一个完整的分页查询功能。
🔬 分页原理
MyBatis-Plus分页插件的工作原理
-
拦截执行的SQL
-
先执行COUNT查询获取总记录数
SELECT COUNT(*) FROM user WHERE age > 20
-
再执行分页查询获取当前页数据
SELECT * FROM user WHERE age > 20 LIMIT 0, 10
-
将两次查询结果封装到Page对象中
🎯 实战案例
案例:带复杂条件的用户分页查询
@GetMapping("/users/search")
public PageResult<UserVO> searchUsers(UserSearchDTO searchDTO) {
// 创建分页对象
Page<User> page = new Page<>(searchDTO.getCurrent(), searchDTO.getSize());
// 构建查询条件
LambdaQueryWrapper<User> query = Wrappers.<User>lambdaQuery();
// 动态添加查询条件
query.like(StringUtils.isNotBlank(searchDTO.getName()), User::getName, searchDTO.getName())
.ge(searchDTO.getMinAge() != null, User::getAge, searchDTO.getMinAge())
.le(searchDTO.getMaxAge() != null, User::getAge, searchDTO.getMaxAge())
.eq(searchDTO.getGender() != null, User::getGender, searchDTO.getGender())
.between(
searchDTO.getStartTime() != null && searchDTO.getEndTime() != null,
User::getCreateTime,
searchDTO.getStartTime(),
searchDTO.getEndTime()
);
// 设置排序
if (StringUtils.isNotBlank(searchDTO.getSortField())) {
query.orderBy(true,
"asc".equalsIgnoreCase(searchDTO.getSortOrder()),
SFunction.parse(searchDTO.getSortField()));
} else {
query.orderByDesc(User::getCreateTime);
}
// 执行查询
Page<User> userPage = userService.page(page, query);
// 转换结果
Page<UserVO> voPage = userPage.convert(user -> {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
vo.setDeptName(getDeptName(user.getDeptId()));
return vo;
});
return PageResult.convert(voPage);
}
📝 小结
MyBatis-Plus分页插件的优势:
优势 | 说明 |
---|---|
使用简单 | 几行代码即可实现分页,无需手写SQL |
无侵入 | 不需要修改现有代码 |
高性能 | 自动优化COUNT查询,提高查询效率 |
可扩展 | 支持自定义分页查询,满足复杂业务需求 |
🔥 最佳实践:在Service层封装分页查询逻辑,Controller层只负责参数接收和结果返回,保持代码层次清晰。
⏭️ 下一步学习
-
ActiveRecord模式 - 了解更简洁的数据操作方式
-
代码生成器 - 自动生成各层代码
-
自定义ID生成器 - 学习如何自定义主键生成策略