MyBatis 探究 | 二、MyBatis应用分析与最佳实践(二)

示例代码:https://github.com/GiraffePeng/mybatis-simple

1、MyBatis 最佳实践

1.1、动态 SQL

1.1.1、为什么需要动态 SQL?

    项目实际开发中,由于前台传入的查询参数不同,需要写很多的 if else,还需要非常注意 SQL 语句里面的 and、空格、逗号和转移的单引号这些,拼接和调试 SQL 就是一件非常耗时的工作。
    MyBaits 的动态 SQL 就帮助我们解决了这个问题,它是基于 OGNL 表达式的。

1.1.2、动态标签有哪些?

    按照官网的分类,MyBatis 的动态标签主要有四类:if,choose (when, otherwise), trim (where, set),foreach。

1.1.2.1、if标签

    需要判断的时候,条件写在 test 中。下面的例子,当参数中的name不为空,则拼接上根据name查询数据的sql

<select id="selectListByPage" resultMap="BaseResultMap" parameterType="map">
  	select 
  	<include refid="Base_Column_List" />
    from member
    where 1=1
    	<if test="name != null and name != ''">
    		and name = #{name,jdbcType=VARCHAR}
    	</if>
  </select>
1.1.2.2、choose (when, otherwise)标签

    choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满则时,则执行 otherwise 中的sql。类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

    下面的例子,比如要根据创建时间的开始时间和结束时间查询member,并且开始时间和结束时间有可能不全部有值,我们可以采用choose。

<select id="selectListByPage" resultMap="BaseResultMap" parameterType="map">
  	select 
  	<include refid="Base_Column_List" />
    from member
    <where>
    	<choose>
    		<when test="startDate != null and endDate == null">
    			and create_time > #{startDate}
    		</when>
    		<when test="startDate == null and endDate != null">
    			and #{endDate} > create_time 
    		</when>
    		<when test="startDate != null and endDate != null">
    			and create_time between #{startDate} and #{endDate}
    		</when>
    		<otherwise>
    		</otherwise>
    	</choose>
    </where>
  </select>  
1.1.2.3、trim (where, set)

    需要去掉 where、and、逗号之类的符号的时候。
    下面的例子,trim 用来指定或者去掉前缀或者后缀,同时注意最后一个条件 detail 多了一个逗号,就是用 trim 去掉的:

  <insert id="insertSelective" parameterType="com.peng.entity.Member" >
    insert into member
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="name != null" >
        name,
      </if>
      <if test="text != null" >
        phone,
      </if>
      <if test="detail != null">
      	detail,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=BIGINT},
      </if>
      <if test="name != null" >
        #{name,jdbcType=VARCHAR},
      </if>
      <if test="phone != null" >
        #{phone,jdbcType=VARCHAR},
      </if>
      <if test="detail != null">
      	#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler},
      </if>
    </trim>
  </insert>
1.1.2.4、foreach

    需要遍历集合的时候。下面以批量插入数据为例。最终的拼装好的sql为:

insert into member(name,phone,detail) values (?,?,?),(?,?,?),(?,?,?);
<insert id="insertBatch" parameterType="list">
  	insert into member(name,phone,detail)
  	values
  	<foreach collection="list" item="item" separator=",">
  		(
  		#{name,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR},
  		#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
  		)
  	</foreach>
  </insert>

    动态 SQL 主要是用来解决 SQL 语句生成的问题。

1.2、批量操作

    我们在生产的项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况 ,当数据量非常大,比如超过几万条的时候,在 Java 代码中循环发送 SQL 到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话,即使我们使用了数据库连接池技术,对于数据库服务器来说也是不堪重负的。在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们生成语法正确的 SQL 语句。

1.2.1、批量插入

    批量插入的语法是这样的,只要在 values 后面增加插入的值就可以了。

insert into member(name,phone,detail) values (?,?,?),(?,?,?),(?,?,?);

    在 Mapper 文件里面,我们使用 foreach 标签拼接 values 部分的语句:

