news 2026/9/1 4:11:05

EasyUI DataGrid分页实战:SSM项目中的参数、SQL与排错全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EasyUI DataGrid分页实战:SSM项目中的参数、SQL与排错全解

简介:这是一份Spring、SpringMVC、MyBatis与EasyUI整合的分页Demo,面向正在学习SSM框架整合及Web分页功能的Java开发者。项目采用Maven构建,完整演示了后端数据库查询、MyBatis映射、SpringMVC控制器处理与前端EasyUI分页组件之间的协作流程,可帮助理解分页参数传递、PageHelper或自定义拦截器等实现思路。压缩包共122个文件,以Java源码、XML配置、SQL脚本、PNG截图为主,另有JSP页面、JS、CSS及Eclipse项目配置等,充分覆盖了一个SSM+EasyUI项目的典型结构,压缩后仅1.68MB,轻量易用。目前已有260人学习下载。通过学习该Demo,开发者可快速掌握SSM框架整合要点、分页前后端联调方法,并基于现成的SQL和前端代码完成二次扩展,适合作为课设、毕设或企业入门项目的参考资料。 最近在维护一套spring + springmvc + mybatis + easyui拼起来的后台管理系统,代码是从上一个团队手里接过来的。功能本身不算复杂,但每次新增一个列表接口,最容易出问题的往往不是业务 SQL,而是分页。前端 EasyUI DataGrid 明明配置了 pagination,后端也写了分页查询,结果接口一调:total 是 0、第二页翻不动、搜索条件一加数据全乱套。这种问题说大不大,但排查起来要顺着页面、请求参数、Controller、Mapper SQL 一路找,没有经验确实会卡半天。下面就把这套组合从请求参数、JSON 返回格式到 MyBatis 分页的完整链路讲清楚,并把我自己联调踩过的坑和验证方法一起整理出来。适合正在接手 SSM 老项目,或者第一次在 SpringMVC 项目里给 EasyUI 表格加分页的同事参考。

1. 先把这个技术组合的分工和分页位置对齐

1.1 四层技术各自管什么

我对这套组合的理解是:Spring 管对象,SpringMVC 管 HTTP 请求和响应,MyBatis 管数据库访问,EasyUI 管页面表格展示。分页不是某一个层的功能,而是横跨四层的一套约定。很多项目失败,不是因为某一层写错了,而是四层之间的"暗号"没有对齐。

层级主要职责分页关心的事
前端 EasyUI DataGrid表格渲染、分页条当前页码、每页条数
SpringMVC Controller接收请求参数,返回 JSONpage/rows 参数怎么接收
Service / MyBatis数据查询、总数查询分页 SQL、count SQL
返回 JSON交给 DataGrid 渲染total 总条数、rows 当前页数组

1.2 "分页"容易在哪里走样

我见过不少同事在第一次接触 EasyUI 时,习惯性地用 Element UI 或 Bootstrap Table 的思维去思考分页,心里想的是 pageNum、pageSize,实际 EasyUI DataGrid 默认给后端传的是 page 和 rows。这两个词表面上只是命名不同,但如果大家都按自己的习惯写,前后端一对接就会出现"total 能拿到,表格只有第一页"、"后端按 pageNum 取参,全是 null"这类问题。

另一个容易走样的点,是页码从 0 开始还是从 1 开始。EasyUI 的默认分页条,页码是从 1 开始的;而后端很多同事写偏移量时直接(page - 1) * rows。如果前端传 1,后端计算成 0,那正好对;如果某个环节写成了(page - 2) * rows,那第一页就会直接从第 rows 条开始取,表现就是"第一页数据莫名少了前几条"。

所以,解决分页问题前,先别急着改代码。打开浏览器开发者工具,看看实际发出去的请求参数是什么,看看后端返回值是什么。把"谁传了什么、谁返回了什么"对齐,问题往往已经解决一半。

2. EasyUI DataGrid 的请求参数和返回 JSON 约定

2.1 前端 pagination 到底做了什么

一个最基础的 EasyUI DataGrid 配置大概是这样的:

$('#dg').datagrid({ url: '/user/list', method: 'post', pagination: true, pageSize: 20, pageList: [10, 20, 50, 100], columns: [[ { field: 'id', title: 'ID' }, { field: 'name', title: '姓名' } ]] });

当表格加载时,EasyUI 会自动把分页参数拼到请求里,实际请求大概是:

POST /user/list page=1&rows=20

注意,这里rows不是数据行数组,而是"每页显示多少条",和常说的 pageSize 一个意思。page是当前页码,从 1 开始。这两个参数名是 EasyUI 的默认约定,也是后端最容易搞混的地方。

