MyBatis分页:常规操作和隐藏坑点

引言

分页是Web应用中的"电梯",它帮助用户高效、有序地浏览大量信息,避免信息过载。

分页在Web应用中,就像建房时的楼层分隔,既保证了结构的稳固,也方便了居住者逐层探索,提升了体验。

一、MyBatis分页基础

MyBatis分页基础主要涉及两个核心概念:基本原理和分页参数。

1.1 MyBatis分页的基本原理

markdown 复制代码
-   MyBatis作为一个半自动的ORM(对象关系映射)框架,提供了灵活的SQL映射和数据操作能力。分页是数据库查询中常见的需求,MyBatis通过在SQL查询中添加限制条件(通常是`LIMIT`和`OFFSET`),来实现数据的分页显示。

-   基本原理是将整个数据集分成多个小的部分,每次只加载和显示一部分数据,这样可以减少单次加载的数据量,提高应用的性能和用户体验。

1.2 分页参数的介绍

markdown 复制代码
-   **页码(Page Number)** :表示用户当前查看的是第几页数据。页码通常从1开始计数。

-   **每页条数(Page Size)** :表示每一页显示的记录数。这个参数允许用户自定义他们希望在每一页看到的记录数量。

在MyBatis中实现分页,通常需要在Mapper接口中定义一个方法来执行分页查询,并在Mapper XML中编写相应的SQL语句,使用LIMITOFFSET来控制查询结果的数量和起始位置。

例如

xml 复制代码
<select id="selectUsers" resultType="User" useCache="true">
  SELECT * FROM users
  ORDER BY id
  LIMIT #{pageNumber}, #{pageSize}
</select>
<!-- #{offset}和#{pageSize}是分页参数,分别代表从第几条记录开始查询和每页显示多少条记录 -->

二、MyBatis分页的常规操作

MyBatis分页的常规操作包括以下几个步骤:

2.1 Mapper接口定义

  • 在Mapper接口中定义分页查询的方法。通常,这些方法会接收分页参数,如当前页码和每页显示的记录数。
java 复制代码
public interface UserMapper {
    // 分页查询方法,接收PageInfo对象作为参数
    List<User> pageFind(PageInfo pageInfo);
}

2.2 Mapper XML配置

  • 在对应的Mapper XML文件中编写分页查询的SQL语句。使用MyBatis的分页插件(如PageHelper)时,通常不需要修改SQL语句本身,只需在查询方法上添加@Select注解或在XML中使用<select>标签。
xml 复制代码
<!-- Mapper XML中的分页查询 -->
<select id="pageFind" resultType="User">
    SELECT * FROM users WHERE 1=1
    <if test="userId != null">AND id = #{userId}</if>
</select>
  • 如果不使用分页插件,需要手动在SQL中添加LIMITOFFSET子句:
xml 复制代码
<select id="pageFind" resultType="User">
    SELECT * FROM users LIMIT #{offset}, #{pageSize}
</select>

2.3 Service层调用

  • 在Service层中调用Mapper接口的分页查询方法,并传入分页参数。通常,这些参数会从前端请求中获取。
java 复制代码
public class UserService {
    private UserMapper userMapper;

    public List<User> getUsersWithPagination(int pageNum, int pageSize) {
        PageInfo pageInfo = new PageInfo(pageNum, pageSize);
        return userMapper.pageFind(pageInfo);
    }
}

注意:在Service层中,可能还需要处理分页结果,例如计算总记录数、总页数等,并将其封装到分页信息对象中,以便返回给前端。

2.4 Controller层处理

  • 在Controller层接收前端的分页请求,并调用Service层的方法获取分页数据。
java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping
    public PageResult<User> pageFind(@RequestParam int pageNum, @RequestParam int pageSize) {
        List<User> users = userService.pageFind(pageNum, pageSize);
        // 构造分页结果对象,包含数据列表、总记录数、总页数等
        return new PageResult<>(users, pageNum, pageSize);
    }
}

三、MyBatis分页插件-PageHelper

3.1 分页插件的作用

  • 分页插件的主要作用是自动处理SQL查询的分页逻辑,开发者只需编写普通的查询语句,插件会自动添加必要的分页参数(如LIMITOFFSET),从而实现分页功能。

  • 它减少了重复编写分页逻辑的工作量,提高了开发效率。

3.2 工作机制

  • 分页插件通常作为一个MyBatis的拦截器(Interceptor)实现,它拦截执行的SQL语句,然后根据传入的分页参数,动态地修改SQL语句,添加分页相关的条件。

  • 插件在执行查询之前,会先执行一个count查询来获取总记录数,以便计算总页数等信息。

3.3 PageHelper的使用

PageHelper是一个流行的MyBatis分页插件,它通过拦截MyBatis的执行器来实现分页功能。以下是使用PageHelper的步骤:

3.3.1 添加依赖

在项目的pom.xml文件中添加PageHelper的依赖项。

xml 复制代码
<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>5.1.2</version>
</dependency>

3.3.2 配置MyBatis

在MyBatis的配置文件mybatis-config.xml中添加PageHelper插件。

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <!-- myBatis本身的配置,开启下划线驼峰自动转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启日志输出 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

    <plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 4.0.0以后版本可以不设置该参数 -->
            <!--<property name="dialect" value="mysql"/>-->
            <!-- 该参数默认为false -->
            <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
            <!-- 和startPage中的pageNum效果一样-->
            <property name="offsetAsPageNum" value="true"/>
            <!-- 该参数默认为false -->
            <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
            <property name="rowBoundsWithCount" value="true"/>
            <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
            <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
            <property name="pageSizeZero" value="true"/>
            <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
            <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
            <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
            <property name="reasonable" value="true"/>
            <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
            <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
            <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
            <!-- 不理解该含义的前提下,不要随便复制该配置 -->
            <!--<property name="params" value="pageNum=start;pageSize=limit;"/>-->
            <!-- 支持通过Mapper接口参数来传递分页参数 -->
            <property name="supportMethodsArguments" value="true"/>
            <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
            <property name="returnPageInfo" value="check"/>
        </plugin>
    </plugins>
