ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MyBatis 动态分页 + 条件组合查询:基于反射实现通用分页封装

MyBatis 动态分页 + 条件组合查询:基于反射实现通用分页封装 一、业务场景说明后台流派、专辑、歌手、歌曲列表都具备下面能力支持多条件模糊搜索比如流派名称模糊查询分页展示数据页码切换上一页、下一页、数字页码点击搜索页码重置为第 1 页搜索条件保留页面回显切换分页条件不丢失通用封装流派、专辑、歌手直接复用同一套分页逻辑不重复写分页代码传统写法弊端每一张表Mapper 写分页 SQL、Service 写分页处理、计算 startNum、总条数大量复制粘贴改逻辑要改 N 处极易出 bug。二、通用 PageT分页实体类PageT 泛型封装存放分页全部信息当前页码、每页条数、起始下标、总记录数、总页数、查询结果集合。package cn.tx.util; import java.util.List; public class PageT { /** 每页显示条数 */ private int pageSize 5; /** 当前页码 */ private Integer pageNo 1; /** 查询数据库limit起始下标 (pageNo‑1)*pageSize */ private int startNum 0; /** 查询条件下总记录数 */ private int totalCount 0; /** 总页数自动计算 */ private int totalPage 1; /** 当前页数据集合 */ private ListT list; public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize pageSize; } public Integer getPageNo() { return pageNo; } public void setPageNo(Integer pageNo) { this.pageNo pageNo; } public int getTotalCount() { return totalCount; } public void setTotalCount(int totalCount) { this.totalCount totalCount; } /** 自动计算总页数向上取整 */ public int getTotalPage() { if(totalCount 0){ totalPage 1; }else { totalPage totalCount / pageSize; if (totalCount % pageSize ! 0) { totalPage; } } return totalPage; } public void setTotalPage(int totalPage) { this.totalPage totalPage; } /** 计算数据库limit起始行号由pageNo和pageSize自动算出 */ public int getStartNum() { return (pageNo - 1) * pageSize; } public void setStartNum(int startNum) { this.startNum startNum; } public ListT getList() { return list; } public void setList(ListT list) { this.list list; } }关键点getStartNum()不直接赋值通过 pageNo 自动计算对应 mysql limit startNum,pageSizegetTotalPage()自动向上取整总记录不能整除每页条数时多一页。三、Query 查询对象规范每个业务表创建 XXXQuery继承实体增加pageNo、startNum两个分页字段。以MtypeQuery.java流派查询对象举例package cn.tx.query; import cn.tx.model.Mtype; public class MtypeQuery extends Mtype { //接收页面传过来的当前页码 private Integer pageNo; //给MyBatis使用limit起始下标 private Integer startNum; public Integer getPageNo() { return pageNo; } public void setPageNo(Integer pageNo) { this.pageNo pageNo; } public Integer getStartNum() { return startNum; } public void setStartNum(Integer startNum) { this.startNum startNum; } }所有业务 Query 都必须提供getPageNo()、setStartNum(int)后面反射依赖这两个方法四、BaseDao 通用 DAO 接口定义package cn.tx.dao; import cn.tx.util.Page; import java.util.List; /** * Q查询对象(XXXQuery) * T数据库实体Model */ public interface BaseDaoQ, T { //新增、修改、删除省略重点看条件查询 ListT selectObjectByCondition(Q q); int selectObjectByConditionCount(Q q); } selectObjectByCondition(Q q)带 limit查询当前页数据 selectObjectByConditionCount(Q q)统计符合条件总记录数用于计算总页数五、BaseService 与反射实现通用分页问题不同 Query 对象属性名不一样但方法名固定getPageNo()、setStartNum(int num)。利用Java 反射调用这两个方法不需要强转具体 Query 类型实现一套代码适配全部业务。BaseService.javapackage cn.tx.service; import cn.tx.util.Page; public interface BaseServiceQ,T { PageT selectObjectByCondition(Q q); }BaseServiceImpl.javapackage cn.tx.service.impl; import cn.tx.dao.BaseDao; import cn.tx.util.Page; import java.lang.reflect.Method; import java.util.List; public class BaseServiceImplQ,T implements BaseServiceQ,T { protected BaseDaoQ, T baseDao; Override public PageT selectObjectByCondition(Q q) { PageT page new Page(); try { //1.反射拿到Query对象的getPageNo方法获取前端传过来的页码 Method getPageNoMethod q.getClass().getMethod(getPageNo); Integer pageNo (Integer) getPageNoMethod.invoke(q); page.setPageNo(pageNo); //2.通过page计算startNum反射调用setStartNum赋值给Query传给MyBatis int startNum page.getStartNum(); Method setStartNumMethod q.getClass().getMethod(setStartNum, int.class); setStartNumMethod.invoke(q, startNum); } catch (Exception e) { e.printStackTrace(); } //3.查询当前页数据带limit ListT dataList baseDao.selectObjectByCondition(q); //4.查询符合条件总记录条数 int total baseDao.selectObjectByConditionCount(q); //封装返回page对象 page.setList(dataList); page.setTotalCount(total); return page; } }完全不依赖具体的 MtypeQuery、AlbumQuery任何业务只要 Query 具备 getPageNo、setStartNum直接复用分页逻辑不用复制代码六、MyBatis Mapper.xml 动态 SQL 实现多条件 limit 分页以流派MtypeMapper.xml为例?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecn.tx.dao.MtypeMapper resultMap idBaseResultMap typecn.tx.model.Mtype id columnTID propertytid/ result columnTNAME propertytname/ result columnTDESC propertytdesc/ /resultMap !--分页查询数据多条件动态查询 limit分页 -- select idselectObjectByCondition parameterTypecn.tx.query.MtypeQuery resultMapBaseResultMap select TID,TNAME,TDESC from mtype where !-- 模糊查询注意模糊查询使用${}拼接字符串 -- if testtname ! null and tname! TNAME like %${tname}% /if /where limit #{startNum},#{pageSize} /select !-- 查询总记录数count -- select idselectObjectByConditionCount parameterTypecn.tx.query.MtypeQuery resultTypeint select count(*) from mtype where if testtname ! null and tname! TNAME like %${tname}% /if /where /select /mapper注意两点limit 语法limit 起始下标,每页条数顺序千万不能颠倒where标签自动去除多余 and/or。七、Controller 层代码接收 Query 对象做页码默认值处理搜索时重置 pageNo1。package cn.tx.controller; import cn.tx.query.MtypeQuery; import cn.tx.service.MtypeService; import cn.tx.util.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; Controller RequestMapping(/mtype) public class MtypeController { Autowired private MtypeService mtypeService; RequestMapping(/list) public String list(MtypeQuery mq, Model model){ //如果页面没有传pageNo默认第一页 if(mq.getPageNo() null){ mq.setPageNo(1); } //调用通用分页方法来自BaseServiceImpl自己没有写任何分页业务代码 PageMtype page mtypeService.selectObjectByCondition(mq); //回显把查询条件再传给页面搜索框保留输入的关键字 model.addAttribute(page,page); model.addAttribute(mq,mq); return songtype; } }八、JSP 页面实现列表、搜索、分页条、条件回显8.1 查询表单搜索条件回显重点% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % form idmtFrom action/mtype/list methodget input typehidden namepageNo idpageNo value${page.pageNo} !--搜索条件回显value取mq.tname搜索完不会清空输入框 -- input typetext nametname value${mq.tname} placeholder输入流派名称 button typebutton idsearchBtn搜索/button table c:forEach items${page.list} varmtype tr td${mtype.tid}/td td${mtype.tname}/td td${mtype.tdesc}/td /tr /c:forEach /table %--分页条引入抽成公共页面pagination.jsp--% jsp:include pagepagination.jsp/ /form script //搜索事件搜索必须重置页码为1 $(#searchBtn).click(function () { $(#pageNo).val(1); $(#mtFrom).submit(); }); /script8.2 公共分页组件 pagination.jsp可所有页面复用% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classpagination input typehidden idtotalPage value${page.totalPage} ul %--上一页 --% li idpreLi a hrefjavascript:void(0)Prev/a /li %--循环输出页码数字 --% c:forEach begin1 end${page.totalPage} varnum li class${page.pageNo num ? active:} a hrefjavascript:goPage(${num})${num}/a /li /c:forEach %--下一页 --% li idnextLi a hrefjavascript:void(0)Next/a /li /ul /div script var pageNo parseInt(${page.pageNo}); var totalPage parseInt(${page.totalPage}); //禁用判断逻辑 if(pageNo 1 pageNo totalPage){ $(#preLi).addClass(disabled); $(#nextLi).addClass(disabled); }else if(pageNo 1){ $(#preLi).addClass(disabled); }else if(pageNo totalPage){ $(#nextLi).addClass(disabled); } //跳转到指定页码 function goPage(pNum){ $(#pageNo).val(pNum); $(#mtFrom).submit(); } //上一页点击 $(#preLi a).click(function () { if(pageNo1){ goPage(pageNo - 1); } }); //下一页点击 $(#nextLi a).click(function () { if(pageNo totalPage){ goPage(pageNo 1); } }); /script关键点全部提交同一个 form 表单查询条件会自动携带分页切换搜索条件不会丢失。九、高频踩坑汇总坑 1${} 和 #{} 在模糊查询中的区别#{tname}预编译占位符会加引号不能直接写 like #{tname}${tname}字符串直接拼接适合%${tname}%模糊查询安全提醒$存在 SQL 注入风险项目内尽量对输入参数做过滤。坑 2MySQL limit 参数顺序写反错误写法limit #{pageSize},#{startNum}正确limit #{startNum},#{pageSize}第一个参数是起始行下标第二个是每页多少条顺序颠倒分页数据错乱。坑 3搜索之后分页条件还停留在旧页码现象我在第 5 页输入新关键词搜索还显示第 5 页搜出来数据为空。解决点击搜索按钮js 把隐藏域pageNo强制赋值为1再提交表单。坑 4分页条 disabled 状态逻辑写错当前是第一页上一页按钮禁用当前是最后一页下一页按钮禁用只有一页数据上一页、下一页全部禁用坑 5查询条件回显丢失现象搜索完成输入框清空。原因没有把 Query 对象mq传给 model页面value${mq.tname}拿不到值。Controller 中必须model.addAttribute(mq,mq);坑 6反射报错 NoSuchMethodException报错NoSuchMethod: getPageNo排查XXXQuery 实体是否写了getPageNo()、setStartNum(int num)方法名不能写错不能改名字反射严格匹配方法名。坑 7总页数计算错误最后一页数据不显示getTotalPage() 没有向上取整比如 53 条数据每页 5 条应该 11 页只算出 10 页。Page 类里面判断if(totalCount%pageSize !0) totalPage坑 8Query 对象 startNum 没有赋值给 MyBatis忘记反射调用setStartNum()导致 limit 第一个参数永远为 null查出来全部数据分页完全失效。十、完整开发流程总结创建数据库表逆向工程生成 model 实体新建 XXXQuery继承 model增加pageNo、startNum提供 get/setMapper.xml 写两套 SQL①带 limit 分页查询②count 统计总条数Mapper 接口继承BaseDaoService 接口继承BaseServiceServiceImpl 继承BaseServiceImpl不需要手写分页业务代码Controller 接收 XXXQuery设置 pageNo 默认值调用selectObjectByCondition(query)拿到 Page 对象jsp 页面写搜索表单回显查询条件引入公共 pagination.jsp 分页组件js搜索重置 pageNo1页码跳转提交表单。核心思想把重复不变的分页逻辑抽取到父类 BaseServiceImpl利用反射适配不同 Query 对象新增业务只需要写 MyBatis 的 SQLJava 层分页代码一行不用重复写。十一、拓展思考问为什么要用反射不用反射怎么实现通用分页答不使用反射可以用强转但是每个业务 ServiceImpl 都要手动写获取 pageNo、设置 startNum回到复制粘贴老路。反射可以做到父类一套代码全部业务复用。缺点反射有微小性能损耗后台管理系统数据量不大完全可以接受高并发互联网项目可以改用工具类传入 startNum。
返回列表