如果需要在查询时带上搜索条件,可以直接调datagrid('load', {...})

$('#searchBtn').click(function () { $('#dg').datagrid('load', { name: $('#name').val(), deptId: $('#deptId').combobox('getValue') }); });

这样发出去的请求会变成:

page=1&rows=20&name=张三&deptId=10

2.2 后端必须返回 total + rows

EasyUI DataGrid 对返回 JSON 的要求非常固定:必须包含totalrows两个字段。total是符合条件的总条数,rows是当前页数据数组。一个标准的返回结构是:

{ "total": 100, "rows": [ { "id": 1, "name": "张三" }, { "id": 2, "name": "李四" } ] }

很多后端会把字段封装成recordslistdata,或者返回PageResult里包含pageNumpageSize,这些 EasyUI 默认都不认识。结果是表格能显示第一页数据,但底部分页栏的 total 永远是 0,翻页也会异常。

如果后端接口已经定了另一种结构,又不想改后端,可以在前端 DataGrid 配置里加loadFilter做映射:

$('#dg').datagrid({ url: '/user/list', pagination: true, loadFilter: function (data) { if (data && data.result) { return { total: data.result.total, rows: data.result.list }; } return data; } });

我的建议是:新写的接口直接按total + rows返回,别留映射这层。老项目如果改后端成本太高,再用loadFilter,但要在注释里写清楚为什么。

3. Controller 到 MyBatis:三种分页落地方式和取舍

3.1 手写 LIMIT 参数:最直白的入门写法

如果你的项目不想引额外依赖,手写 LIMIT 是最直观的方式。Controller 接收 page 和 rows,Service 计算 offset,Mapper 的 SQL 用 LIMIT 接收两个参数。

@RequestMapping("/list") @ResponseBody public Map<String, Object> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "20") Integer rows, UserQuery query) { int offset = (page == null || page < 1) ? 0 : (page - 1) * rows; query.setOffset(offset); query.setRows(rows); List<User> list = userMapper.selectPage(query); int total = userMapper.countPage(query); Map<String, Object> result = new HashMap<>(); result.put("total", total); result.put("rows", list); return result; }

Mapper XML 里大概是:

<sql id="queryCondition"> <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="deptId != null"> and dept_id = #{deptId} </if> </where> </sql> <select id="selectPage" resultType="User"> select * from t_user <include refid="queryCondition"/> order by id desc limit #{offset}, #{rows} </select> <select id="countPage" resultType="int"> select count(*) from t_user <include refid="queryCondition"/> </select>

这种方式最大的好处是没有魔法,SQL 完全可控。代价是每个列表接口都要写 selectPage 和 countPage 两段 SQL,还要保证它们查询条件完全一致。条件一旦多起来,<sql>片段的重要性就非常明显了。

不同数据库的方言也不一样。手写时至少要清楚自己的库用哪种写法:

数据库分页写法
MySQLLIMIT #{offset}, #{rows}
PostgreSQLLIMIT #{rows} OFFSET #{offset}
Oracle 12c+OFFSET #{offset} ROWS FETCH NEXT #{rows} ROWS ONLY
SQL Server 2012+OFFSET #{offset} ROWS FETCH NEXT #{rows} ROWS ONLY

3.2 PageHelper:省心,但有几条红线不能碰

如果项目里允许引入 PageHelper,我通常会优先用插件,因为省去了写 count 查询的麻烦,也避免了 count 和 list 查询条件不一致这类低级错误。

SSM 项目里,除了依赖,要在 MyBatis 配置里注册分页拦截器:

<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> </plugin> </plugins>

Java 代码里这样用:

PageHelper.startPage(page, rows); List<User> list = userMapper.selectList(query); PageInfo<User> pageInfo = new PageInfo<>(list); Map<String, Object> result = new HashMap<>(); result.put("total", pageInfo.getTotal()); result.put("rows", pageInfo.getList());

PageHelper 的原理可以简单理解成:startPage把分页参数放到 ThreadLocal,MyBatis 执行下一条 select 语句时,拦截器动态给这条 SQL 拼接 LIMIT,并额外生成一条 count 查询。正是因为这样,它有几条非常容易踩的红线:

  1. startPage后面必须紧跟第一条 Mapper 查询,中间不能插入其他无关查询。
  2. 同一个方法里如果有多条 select,只有紧跟startPage后的那一条会被分页,后面的不会。
  3. 如果在 SpringMVC 的 Controller 里用,要确保一个请求一个线程,不能随便开异步线程去查,否则 ThreadLocal 里的分页参数可能串到别的请求。
  4. 稳妥做法是在 finally 里调用PageHelper.clearPage(),防止异常场景下分页参数残留。