<insert id="insertBatch" parameterType="list">
  	insert into member(name,phone,detail)
  	values
  	<foreach collection="list" item="item" separator=",">
  		(
  		#{name,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR},
  		#{detail,jdbcType=VARCHAR,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
  		)
  	</foreach>
  </insert>

    Java 代码里面,直接传入一个 List 类型的参数。 效率要比循环发送 SQL 执行要高得多。最关键的地方就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。

1.2.2、批量更新

    批量更新的语法是这样的,通过 case when,来匹配 id 相关的字段值。

UPDATE member
	SET name = CASE id
	WHEN ? THEN
		?
	WHEN ? THEN
		?
	WHEN ? THEN
		?
END,
 	phone = CASE id
	WHEN ? THEN
		?
	WHEN ? THEN
		?
	WHEN ? THEN
		?
END,
 	detail = CASE id
	WHEN ? THEN
		?
	WHEN ? THEN
		?
	WHEN ? THEN
		?
END
WHERE
	id IN (?, ?, ?);

    利用mapper中的foreach标签来拼接sql。

  <update id="updateBatch" parameterType="list">
  	update member set 
  	name = 
  	<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
  		when #{item.id} then #{item.name}
  	</foreach>
  	,phone = 
  	<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
  		when #{item.id} then #{item.phone}
  	</foreach>
  	,detail = 
  	<foreach collection="list" item="item" index="index" separator=" " open="case id" close="end" >
  		when #{item.id} then #{item.detail,typeHandler=com.peng.mybatis.typehandler.JsonTypeHandler}
  	</foreach>
  	where id in 
  	<foreach collection="list" item="item" index="index" separator="," open="(" close=")" >
  		#{item.id}
  	</foreach>
  </update>

    批量删除也是类似的。

1.2.3、Batch Executor

    当然 MyBatis 的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的 SQL 语句过大。
    MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置才可以解决这个问题。

Caused by : com.mysql.jdbc.PacketTooBigException:Packet for query is too large 
(7188967 >4194304). You can change this value on the server by setting the 
max_allowed_packet' variable.

    在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种 BatchExecutor。

<setting name="defaultExecutorType" value="BATCH" />

    也可以在创建会话的时候指定执行器类型:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

    BatchExecutor 底层是对 JDBC ps.addBatch()的封装,原理是攒一批 SQL 以后再发送。
    那么三种执行器(Simple、Reuse、Batch)的区别是什么?

  • SimpleExecutor:是普通的执行器,在进行Mapper.save()方法时,相当于JDBC的stmt.execute(sql)。
  • ReuseExecutor:会重用预处理语句(prepared statements),在进行Mapper.save()方法时,相当于JDBC重用一条sql,再通过stmt传入多项参数值,然后执行stmt.executeUpdate()或stmt.executeBatch()
  • BatchExecutor:将重用语句并执行批量更新,在进行Mapper.save()方法时,相当于JDBC语句的 stmt.addBatch(sql),即仅仅是将执行SQL加入到批量计划。 所以此时不会抛出主键冲突等运行时异常,而只有临近commit前执行stmt.execteBatch()后才会抛出异常。

1.3、嵌套(关联)查询

    我们在查询业务数据的时候经常会遇到跨表关联查询的情况,
比如想查询用户并带出该用户的消费记录(一对多),或者查询用户并查询出该用户的钱包余额(一对一)。
在这里插入图片描述

    我们映射结果有两个标签,一个是 resultType,一个是 resultMap。
    resultType 是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字段无法直接映射,就要用 resultMap 来建立映射关系。
    对于关联查询的这种情况,通常不能用 resultType 来映射。用 resultMap 映射,要么就是修改 DTO(Data Transfer Object),在里面增加字段,这个会导致增加很多无关的字段。要么就是引用关联的对象,比如 Member里面包含了一个 PurchaseLog 对象,这种情况下就要用到关联查询(collection,或者association),MyBatis 可以帮我们自动做结果的映射。
    一对多的关联查询有两种配置方式:

1.3.1、嵌套结果
<resultMap id="BaseResultMap" type="com.peng.entity.Member" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="phone" property="phone" jdbcType="VARCHAR" />
    <result column="detail" property="detail" jdbcType="VARCHAR" typeHandler="com.peng.mybatis.typehandler.JsonTypeHandler"/>
    <!-- 嵌套结果 -->      
    <collection property="purchaseLogs"  javaType="java.util.ArrayList" ofType="com.peng.entity.PurchaseLog">
    	<result column="purchase_price" property="purchasePrice" jdbcType="DECIMAL"  />
        <result column="purchase_name" property="purchaseName" jdbcType="VARCHAR"  />
    </collection>
  </resultMap>
  
   <!-- 验证嵌套结果查询 -->      
   <select id="selectList" resultMap="BaseResultMap">
     select  a.id, a.name, a.phone, a.detail, b.purchase_price, b.purchase_name
     from member a 
     left join purchase_log b 
     on a.id = b.member_id
   </select>
1.3.2、嵌套查询
<resultMap id="BaseResultQueryMap" type="com.peng.entity.Member" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="phone" property="phone" jdbcType="VARCHAR" />
    <result column="detail" property="detail" jdbcType="VARCHAR" typeHandler="com.peng.mybatis.typehandler.JsonTypeHandler"/>
    <!-- 
        嵌套查询,会出现N+1的问题
        使用collection标明一对多的关系,通过声明的select中的内容,会在调用完member表的查询后,
    	再去调用com.peng.mybatis.mapper.PurchaseLogMapper.selectListByMemberId
    	方法来完成关联表的查询
    -->
    <collection property="purchaseLogs"  javaType="java.util.ArrayList" ofType="com.peng.entity.PurchaseLog"
    select="com.peng.mybatis.mapper.PurchaseLogMapper.selectListByMemberId" column="{memberId=id}">
    </collection>
  </resultMap>

    嵌套查询由于是分两次查询,当我们查询了用户信息之后,会再发送一条 SQL 到数据库查询用户消费记录信息。
    我们只执行了一次查询用户信息的 SQL(所谓的 1),如果返回了 N 条记录,就会再发送 N 条到数据库查询用户消费记录的信息(所谓的 N),这个就是我们所说的 N+1 的问题。 这样会白白地浪费我们的应用和数据库的性能。
    如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用用户消费记录的信息的时候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。这个可以参考上一篇的延迟加载部分:MyBatis应用分析与最佳实践(一)

1.4、MBG 与 Example

https://github.com/mybatis/generator
    我们在项目中使用 MyBaits 的时候,针对需要操作的一张表,需要创建实体类、 Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,这部分的工作是非常繁琐的。而大部分时候我们对于表的操作是相同的,比如根据主键查询、根据 Map 查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着有大量的重复工作。所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper 映射器、Mapper 接口,里面包含了我们需要用到的这些基本方法和 SQL 呢?
    MyBatis 提供了MyBatis Generator,简称 MBG。我们只需要修改一个配置文件,使用相关的 jar 包命令或者 Java 代码就可以帮助我们生成实体 类、映射器和接口文件。
    MBG 的配置文件里面有一个 Example 的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成 where 条件。
    原理:在实体类中包含了两个有继承关系的 Criteria,用其中自动生成的方法来构建查询条件。把这个包含了 Criteria 的实体类作为参数传到查询参数中,在解析 Mapper 映射器的时候会转换成 SQL 条件。
以member表为例,通过MBG生成后的类包括:

  • Member:对应表的实体类
  • MemberExample:相当于一个查询构建器,里面持有List<Criteria> oredCriteria的属性,Criteria相当于where参数的构建器,通过memberExample.createCriteria()方法来创建出Criteria,以member表为例,Criteria含有方法如下图所示:
    在这里插入图片描述
        我们看到是对表里面的字段封装的 ==,isNUll,isNotNull,like、not like、>、<等等。
  • MemberMapper.java:里面声明了基本的增删改查的方法。
  • MemberMapper.xml:自动生成了mapper.xml文件,里面包含基本的增删改查的sql声明。值得提到的是里面的查询方法selectByExample,代码如下:
<sql id="Example_Where_Clause" >
    <where >
      <foreach collection="oredCriteria" item="criteria" separator="or" >
        <if test="criteria.valid" >
          <trim prefix="(" suffix=")" prefixOverrides="and" >
            <foreach collection="criteria.criteria" item="criterion" >
              <choose >
                <when test="criterion.noValue" >
                  and ${criterion.condition}
                </when>
                <when test="criterion.singleValue" >
                  and ${criterion.condition} #{criterion.value}
                </when>
                <when test="criterion.betweenValue" >
                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                </when>
                <when test="criterion.listValue" >
                  and ${criterion.condition}
                  <foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
                    #{listItem}
                  </foreach>
                </when>
              </choose>
            </foreach>
          </trim>
        </if>
      </foreach>
    </where>
  </sql>
<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.sina.pojo.MemberExample" >
    select
    <if test="distinct" >
      distinct
    </if>
    <include refid="Base_Column_List" />
    from member
    <if test="_parameter != null" >
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null" >
      order by ${orderByClause}
    </if>
  </select>

    可以看到,通过我们传入的MemberExample,MBG通过引入动态SQL:Example_Where_Clause,来完成where相关字段的条件查询。其中_parameter 是mybatis内置的参数, _parameter:代表整个参数,单个参数:_parameter就是这个参数。多个参数:参数会被封装为一个map:_parameter就是代表这个map。 上面的代码 _parameter指得就是 MemberExample,目的在于比较通用的判断是否传递了Example。
    根据用户的id查询用户信息,我们实际使用可以这么写:

MemberMapper mapper = session.getMapper(MemberMapper.class);
MemberExample memberExample = new MemberExample();
Criteria criteria = memberExample.createCriteria();
criteria.andIdEqualTo(1L);
List<Member> memberList = mapper.selectByExample(memberExample);
System.out.println(memberList.get(0).getName());

    生成的语句:

select id, name, phone, detail from member WHERE ( id = ? ) 

1.5、翻页

    在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。
    逻辑翻页与物理翻页
    在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物理翻页是真正的翻页,比如 MySQL 使用 limit 语句,Oracle 使用 rownum 语句,SQL Server 使用 top 语句。

1.5.1、逻辑翻页

    MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和 limit(从第几条开始,查询多少条)。
    我们可以在 Mapper 接口的方法上加上这个参数,不需要修改 xml 里面的 SQL 语句。
    声明查询方法:

public List<Member> selectListByRowBounds(RowBounds rowBounds);
  <select id="selectListByRowBounds" resultMap="BaseResultMap" >
  	 select
  	 <include refid="Base_Column_List" />
  	 from member
  </select>
MemberMapper mapper = session.getMapper(MemberMapper.class);
//从第一页查询,查询2条
RowBounds rowBounds = new RowBounds(0, 2);
List<Member> members = mapper.selectListByRowBounds(rowBounds);
System.out.println(members.size());

    它的底层其实是对 ResultSet 的处理。它会舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条。

public class DefaultResultSetHandler implements ResultSetHandler {
	...
	private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    	DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    	ResultSet resultSet = rsw.getResultSet();
    	skipRows(resultSet, rowBounds);
    	while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      		ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      		Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      		storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    	}
  	}
	...
}

    很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用 subList(start,end)没什么区别),所以我们要用到物理翻页。

1.5.2、物理翻页

    物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
    第一种简单的办法就是传入参数(或者包装一个 page 对象),在 SQL 语句中翻页。

<select id="selectList" resultMap="BaseResultMap" parameterType="map">
  	 select
  	 <include refid="Base_Column_List" />
  	 from member
  	 limit #{current}, #{size}
  </select>

    第一个问题是我们要在 Java 代码里面去计算起止序号;第二个问题是:每个需要翻页的 Statement 都要编写 limit 语句,会造成 Mapper 映射器里面很多代码冗余。
    那我们就需要一种通用的方式,不需要去修改配置的任何一条 SQL 语句,只要在我们需要翻页的地方封装一下翻页对象就可以了。
    我们最常用的做法就是使用翻页的插件,这个是基于 MyBatis 的拦截器实现的,比如 PageHelper。

https://pagehelper.github.io/

