基于若依框架的动态分页逻辑的实现分析
如果让我们自己写分页查询的逻辑,应该怎么写呢?
在前端要完成分页的逻辑实际要做的工作还是挺多的。
- 分页查询应该支持查询参数的输入,对于一个有众多属性的列表,可能有很多查询参数,对于不同的参数类型,有的需要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-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
style="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则不显示分页组件。
// 页面添加分页组件,传入分页变量
<pagination
v-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;
/** 搜索值 */
@JsonIgnore
private 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 u
left join sys_dept d on u.dept_id = d.dept_id
where 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;
}
原文地址:https://blog.csdn.net/qq_40610003/article/details/145197484
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!