3.3 自定义拦截器:理解原理比拿来用更重要

有些老项目因为历史原因不想引第三方分页插件,或者需要兼容多套数据库,会自己写 MyBatis 拦截器。我也做过,但一般不建议在没有充分测试的情况下直接上生产。拦截器的思路大致是:实现org.apache.ibatis.plugin.Interceptor,拦截Executorquery方法,判断参数里是否包含分页对象;如果有,就先改写 BoundSql 生成 count 查询,再把原 SQL 拼接上当前方言的分页语句,最后把结果封装成带 total 的对象。

这个方案难点不在截获 SQL,而在"如何判断参数对象里的分页字段"、"如何同时处理 count 和 list 两条 SQL"、"如何兼容多方言"。说实话,做出来至少需要两三天的测试成本。如果你只是要解决一个简单表格的分页,手写 LIMIT 或 PageHelper 已经够了。自定义拦截器更适合需要沉淀公司内部框架、统一分页规范的大团队。

4. 联调中出现最多的四个分页症状和排查路径

4.1 症状对照表

我在项目里遇到最多的分页问题,基本可以收敛成下面四类:

症状最常见原因快速处理方向
total 显示 0,表格却有数据返回 JSON 没有 total 字段,或字段名不叫 total查看响应 JSON,改字段名或加 loadFilter
点第二页,数据和第一页完全一样后端没有根据 page 计算 offset,SQL 没拼接 LIMIT看 SQL 日志,确认 LIMIT 参数是否变化
搜索条件后翻页,total 恢复成全部数据搜索条件没有传到 count 查询检查 controller 接收参数和 count SQL 的 where 条件
请求 page/rows 总是为空Controller 接收的是 pageNum/pageSize,或前端没传统一参数名,或在 Controller 用 @RequestParam 显式声明

4.2 完整的排查链路

遇到分页问题,我建议按下面的顺序去看,而不是一上来就怀疑 MyBatis 配置:

  1. 打开浏览器开发者工具,切到 Network,重新点一次查询或翻页。
  2. 查看请求 URL 和 Payload,确认有没有pagerows,确认值是否在变。
  3. 查看响应 JSON,确认total是不是预期数字,rows是不是数组。
  4. 如果响应没问题但页面显示不对,再去看后端日志里 MyBatis 打印的 SQL 和参数。
  5. 检查 SQL 里的 LIMIT 参数:第一页应该是 0, 20,第二页应该是 20, 20。

这套顺序能帮你快速把问题锁定在"前端没传""后端没接""SQL 没分页""返回格式不对"四类中的某一类。每次排查都从网络面板开始,会节省大量时间。

4.3 搜索条件怎么才不会丢

搜索条件丢失是分页问题里最隐蔽的一类。很多时候第一页查询带着 name 条件没问题,翻到第二页,条件就丢了,或者 total 变成了不符合条件的数据总数。

原因通常是前端翻页时 EasyUI 只带了 page 和 rows,之前load传的条件没有保留。解决方法是把搜索条件做成一个独立对象,每次翻页和搜索都走datagrid('load', query),而不是直接修改 URL 或只调用reload

function loadGrid() { var query = { name: $('#name').val(), status: $('#status').combobox('getValue') }; $('#dg').datagrid('load', query); } $('#searchBtn').click(loadGrid);

后端这边,建议把查询条件封装成一个 Query 对象,Controller 方法参数直接写UserQuery query,这样 page、rows 和业务条件都在一个对象里,后续加条件也不会漏。

5. 让分页 SQL 现形:日志配置与大分页优化

5.1 MyBatis 日志怎么打开

要确认分页到底有没有生效,最实在的方法是看 MyBatis 打印的 SQL。在 logback 或 log4j 里把 Mapper 包日志级别调到 DEBUG:

<logger name="com.example.mapper" level="DEBUG"/>

如果项目用的是 MyBatis 全局配置,可以显式指定日志实现:

<settings> <setting name="logImpl" value="SLF4J"/> </settings>

打开后,执行列表查询时日志里应该能看到类似的输出:

Preparing: select * from t_user where name like concat('%', ?, '%') order by id desc limit ?, ? Parameters: 张三(String), 0(Integer), 20(Integer)

看到limit ?, ?和参数里的0, 20,说明分页 SQL 是生效的。如果日志里只有 select 没有 limit,说明前端 page/rows 参数没有传到 Mapper,或者 PageHelper 的 startPage 没有作用到这条查询上。

5.2 大分页不要硬翻

LIMIT 分页在数据量小时很轻松,但一旦出现LIMIT 100000, 20这种深分页,数据库仍然要把前 10 万行扫出来再丢掉,性能会肉眼可见地下降。对后台管理列表来说,最简单的限制是限制最大页码和页大小,比如 page 最大 100,rows 最大 100,超过就返回空或按最大值处理。

如果业务确实需要深翻,可以考虑"主键游标"方式。比如列表按 id 排序,记录上一页最后一条的 id,下一页查询用:

select * from t_user where id > #{lastId} order by id limit #{rows}

这样跳过的行数不再随页数增加,性能会稳定很多。缺点是 EasyUI 的分页条是基于页码的,要做成"加载更多"或"上一页/下一页"模式,对列表交互有一定改造。

如果不想改交互,还可以用子查询先取主键:

select u.* from t_user u inner join ( select id from t_user order by id limit #{offset}, #{rows} ) tmp on u.id = tmp.id order by u.id;

这种写法对 InnoDB 大表比较友好,可以先走覆盖索引拿到主键,再回表取完整行。

5.3 我习惯的公共分页返回结构

最后分享一个我一直在用的公共结构。把请求参数封装成一个 PageQuery,把返回结果封装成一个 PageResult,所有 Controller 都返回同一套格式,前端 EasyUI 只需配置一次,后续新页面几乎不需要再调分页:

public class PageQuery { private Integer page = 1; private Integer rows = 20; // getter / setter 略 } public class PageResult<T> { private long total; private List<T> rows; // getter / setter 略 }

Controller 里统一转成 Map 或者直接返回 PageResult 都行,只要最终 JSON 里是 total 和 rows 这两个字段。几个项目跑下来,这套小封装能省掉大量重复的分页联调时间。遇到分页问题也只需要盯着一个公共类排查,不用每个接口各查各的。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 4:08:52

Grok Bot接入实战:API调用、本地部署与虚拟信用卡代购风险解析

这次我们不谈 “Grok” 的宏大概念&#xff0c;直接看一个非常实际的问题&#xff1a;网上流传的 “Grok Bot 虚拟信用卡可代购” 到底靠不靠谱、能不能落地、怎么落地。这篇文章会同时覆盖两条线&#xff1a;一是 Grok Bot 这类大模型应用的真实接入方式&#xff0c;二是围绕 …

作者头像 李华
网站建设 2026/9/1 4:08:31

基于DSP28335的三电平SVPWM算法实现与调试

简介&#xff1a;本资源是面向电机控制工程师与电力电子方向嵌入式开发者的三电平空间矢量脉宽调制&#xff08;SVPWM&#xff09;算法工程实现&#xff0c;专为TI TMS320F28335 DSP平台设计&#xff0c;解决三相三电平逆变器在高效率、低谐波、强实时性场景下的PWM波形生成与硬…

作者头像 李华
网站建设 2026/9/1 4:07:13

毕业写论文不用乱氪金!一站式学术 AI,帮你省下查重会员钱

这几年写论文&#xff0c;市面上各类 AI 工具我几乎都体验过。有的打着免费旗号&#xff0c;写一点内容就锁字数诱导开会员&#xff1b;有的生成通篇套话&#xff0c;一眼就能看出是 AI 水文&#xff1b;更坑的是部分查重工具&#xff0c;自查重复率很低&#xff0c;学校正式检…

作者头像 李华
网站建设 2026/9/1 4:06:52

Replit智能路由与企业功能实战:从云端部署到灰度发布的完整指南

Replit 本周更新的重点&#xff0c;其实就落在两个词上&#xff1a;智能路由和企业功能。以前我们在 Replit 上部署一个 Web 应用&#xff0c;注意力大多放在“代码能不能跑、页面能不能开、数据存哪里”&#xff1b;这次更新的方向明显是往“流量怎么分发、多版本怎么切换、团…

作者头像 李华
网站建设 2026/9/1 4:06:20

LeetCode题库压缩包:从解压避坑到打造个人刷题工作区

简介&#xff1a;LeetCode全量题目与解答合集&#xff0c;适合准备技术面试、系统刷题或巩固算法基础的程序员使用。压缩包共972个文件&#xff0c;大小仅12.54MB&#xff0c;以Markdown解析笔记、Java源码和TXT题解说明为主&#xff0c;另含少量PDF、HTML辅助文档&#xff0c;…

作者头像 李华
网站建设 2026/9/1 4:05:56

开放世界多智能体自主数学发现:框架设计与工程实践

开放世界多智能体环境中的自主数学发现&#xff0c;简单说就是让多个大模型智能体组成一个科研小组&#xff0c;在一个持续变化、可交互、带工具调用的环境里&#xff0c;自动完成“提出问题、形成猜想、数值验证、符号证明、接受或推翻”的完整数学发现链路。它不是一个单一模…

作者头像 李华