MemberMapper mapper = session.getMapper(MemberMapper.class);
//物理分页
PageHelper.startPage(0, 10);
List<Member> members = mapper.selectListByPageHelper();
//计算分页所需参数 比如 总页数,总条数,是否有下一页等等
PageInfo<Member> pageInfo = new PageInfo<Member>(members);
System.out.println(JSON.toJSONString(pageInfo));

    别忘了配置分页插件,我们是使用的原生mybatis,需要指定插件

<plugins>
	<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

    PageHelper 是通过 MyBatis 的拦截器实现的,简单地来说,它会根据 PageHelper 的参数,改写我们的 SQL 语句。比如 MySQL 会生成 limit 语句,Oracle 会生成 rownum 语句,SQL Server 会生成 top 语句。
    我们可以看到在mapper的方法上并没有声明任何的参数,他为什么就会执行相应的物理分页的,首先我们查看PageHelper.startPage(0, 10)的源码

public abstract class PageMethod {
	protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
	/**
     * 设置 Page 参数
     *
     * @param page
     */
    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

	public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        //将page对象保存在threadLocal中
        setLocalPage(page);
        return page;
    }
}

    发现PageHelper将我们传递的当前页和一页多少条存入在Page对象里,同时把Page对象放入在ThreadLocal中。接着我们去看PageHelper声明的分页插件.

通过拦截mybatis的Executor执行器的query方法。
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
	......
	@Override
    public Object intercept(Invocation invocation) throws Throwable {
	...
	//调用方法判断是否需要进行分页,如果不需要,直接返回结果
	if (!dialect.skip(ms, parameter, rowBounds)) {
        //调用ExecutorUtil的pageQuery方法来执行查询
	    resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
    }else{
        //不进行分页处理
		resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
	}
	
	...
	}
}

    首先可以看到先是判断了是否需要分页调用dialect.skip(ms, parameter, rowBounds)方法,我们点进去查看方法。

@Override
    public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        ....
        //这里就是从刚才的ThreadLocal里获取page对象,
        //如果存在,代表本次查询是有分页需求的
        //如果为null,证明没有声明PageHelper.startPage(0, 10);不执行分页查询
        Page page = pageParams.getPage(parameterObject, rowBounds);
        
        if (page == null) {
            return true;
        } else {
            //设置默认的 count 列
            if (StringUtil.isEmpty(page.getCountColumn())) {
                page.setCountColumn(pageParams.getCountColumn());
            }
            autoDialect.initDelegateDialect(ms);
            return false;
        }
    }

    回到分页本身可以看到最终调用的为ExecutorUtil的pageQuery方法,我们看ExecutorUtil的源码。

public abstract class ExecutorUtil {
		public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //这里根据不同的数据库拼接不同的分页语句,
            //最后将原有的查询sql后拼接分页关键字,得到新的sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }
}

    我们进入dialect.getPageSql()方法查询是如何拼接分页语句的

public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {
	...
	@Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        //在这里,通过threadLocal中获取Page对象
        Page page = getLocalPage();
        //支持 order by
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        if (page.isOrderByOnly()) {
            return sql;
        }
        //这里声明了一个抽象方法
        return getPageSql(sql, page, pageKey);
    }
	...
}

    我们看到先去threadLocal中获取Page对象,然后调用getPageSql(sql, page, pageKey)的抽象方法,我们查看getPageSql的实现类,发现根据数据库类型的不同,有不同的实现类,以我们常见的mysql为例,我们进入MysqlDialect.
在这里插入图片描述

public class MySqlDialect extends AbstractHelperDialect {
	...
	@Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }
	...
}

    可以看到,就是通过在原有的sql上追加LIMIT关键字来完成的物理分页的逻辑。至此,我们可以了解到PageHelper的相关实现逻辑。至于为什么通过声明一个mybatis插件,在相关的方法执行前会被拦截呢,后续会提到。

1.6、Mybatis-plus

    https://mybatis.plus/guide
    MyBatis-Plus 是原生 MyBatis 的一个增强工具,可以在使用原生 MyBatis 的所有功能的基础上,使用 plus 特有的功能。
在这里插入图片描述

    MyBatis-Plus的核心功能:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
    内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

Mybatis-plus章节内容来源自:https://mybatis.plus/guide

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值