</configuration>

3.3.3 具体使用代码

核心就在于下面的PageHelper.startPage(paramDto.getPageNum(), paramDto.getPageSize());;在xml代码里面就不再需要手写limit语句。

java 复制代码
/**
 * 分页查询
 *
 * @param paramDto 参数
 * @return 分页数据
 */
@Transactional(propagation = Propagation.REQUIRED, readOnly = true, rollbackFor = Exception.class)
@Override
public MessageResponse pageFind(TrafficDto paramDto) {
    /* 分页查询-会将后面第一个select拦截分页 */
    PageHelper.startPage(paramDto.getPageNum(), paramDto.getPageSize());
    List<TrafficVo> list = trafficCalendarMapper.selectByParam(paramDto);

    // 封装结果集
    PageInfo<TrafficVo> pageInfo = new PageInfo<>(list);
    PageResult<TrafficVo> pageResult = new PageResult<>(pageInfo.getPageNum(), pageInfo.getTotal(), pageInfo.getPages(), list);
    return Response.success(pageResult);
}

四、干货:PageHelper分页bug

4.1 bug描述

在使用PageHelper分页插件的时候,如果我们的对象存在一对多的情况,那么分页就会主线条目不对;具体代码如下:

xml 复制代码
<!-- mybatis collection 分页bug处理案例 一对多 -->
<resultMap id="BaseResultMapVo" type="com.haha.admin.plus.entity.vo.AllowUserListVo">
    <id property="id" column="id" jdbcType="VARCHAR"/>
    <result property="email" column="email" jdbcType="VARCHAR"/>
    <collection property="list" ofType="com.haha.admin.plus.entity.vo.PremiumUserExtendOrderVo">
        <result property="extendId" column="extendId" jdbcType="VARCHAR"/>
        <result property="orderId" column="orderId" jdbcType="VARCHAR"/>
        <result property="orderTotalCost" column="orderTotalCost" jdbcType="DECIMAL"/>
    </collection>
</resultMap>

上面的代码查出来的分页数据,一旦list的size大于1,分页数据就会不准确;这个大家可以自己实验,这里主要说说解决办法。

4.2 解决方案

俗话说:毒蛇出没之处,七步之内必有解药;这个插件的bug也是稍微变动一下就OK了,咱一起瞧瞧。

  • 第一步:改造上面的resultMap仔细看实际只是多了一个select="selectExtendOrder" column="extendId"
xml 复制代码
<!-- mybatis collection 分页bug处理案例 一对多 -->
<resultMap id="BaseResultMapVo" type="com.haha.admin.plus.entity.vo.AllowUserListVo">
    <id property="id" column="id" jdbcType="VARCHAR"/>
    <result property="email" column="email" jdbcType="VARCHAR"/>
    <collection property="list" ofType="com.haha.admin.plus.entity.vo.PremiumUserExtendOrderVo" 
select="selectExtendOrder" column="extendId">
        <result property="extendId" column="extendId" jdbcType="VARCHAR"/>
        <result property="orderId" column="orderId" jdbcType="VARCHAR"/>
        <result property="orderTotalCost" column="orderTotalCost" jdbcType="DECIMAL"/>
    </collection>
</resultMap>
  • 第二步:selectExtendOrder是什么呢?咱直接看下面,这里可以看出这里是一个查询sql
xml 复制代码
<select id="selectExtendOrder"
        resultType="com.haha.admin.plus.entity.vo.PremiumUserExtendOrderVo">
    SELECT
        eo.extend_id as extendId,
        eo.order_id AS orderId,
        eo.order_total_cost AS orderTotalCost
    FROM
        premium_user_extend_order AS eo
    WHERE
        eo.extend_id = #{extendId}
</select>
  • 注意:BaseResultMapVo也是对应的一个查询,如下:
xml 复制代码
<select id="selectByEmailOrCardNoList" resultMap="BaseResultMapVo">
    SELECT
    a.id,
    a.email,
    FROM
    allow_user_list AS a
    WHERE 1 = 1
    <if test="paramDto.email != null and paramDto.email != ''">
        AND a.email LIKE CONCAT("%", #{paramDto.email}, "%")
    </if>
    ORDER BY a.created DESC
</select>

好了,到这里干货就结束了

五、总结

分页是我们做系统必不可少的一个操作,我曾经看到过一个代码,把表全部查到内存里面,然后在内存分页,所导致的结果就是OOM,这种骚操作咱就不要做了

各种ORM框架肯定都是支持真正的分页的,当然里面可能有一些隐藏的坑,咱且行且珍惜。

希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。

同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。

感谢您的支持和理解!

相关推荐
Tech Synapse几秒前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴1 分钟前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
xoxo-Rachel7 分钟前
(超级详细!!!)解决“com.mysql.jdbc.Driver is deprecated”警告:详解与优化
java·数据库·mysql
微信-since8119217 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails
JH307341 分钟前
Oracle与MySQL中CONCAT()函数的使用差异
数据库·mysql·oracle
蓝染-惣右介42 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
武子康2 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot
一二小选手2 小时前
【Mybatis】@Param注解 resultMap手动映射
java·mybatis
代码吐槽菌2 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫2 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai