ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Mybatis分页功能

2021-12-19 13:32:44  阅读:121  来源: 互联网

标签:功能 分页 Department rowBounds Mybatis id RowBounds name


Mybatis分页处理

 

最近有使用Mybatis3作为项目的ORM框架,在处理分页的时候,发现Mybatis本身自带RowBounds类,貌似利用它可来实现分页功能,到底效果如何,以及Mybatis内部是如何处理的,让我们搞一个Demo项目跑一下便可知晓。

项目类型:Java 控制台项目
Maven依赖:

<dependencies>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
  </dependency>

  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
  </dependency>

  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.3</version>
  </dependency>
</dependencies>

 

说明:lombok的引入是为了写Domain类的时候可以偷懒。

 

Maven资源处理:

<build>
  <resources>
    <resource>
      <directory>src/main/resources</directory>
      <includes>
        <include>**/*.*</include>
      </includes>
    </resource>
    <resource>
      <directory>src/main/java</directory>
      <includes>
       <include>**/*.xml</include>
       </includes>
      </resource>
  </resources>
</build>

 

说明:我们的Mapper XML文件放在与Mapper Java 接口文件同一个目录,因此需要把java目录下的xml也做资源处理过去,否则运行时找不到Mapper XML文件报错。


定义一个mybatis-config.xml
该文件为Mybatis的SqlSessionFactory实例化所必须的配置文件,放在resources目录下。

<?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>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
  <environments default="dev">
    <environment id="dev">
      <transactionManager type="JDBC"></transactionManager>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/study?useSSL=false" />
        <property name="username" value="****"/>
        <property name="password" value="****"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/alan/mybatis/consoleclient/dao/DepartmentMapper.xml" />
  </mappers>
</configuration>

 

说明:logImpl配置让我们可以在项目运行的时候实时看到执行的SQL语句,方便调试。在生产环境上可以关闭或更换为SLF4J等日志实现方式。
Mapper XML文件需要在配置里说明具体在哪里。


定义运行的main方法:

public class Application {

  public static void main(String[] args) throws IOException {
    final String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    System.out.println("using queryDepartments0 ------- ");

    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
      List<Department> departments = departmentMapper.queryDepartments0(null); for (Department department : departments) {
        System.out.println("===>" + department);
      }
    }

    System.out.println("using queryDepartments1 ------- ");

    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
       DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
       RowBounds rowBounds = new RowBounds(3, 4);
       List<Department> departments = departmentMapper.queryDepartments1(null, rowBounds);
       for (Department department : departments) {
          System.out.println("===>" + department);
       }
    }
  }
}

 


数据库准备:

create table if not exists Department (id int, name varchar)

 

数据内容:

Row: 1, 销售部
Row: 2, 研发部
Row: 3, 财务部
Row: 4, 人资部
Row: 5, 生产部
Row: 6, 工程部
Row: 7, 采购部
Row: 8, 后勤部
Row: 9, 流程与标准化部
Row: 10, 总裁办

 

Domain类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Department {
    private Integer id;
    private String name;
}

 


Dao类:

public interface DepartmentMapper {
    List<Department> queryDepartments0(String nameFuzzy);
    List<Department> queryDepartments1(String nameFuzzy, RowBounds rowBounds);
}

 

Mapper XML文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.alan.mybatis.consoleclient.dao.DepartmentMapper">
  <resultMap id="BaseResultMap" type="org.alan.mybatis.consoleclient.domain.Department">
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="name" property="name" jdbcType="VARCHAR" />
  </resultMap>

  <sql id="Base_Column_List">
     id, name
  </sql>

  <select id="queryDepartments0" resultMap="BaseResultMap" parameterType="java.lang.String">
    select <include refid="Base_Column_List"></include>
    from Department
    <where>
      <if test="param0 != null">
        name like #{param0}
      </if>
    </where>
  </select>

  <select id="queryDepartments1" resultMap="BaseResultMap" parameterType="java.lang.String">
    select <include refid="Base_Column_List"></include>
    from Department
    <where>
      <if test="param0 != null">
        name like #{param0}
      </if>
    </where>
  </select>
</mapper>

 

可以看到,在 Mapper XML 中定义的两个select (queryDepartments0, queryDepartments1)内容其实是一摸一样的,但是在 Java Mapper 的接口中定义的方法却是不一样的,一个为 queryDepartments0(String nameFuzzy),另一个为 queryDepartments1(String nameFuzzy, RowBounds rowBounds),明显的是,后面一个带了 RowBounds 参数。我就是想看看 RowBounds 参数会产生什么不一样的效果。


