基于若依框架的动态分页逻辑的实现分析
如果让我们自己写分页查询的逻辑,应该怎么写呢?
在前端要完成分页的逻辑实际要做的工作还是挺多的。
- 分页查询应该支持查询参数的输入,对于一个有众多属性的列表,可能有很多查询参数,对于不同的参数类型,有的需要like模糊查找、有的需要equals相等、有的必须是一个between时间范围。
- 分页查询要有一个查询区的页面组件提供查询参数的输入,这个要考虑查询区的形式,提供平铺式的,还是提供一个简单的搜索输入框,要考虑是否记录历史值,使用get还是post发送请求。
- 分页组件做页面切换时需要实时改变分页参数,当页数较小时应该简便显示,当页数较多时应该详细显示组件。同时也要选择合适的条数。
- 表格的某个字段需要做筛选、排序、过滤时,也需要分页查询接口提供支持。
因此、分页查询功能的完成其实也不简单。
后端对于分页查询的逻辑编写,其实也不简单。
例如,对于众多变化的查询参数应该用什么请求对象来接收。用HashMap
?还是专门的DTO
请求对象?
支持众多可选查询参数的接口,有的属性会传入查询值,有的又没有传,难道我要写很多个if条件来判断?
分页参数又是怎么影响sql语句做到分页分段查询的?
接下来,我们分析下在若依框架中的分页逻辑是怎么完成的。
一、前端调用实现
(一)分页变量定义
将分页变量放入查询参数中,并将查询参数提取出来,防止嵌套过深。
const data = reactive({form: {},// 一般在查询参数中定义分页变量queryParams: {pageNum: 1,pageSize: 10,//userName: "", 其它查询参数},rules: {}
});
// 提取出变量,防止嵌套过深
const { queryParams, form, rules } = toRefs(data);
(二)查询区
一般分页查询是支持传入参数后进行搜索的,因此在前端页面有个填写查询参数的查询区。
<el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px"><el-form-item label="用户名称" prop="roleName"><el-inputv-model="queryParams.userName"placeholder="请输入用户名称"clearablestyle="width: 240px"@keyup.enter="handleQuery"/></el-form-item><!-- 其它查询参数项--><el-form-item><el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button><el-button icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
(三)分页组件
分页组件可以更改分页参数,并接收后台返回的总条数total,若total为0则不显示分页组件。
// 页面添加分页组件,传入分页变量
<paginationv-show="total>0":total="total":page.sync="queryParams.pageNum":limit.sync="queryParams.pageSize"@pagination="getList"
/>
(四)调用方法
/** 查询用户列表 */
function getList() {loading.value = true;// 调用后台方法,传入参数 获取结果listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {loading.value = false;// 接受数据集合userList.value = res.rows;// 接受数据总条数total.value = res.total;});
};
(五)前端接口方法
这里可以看到分页查询的请求是GET请求,而不是POST请求。
查询参数以?name1=value1&name2=value2
的形式发送请求,优点是可以进行浏览器缓存,方便记录查询日志。缺点是若查询参数很长很复杂,或需要保密,则还是用POST请求比较好。
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi";// 查询用户列表
export function listUser(query) {return request({url: '/system/user/list',method: 'get',params: query})
}
二、后端逻辑实现
(一)控制器Controller
对接前端接口的请求路径、方法的内容在Controller文件中。
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController@GetMapping("/list")public TableDataInfo list(SysUser user){startPage(); // 此方法配合前端完成自动分页List<SysUser> list = userService.selectUserList(user);return getDataTable(list);}
}
可以看到,前端控制器继承了一个基类控制器BaseController
,该基类控制器委托给分页工具类PageUtils
进行分页功能的处理。
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;public class BaseController
{protected final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 设置请求分页数据*/protected void startPage(){PageUtils.startPage();}/*** 设置请求排序数据*/protected void startOrderBy(){PageDomain pageDomain = TableSupport.buildPageRequest();if (StringUtils.isNotEmpty(pageDomain.getOrderBy())){String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());PageHelper.orderBy(orderBy);}}/*** 清理分页的线程变量*/protected void clearPage(){PageUtils.clearPage();}
}
(二)分页工具类
实际上,分页工具类又是委托给PageHelper插件进行分页处理。
import com.github.pagehelper.PageHelper;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.sql.SqlUtil;public class PageUtils extends PageHelper
{/*** 设置请求分页数据*/public static void startPage(){PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());Boolean reasonable = pageDomain.getReasonable();// 委托给分页插件处理PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}/*** 清理分页的线程变量*/public static void clearPage(){PageHelper.clearPage();}
}
分页插件中内部深层调用了PageMethod
中的startPage
方法
public abstract class PageMethod {// 线程变量protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();public static <E> Page<E> startPage(Object params) {Page<E> page = PageObjectUtil.getPageFromObject(params, true);Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}setLocalPage(page);return page;}// 设置线程变量public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}public static <T> Page<T> getLocalPage() {return (Page)LOCAL_PAGE.get();}
}
(三)表格数据处理
由于表格列表的按字段排序的功能也需要调用分页查询接口,这里封装了分页的属性名称的默认值。
public class TableSupport
{/** 当前记录起始索引 */public static final String PAGE_NUM = "pageNum";/** 每页显示记录数 */public static final String PAGE_SIZE = "pageSize";/** 排序列 */public static final String ORDER_BY_COLUMN = "orderByColumn";/** 排序的方向 "desc" 或者 "asc". */public static final String IS_ASC = "isAsc";/** 分页参数合理化 */public static final String REASONABLE = "reasonable";/** 封装分页对象 */public static PageDomain getPageDomain(){PageDomain pageDomain = new PageDomain();pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));return pageDomain;}public static PageDomain buildPageRequest(){return getPageDomain();}
}
然后创建分页对象PageDomain
交给分页插件PageHelper
使用。
// 分页参数的封装
public class PageDomain
{/** 当前记录起始索引 */private Integer pageNum;/** 每页显示记录数 */private Integer pageSize;/** 排序列 */private String orderByColumn;/** 排序的方向desc或者asc */private String isAsc = "asc";/** 分页参数合理化,例如:前端传递的参数是:pageNum=-1是不合理的 */private Boolean reasonable = true;
}
由于pagehelper的插件只对mybatis有用,如果自己需要对别的数据来源进行分页。
可以参照框架使用一部分功能,例如以下是对list集合进行分页:
public TableDataInfo getTodoItems(@RequestParam String searchValue) {/**第一步:pageNum和pageSize是从前端数据里传进来的分页对象的属性**/PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String userName = SecurityUtils.getUsername();/**第二步:过滤并获取数据**/List<TodoTaskDTO> result = taskCenterService.getTodoItem(searchValue);/**第四步:获取处理好的list集合**/int num = result.size();// 对列表进行分页result = result.stream().skip((pageNum - 1) * pageSize).limit(pageSize).collect(Collectors.toList());// 组装响应数据TableDataInfo rspData = new TableDataInfo();rspData.setCode(0);rspData.setRows(result);rspData.setTotal(num);return rspData;}
(四)实体基类
注意到实体类,例如SysUser
类既作为请求对象,又是ORM映射的数据库表实体对象。
这里共用了同一个对象,简化了类的数量。但是否会出现功能不适配呢?
public class SysUserController extends BaseController@GetMapping("/list")public TableDataInfo list(SysUser user){startPage(); // 此方法配合前端完成自动分页List<SysUser> list = userService.selectUserList(user);return getDataTable(list);}
}public class SysUser extends BaseEntity{private Long userId;private String userName;// 省略其它...
}
可以在请求URL的携带参数中看到,日期范围是含在params的数组里的。
http://localhost:8811/dev-api/system/user/list?pageNum=1&pageSize=10&userName=admin¶ms%5BbeginTime%5D=2025-01-01¶ms%5BendTime%5D=2025-01-03
SysUser
类继承了BaseEntity
基类,基类中包含如下属性:
- searchValue :搜索值
- createBy :创建人,记录创建该实体的对象或用户的标识符,可以是用户名、用户ID等。
- createTime :创建时间,使用 @JsonFormat 注解来指定日期时间格式,以便于JSON序列化/反序列化时保持一致的格式。
- updateBy :更新人,记录最后一次更新该实体的对象或用户的标识符。
- updateTime :更新时间,使用 @JsonFormat 注解来指定日期时间格式,以便于JSON序列化/反序列化时保持一致的格式。
- remark :提供额外的备注信息,可以用于描述实体的任何相关信息。
- params :请求参数,用于传递额外的参数或非结构化数据
public class BaseEntity implements Serializable
{private static final long serialVersionUID = 1L;/** 搜索值 */@JsonIgnoreprivate String searchValue;/** 创建者 */private String createBy;/** 创建时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime;/** 更新者 */private String updateBy;/** 更新时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date updateTime;/** 备注 */private String remark;/** 请求参数 */@JsonInclude(JsonInclude.Include.NON_EMPTY)private Map<String, Object> params;
}
(五)动态SQL
这里接收各种参数查询的功能是通过使用mybatis的动态sql的方式完成的。
应该通过代码生成工具自动生成以下代码,否则会有点繁琐。
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user uleft join sys_dept d on u.dept_id = d.dept_idwhere u.del_flag = '0'<if test="userId != null and userId != 0">AND u.user_id = #{userId}</if><if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if><if test="status != null and status != ''">AND u.status = #{status}</if><if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if><if test="params.beginTime != null and params.beginTime != ''"> <!-- 开始时间检索 -->AND date_format(u.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')</if><if test="params.endTime != null and params.endTime != ''"> <!-- 结束时间检索 -->AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')</if><if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))</if><!-- 数据范围过滤 -->${params.dataScope}
</select>
若使用MyBatis-Plus 的功能,那么不用生成动态sql的代码,但是可能需要用以下方式实现功能。
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
// 使用 lambda 表达式安全地添加条件
wrapper.like(StringUtils.isNotEmpty(userName), SysUser::getUserName, userName);
// 如果有其他条件,继续添加...
if (StringUtils.isNotEmpty(phonenumber)) {wrapper.like(SysUser::getPhonenumber, phonenumber);
}
if (status != null && !"".equals(status)) {wrapper.eq(SysUser::getStatus, status);
}// 处理日期范围查询
if (params.getBeginTime() != null) {wrapper.ge(SysUser::getCreateTime, params.getBeginTime());
}
if (params.getEndTime() != null) {wrapper.le(SysUser::getCreateTime, params.getEndTime());
}
// 执行查询
List<SysUser> userList = mapper.selectList(wrapper);
对于复杂查询,特别是涉及到多表关联、分页、排序、数据权限等功能时,MyBatis-Plus 虽然提供了很多便利的方法,但在某些情况下还是需要结合 XML 配置或手写 SQL 来满足特定需求。
(六)返回响应数据
获取到的数据是通过封装TableDataInfo
返回给前端的。
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;public class BaseController
{/*** 响应请求分页数据*/@SuppressWarnings({ "rawtypes", "unchecked" })protected TableDataInfo getDataTable(List<?> list){TableDataInfo rspData = new TableDataInfo();rspData.setCode(HttpStatus.SUCCESS);rspData.setMsg("查询成功");rspData.setRows(list);rspData.setTotal(new PageInfo(list).getTotal());return rspData;}
}@Schema(title = "表格分页数据对象")
public class TableDataInfo implements Serializable
{/** 总记录数 */@Schema(title = "总记录数")private long total;/** 列表数据 */@Schema(title = "列表数据")private List<?> rows;/** 消息状态码 */@Schema(title = "消息状态码")private int code;/** 消息内容 */@Schema(title = "消息内容")private String msg;
}