对于 queryDepartments0 ,我们传入fuzzy=null,实际上就是查询所有记录。运行我们的程序,可以看到结果与预期相符:

===>Department(id=1, name=销售部)
===>Department(id=2, name=研发部)
===>Department(id=3, name=财务部)
===>Department(id=4, name=人资部)
===>Department(id=5, name=生产部)
===>Department(id=6, name=工程部)
===>Department(id=7, name=采购部)
===>Department(id=8, name=后勤部)
===>Department(id=9, name=流程与标准化部)
===>Department(id=10, name=总裁办)

 

对于 queryDepartments1 ,我们传入fuzzy=null,RowBounds为offset=3,limit=4,实际上就是查询自第四条记录开始(因为第一条记录offset为0)的随后四条(limit=4)记录。运行我们的程序,可以看到结果与预期相符:

===>Department(id=4, name=人资部)
===>Department(id=5, name=生产部)
===>Department(id=6, name=工程部)
===>Department(id=7, name=采购部)

 

所以,我们知道了,如果想要查询中间某段的记录,我们可以在接口方法的最后一个参数加上RowBounds,Mybatis会自己帮我们返回合适的数据集,并不需要在Mapper XML中针对这个RowBounds参数做任何特殊的处理。那么Mybatis是如何做到的呢?我们可以看看Mybatis的日志输出:

using queryDepartments1 ------- 
Opening JDBC Connection
Checked out connection 210652080 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c8e4bb0]
==> Preparing: select id, name from Department 
==> Parameters: 
<== Columns: id, name
<== Row: 1, 销售部
<== Row: 2, 研发部
<== Row: 3, 财务部
<== Row: 4, 人资部
<== Row: 5, 生产部
<== Row: 6, 工程部
<== Row: 7, 采购部

 

从这里,我们可以推测,Mybatis应该没有特殊的去处理SQL,而是在SQL执行后,对数据库返回的结果集做了一次过滤再返回给了我们。其中大家可以去看看Mybatis源码 DefaultResultSetHandler,以及 DefaultCursor。
那么这种方式对于我们实际的生产应用说,应该是达不到要求的,尤其是在深分页的时候。比如说我们的数据库里有一万条的记录,我想查9800~9900的记录,按这种方式,需要查出9900条记录,而且前面9800条记录是没有用的。这种方式对于优化数据查询没有任何意义,同时对我们的程序来说,创建那么多不用的对象,加重了系统的资源负担。我们期望的是,如果我们传入RowBounds(9800, 100)的时候,Mybatis能自动帮我们把SQL加上Limit分页查询子句,这样才能真正做到优化查询,那么该如何做到呢?

这个时候Mybatis的插件就能帮上忙了。首先我们写一个Statement拦截器,安装在Mybatis中,以便我们可以在Mybatis执行前,根据当前执行主体内RowBounds的值来决定是否修正SQL(这里我们会用到一些反射的技术来获取和设置对象的field值):

@Intercepts({
  @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class })
})
public class MybatisStatementHandlerInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    RoutingStatementHandler statement = (RoutingStatementHandler) invocation.getTarget();
    StatementHandler handler = (StatementHandler) ReflectionUtil.getFieldValue(statement, "delegate");
    BoundSql boundSql = statement.getBoundSql();
    String sql = boundSql.getSql();
    if (handler instanceof PreparedStatementHandler) {
      // 获取RowBounds内部值,如果需要分页的情况下,修正SQL语句
      RowBounds rowBounds = (RowBounds) ReflectionUtil.getFieldValue(handler, "rowBounds");
      if (rowBounds.getLimit() > 0 && rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT) {
        System.out.println("------->yes fix sql with row bounds(" + rowBounds.getOffset() + "," + rowBounds.getLimit() + "): " + sql);
        String fixedSql = getLimitString(sql, rowBounds.getOffset(), rowBounds.getLimit());
        ReflectionUtil.setFieldValue(boundSql, "sql", fixedSql);
      }
    }
    return invocation.proceed();
  }

  private String getLimitString(String sql, int offset, int limit) {
    StringBuilder sqlBuilder = new StringBuilder();
    sqlBuilder.append(sql.trim());
    sqlBuilder.append(" LIMIT ");
    sqlBuilder.append(offset);
    sqlBuilder.append(", ");
    sqlBuilder.append(limit);
    return sqlBuilder.toString();
  }
}

 

然后我们在mybatis-config.xml中把该插件配置上去:

<plugins>
  <plugin interceptor="org.alan.mybatis.consoleclient.dao.MybatisStatementHandlerInterceptor">
  </plugin>
</plugins>

 

然后我们再运行一次 queryDepartments1 ,传入fuzzy=null,RowBounds为offset=3,limit=4,看看执行结果,输出为:

===>Department(id=7, name=采购部)

 

不对啊,按道理应该输出 4,5,6,7 四个记录才对啊,怎么只输出了一个7呢?看看打印的SQL语句,SQL也是被修正过了的呀:

using queryDepartments1 ------- 
Opening JDBC Connection
Checked out connection 1106131243 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@41ee392b]
------->yes fix sql with row bounds(3,4): select 
id, name

from Department
==> Preparing: select id, name from Department LIMIT 3, 4 
==> Parameters: 
<== Columns: id, name
<== Row: 4, 人资部
<== Row: 5, 生产部
<== Row: 6, 工程部
<== Row: 7, 采购部
<== Total: 4

 

SQL语句也没错,SQL执行的输出也毫无问题。怎么返回到客户端的结果就只剩一个7记录了呢?

回想一下前面我们说的,Mybatis处理RowBounds的方式是是在SQL执行后,对数据库返回的结果集做了一次过滤再返回给了我们。而这个处理是在Mybatis内部的ResultSetHandler中完成的。因此,我们要修正这个带来的意外效果,就是需要在Mybatis处理ResultSetHandler的阶段之前,把RowBounds对象修改为一个无限制的RowBounds对象,Mybatis就不会施加这个过滤效果了。那怎么修改呢?还是 和 前面的 MybatisStatementHandlerInterceptor 一样,使用拦截器完成,只不过这个拦截器拦截的是ResultSetHandler的handleResultSets方法:

@Intercepts({
  @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class MybatisResultSetHandlerInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();
    if (target instanceof DefaultResultSetHandler) {
      DefaultResultSetHandler resultSetHandler = (DefaultResultSetHandler)target;
      Object rowBounds = ReflectionUtil.getFieldValue(resultSetHandler, "rowBounds");
      if (rowBounds instanceof RowBounds) {
        RowBounds castedRowBounds = (RowBounds)rowBounds;
        // 如果RowBounds对象表示需要进行分页,那么表示就是需要去除这个待分页的RowBounds,设置为默认不
        // 需要分析的RowBounds对象。
        if (castedRowBounds.getLimit() > 0 && castedRowBounds.getLimit() < RowBounds.NO_ROW_LIMIT) {
          System.out.println("---->yes " + castedRowBounds.toString() + " forget it!");
          ReflectionUtil.setFieldValue(resultSetHandler, "rowBounds", new RowBounds());
        } else {
          System.out.println("---->no " + castedRowBounds.toString());
        }
      }
    }
    return invocation.proceed();
  }
}

 


然后我们在mybatis-config.xml中把该插件配置上去:

<plugins>
  <plugin interceptor="org.alan.mybatis.consoleclient.dao.MybatisStatementHandlerInterceptor">
  </plugin>
  <plugin interceptor="org.alan.mybatis.consoleclient.dao.MybatisResultSetHandlerInterceptor">
  </plugin>
</plugins>

 

然后我们再运行一次 queryDepartments1 ,传入fuzzy=null,RowBounds为offset=3,limit=4,看看执行结果,输出为:

===>Department(id=4, name=人资部)
===>Department(id=5, name=生产部)
===>Department(id=6, name=工程部)
===>Department(id=7, name=采购部)

 

看看SQL输出,

==> Preparing: select id, name from Department LIMIT 3, 4 

 

这个SQL修正结果与执行返回到客户端的数据集输出果,正是我们想要的。


==========================================================================
综上实验结果,如果想要在基于Mybatis ORM的项目中实施分页功能,只需要给安装两个Mybatis
插件即可:
一个拦截器用于修改SQL:MybatisStatementHandlerInterceptor
一个拦截去用于去除Mybatis内部对RowBounds的处理逻辑:MybatisResultSetHandlerInterceptor

这样的分页方案对于写Mapper XML的人来说是无感的,我们不需要在Mapper XML中处理任何分页相关代码。
只需要在Mapper的接口方法参数中添加最后一个RowBounds参数即可实现分页。

 

标签:功能,分页,Department,rowBounds,Mybatis,id,RowBounds,name
来源: https://www.cnblogs.com/hcfalan/p/15707272.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有