Mybatis(二)Mybatis的高级使用

本文深入探讨了Mybatis接口绑定、动态SQL、分页处理及MybatisPlus对比等高级特性,涵盖了注解绑定、XML映射、动态SQL标签、分页插件原理及MybatisPlus特性的应用,旨在提升数据库操作效率与代码维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列文章:
  Mybatis(一)Mybatis的基本使用
  Mybatis(二)Mybatis的高级使用
  Mybatis(三)集成Mybatis

一、Mybatis高级查询

1.1 高级结果映射

  在关系型数据库中,我们经常要处理一对一、一对多的关系。

1.1.1 一对一映射(association标签)

  一对一映射因为不需要考虑是否存在重复数据,因此使用起来很简单,而且可以直接使用MyBatis的自动映射。
  假设在RBAC权限系统中,一个用户只能拥有一个角色。示例:

//用户
public class SysUser{
	//用户角色
	private SysRole role;
	//...
}
  • 1、使用自动映射处理一对一关系
      使用自动映射就是通过别名让MyBatis自动将值匹配到对应的字段上,简单的别名映射如user_name对应userName。除此之外MyBatis还支持复杂的属性映射,可以多层嵌套,例如将role.role_name映射到role.roleName上。
      示例:
<select id="selectUserAndRoleById"
	resultType="tk.mybatis.simple.model.SysUser">
	select
		u.id,
		u.user_name userName,
		u.user_password userPassword,
		u.user_email userEmail,
		u.user_info userInfo,
		u.head_img headImg,
		u.create_time createTime,
		r.id "role.id",
		r.role_name "role.roleName",
		r.enabled "role.enabled"
	from sys_user u
	inner join sys_user_role ur on u.id = ur.user_id
	inner join sys_role r on ur.role_id =r.id
	where u.id = #{id}
</select>

  上述方法中sys_role查询列的别名都是“role.”前缀,通过这种方式将role的属性都映射到了SysUser的role属性上。

  • 2、使用resultMap配置一对一映射(1)
      示例:
<resultMap id="userRoleMap"type="tk.mybatis.simple.model.SysUser">
	<id property="id"column="id"/>
	<result property="userName"column="user_name"/>
	<result property="userPassword"column="user_password"/>
	<result property="userEmail"column="user_email"/>
	<result property="userInfo"column="user_info"/>
	<!--ro1e相关属性-->
	<result property="role.id"column="role id"/>
	<result property="role.roleName"column="role_name"/>
	<result property="role.enabled"column="enabled"/>
	<result property="role.createBy"column="create_by"/>
	<result property="role.createTime"column="role_create_time"jdbcType="TIMESTAMP"/>
</resultMap>

  MyBatis是支持resultMap映射继承的,因此可以简化上面的resultMap配置。示例:

<resultMap id="userRoleMap" extends="userMap"
	type="tk.mybatis.simple.model.SysUser">
	<result property="role.id"column="role_id"/>
	<result property="role.roleName"column="role_name"/>
	<result property="role.enabled"column="enabled"/>
	<result property="role.createBy"column="create_by"/>
</resultMap>
  • 3、使用resultMap配置一对一映射(2)
      当进行一对一映射时,更常见的做法是使用<association>标签。比如有这样的实体类:
/**
*书籍
*/
@Data
public class Book {
    private String id;
    private String name;
    private String author;
    private Double price;
    //出版社
    private Publisher pub;//一本书对应一个出版社
}

/**
*出版社
*/
@Data
public class Publisher {
    private String id;
    private String name;
    private String phone;
    private String address;
}

  那么查询一本书(包含出版社)的查询SQL可以使用<association>标签写,示例:

<!--配置关联实体类-->
<resultMap id="bookResultMap" type="com.entity.Book">
    <!--主键属性-->
    <id property="id" column="id"></id>
    <!--普通属性-->
    <result property="name" column="name"></result>
    <result property="author" column="author"></result>
    <result property="price" column="price"></result>
    <!--一对一映射-->
    <association property="pub" javaType="com.entity.Publisher">
        <id property="id" column="id"></id>
        <result property="name" column="name"></result>
        <result property="phone" column="phone"></result>
        <result property="address" column="address"></result>
    </association>
</resultMap>
<!--关联查询-->
<select id="selectAllBook" resultMap="bookResultMap">
    SELECT * FROM book e
    left JOIN publisher d ON e.publisher_id = d.id
</select>
1.1.2 一对多映射(collection标签)

  一对多映射都是使用<collection>标签进行的。
  在一对多的关系中,主表的一条数据会对应关联表中的多条数据。
  在RBAC权限系统中,一个用户拥有多个角色,每个角色又是多个权限的集合,所以要渐进式地去实现一个SQL,查询出所有用户和用户拥有的角色,以及角色所包含的所有权限信息的两层嵌套结果。
  一个用户有多个角色:

// 用户表
public class SysUser {
	//角色集合
	private List<SysRole>roleList;
	//...
}
<resultMap id="userRoleListMap"extends="userMap"
	type="tk.mybatis.simple.model.SysUser">
	<id property="id"column="id"/>
	<result property="userName"column="user_name"/>
	<result property="userPassword"column="user_password"/>
	<result property="userEmail"column="user_email"/>
	<collection property="roleList"columnPrefix="role_"
		javaType="tk.mybatis.simple.model.SysRole">
		<id property="id"column="id"/>
		<result property="roleName"column="role_name"/>
		<result property="enabled"column="enabled"/>
		<result property="createBy"column="create_by"/>
		<result property="createTime"column="create_time"jdbcType="TIMESTAMP"/>
	</collection>
</resultMap>

  由于resultMap可以继承,并且可以引用其他resultMap,因此上面的resultMap可以简化为:

<resultMap id="userRoleListMap"extends="userMap"	
	type="tk.mybatis.simple.model.Sysuser">
	<collection property="roleList"columnPrefix="role_"
		resultMap="tk.mybatis.simple.mapper.RoleMapper.roleMap"/>
</resultMap>

  对应的SQL为:

	<select id="selectAllUserAndRoles" resultMap="userRoleListMap">
	    select 
	    	u.id, 
	    	u.user_name, 
	        u.user_password,
	        u.user_email,
	        u.user_info,
	        u.head_img,
	        u.create_time,
	        r.id role_id,
			r.role_name role_role_name, 
			r.enabled role_enabled,
			r.create_by role_create_by,
			r.create_time role_create_time,
			p.id role_privilege_id,
			p.privilege_name role_privilege_privilege_name,
			p.privilege_url role_privilege_privilege_url
		from sys_user u
		inner join sys_user_role ur on u.id = ur.user_id
		inner join sys_role r on ur.role_id = r.id
		inner join sys_role_privilege rp on rp.role_id = r.id
		inner join sys_privilege p on p.id = rp.privilege_id
	</select>

1.2 使用枚举或其他对象

  在sys_role表中存在一个字段enabled,这个字段只有两个可选值,0为禁用,1为启用。但是在SysRole类中,我们使用的是Integer enabled,这种情况下必须手动校验enabled的值是否符合要求。在只有两个值的情况下,处理起来还比较容易,但是当出现更多的可选值时,对值进行校验就会变得复杂。因此在这种情况下,可以选择使用枚举来解决。
  对于枚举类型变量,Mybatis有两种类型转换器:EnumTypeHandler和EnumOrdinalTypeHandler。
  EnumTypeHandler:默认的枚举转换器,该转换器将枚举实例转换为实例名称的字符串,如将 SexEnum.MAN转换MAN。
  EnumOrdinalTypeHandler:将枚举实例的ordinal属性(int值,表示的含义是存入枚举类中的值的次序,从0开始)作为取值。如SexEnum.MAN的ordinal属性是0,SexEnum.WOMAN的ordinal属性是1。SexEnum定义示例:

public enum SexEnum {
	MAN, 
	WOMAN; 
}
1.2.1 使用MyBatis提供的枚举处理器

  定义Enabled枚举类:

public enum Enabled {
	disabled,//禁用
	enabled;//启用
}

  因为枚举除了本身的字面值外,还可以通过枚举的ordinal()方法获取枚举值的索引。在这个枚举类中,disabled对应索引0,enab1ed对应索引1。
  然后将原来的Integer类型的enabled字段,改成枚举类型:

	private Enabled enabled;

  在数据库中不存在一个和Enabled枚举对应的数据库类型,因此在和数据库交互的时候,不能直接使用枚举类型,在查询数据时,需要将数据库int类型的值转换为Java中的枚举值。在保存、更新数据或者作为查询条件时,需要将枚举值转换为数据库中的int类型。
  MyBatis在处理Java类型和数据库类型时,使用TypeHandler(类型处理器)对这两者进行转换。Mybatis为Java和数据库JDBC中的基本类型和常用的类型提供了TypeHandler接口的实现。MyBatis在启动时会加载所有的JDBC对应的类型处理器,在处理枚举类型时默认使用EnumTypeHandler处理器,这个处理器会将枚举类型转换为字符串类型的字面值并使用,对于Enabled而言便是"disabled"和"enabled"字符串。在这个例子中,由于数据库使用的是int类型,所以在Java的String类型和数据库int类型互相转换时,肯定会报错。
  除了这个EnumTypeHandler枚举类型处理器,MyBatis还提供了另一个EnumOrdinalTypeHandler处理器,这个处理器使用枚举的索引进行处理,可以解决此处遇到的问题。想要使用这个处理器,需要在mybatis-config.xml中添加如下配置。

<typeHandlers>
	<typeHandler
		javaType="tk.mybatis.simple.type.Enabled"
		handler="org.apache.ibatis.type.EnumordinalTypeHandler"/>
</typeHandlers>

  这样配置后,就可以实现disabled/enabled和0/1之间的互相转换。

1.2.2 使用自定义的类型处理器(实现TypeHandler接口)

  有时,值既不是枚举的字面值,也不是枚举的索引值,这种情况下就需要自己来实现类型处理器了。修改枚举类Enabled:

public enum Enabled {
	enabled(1), //启用
	disabled(0);//禁用
	
	private final int value;

	private Enabled(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}
}

  要实现自定义类型处理器,有两种方式:

  1. 实现TypeHandler接口。
  2. 继承BaseTypeHandler类。

  以实现接口的方式实现自定义类型转换器为例。具体做法为:实现TypeHandler的setParameter()和getResult()接口方法。TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParametergetResult两个方法,分别代表设置sql问号占位符参数和获取列查询结果。

  • 1、实现自定义的类型处理器
ublic class EnabledTypeHandler implements TypeHandler<Enabled> {
	private final Map<Integer, Enabled> enabledMap = new HashMap<Integer, Enabled>();

	public EnabledTypeHandler() {
		for(Enabled enabled : Enabled.values()){
			enabledMap.put(enabled.getValue(), enabled);
		}
	}
	
	public EnabledTypeHandler(Class<?> type) {
		this();
	}

	@Override
	public void setParameter(PreparedStatement ps, int i, Enabled parameter, JdbcType jdbcType) throws SQLException {
		ps.setInt(i, parameter.getValue());
	}

	@Override
	public Enabled getResult(ResultSet rs, String columnName) throws SQLException {
		Integer value = rs.getInt(columnName);
		return enabledMap.get(value);
	}

	@Override
	public Enabled getResult(ResultSet rs, int columnIndex) throws SQLException {
		Integer value = rs.getInt(columnIndex);
		return enabledMap.get(value);
	}

	@Override
	public Enabled getResult(CallableStatement cs, int columnIndex) throws SQLException {
		Integer value = cs.getInt(columnIndex);
		return enabledMap.get(value);
	}

}

  重写的4个方法的含义:

  setParameter:将Java对象的参数设置到PreparedStatement中,通常用于将参数绑定到SQL语句中的占位符。
  getResult(ResultSet rs, String columnName):从ResultSet对象中获取结果,根据列名columnName获取对应的列的值。
  getResult(ResultSet rs, int columnIndex):从ResultSet对象中获取结果,根据列索引columnIndex获取对应的列的值。
  getResult(CallableStatement cs, int columnIndex):从CallableStatement对象中获取结果,根据列索引columnIndex获取对应的列的值。通常,这种情况用于从存储过程中获取结果。

  • 2、注册自定义的类型处理器
      EnabledTypeHandler实现了TypeHandler接口,并且针对4个接口方法对Enabled类型进行了转换。
      实现了自定义类型处理器后,还需要在mybatis-config.xml中进行配置:
	<typeHandlers>
		<typeHandler 
			javaType="tk.mybatis.simple.type.Enabled" 
			handler="tk.mybatis.simple.type.EnabledTypeHandler"
			jdbcType="TINYINT"/>
	</typeHandlers>
  • 3、在Mapper.xml中使用自定义的类型处理器
      使用方式,和使用Mybatis自带的类型处理器一样。示例:
  <result property="localUrl" column="local_url" 
  	  typeHandler="com.chen.handler.StringToListTypeHandler" />
1.2.3 对Java8日期(JSR-310)的支持

  MyBatis从3.4.0版本开始增加了对Java8日期(JSR-310)的支持。如果使用3.4.0及以上版本,只需要在Maven的pom.xml中添加如下依赖即可。

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-typehandlers-jsr310</artifactId>
	<version>1.0.2</version>
</dependency>

  如果使用比3.4.0更早的版本,若要支持Java8日期,还需要在mybatis–config.xml中添加如下配置。

<typeHandlers>
	<typeHandler handler="org.apache.ibatis.type.InstantTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.LocalDateTimeTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.LocalDateTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.LocalTimeTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.offsetDateTimeTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.offsetTimeTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.ZonedDateTimeTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.YearTypeHandler"/>
	<typeHandler handler="org.apache.ibatis.type.MonthTypeHandler"/>
</typeHandlers>

  增加上面这些配置后,就可以在Java中使用新的日期类型了。

1.2.4 Mybatis自带的类型处理器
类型处理器Java类型JDBC类型
BooleanTypeHandlerBoolean,boolean数据库兼容的BOOLEAN
ByteTypeHandlerByte,byte数据库兼容的NUMERIC或BYTE
ShortTypeHandlerShort,short数据库兼容的NUMERIC或SHORT INTEGER
IntegerTypeHandlerInteger,int数据库兼容的NUMERIC或INTEGER
LongTypeHandlerLong,long数据库兼容的NUMERIC或LONG INTEGER
FloatTypeHandlerFloat,float数据库兼容的NUMERIC或FLOAT
DoubleTypeHandlerDouble,double数据库兼容的NUMERIC或DOUBLE
BigDecimalTypeHandlerBigDecimal数据库兼容的NUMERIC或DECIMAL
StringTypeHandlerStringCHAR、VARCHAR
DateTypeHandlerjava.util.DateTIMESTAMP
DateOnlyTypeHandlerjava.util.DateDATE
TimeOnlyTypeHandlerjava.util.DateTIME
SqlTimestampTypeHandlerjava.sql.TimestampTIMESTAMP
SqlDateTypeHandlerjava.sql.DateDATE
SqlTimeTypeHandlerjava.sql.TimeTIME
EnumTypeHandlerEnumeration TypeVARCHAR任何兼容的字符串类型,存储枚举的名称(而不是索引)
EnumordinalTypeHandlerEnumeration Type任何兼容的NUMERIC或DOUBLE类型,存储枚举的索引(而不是名称)。

二、缓存

  使用缓存可以使应用更快地获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。
  缓存分为一级缓存和二级缓存。一级缓存:线程级别的缓存,sqlSession级别的缓存;二级缓存:全局范围的缓存。默认情况下一级缓存是开启的,而且是不能关闭的。

  一般提到MyBatis缓存的时候,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制,因此很少会提到。

  Mybatis缓存机制示意图:

2.1 一级缓存(默认开启)

  MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。
  一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取。一级缓存最多缓存1024条SQL

  当查询过的数据有INSERT、UPDATE、DELETE时,都会清除一级缓存。即第一次和第二次相同的SQL查询之间,执行DML(增删改),则一级缓存会被清空,第二次查询相同SQL仍然会走数据库。

  • 一级缓存会被清除的情况
      1、在同一个SqlSession下执行增删改操作时,会清除一级缓存;
      2、SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存;
      3、对mapper.xml中的某个CRUD标签,设置属性flushCache=true,即强制刷新缓存。默认情况下,增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。查询默认flushCache=false。查询时的一级缓存可以通过flushCache属性关闭,但是一般不这么做,因为清除一级缓存的话,每次都要查数据库。示例:
<select id="selectById" flushCache="true" resultMap="userMap">
	select * from sys_user where id =#(id}
</select>

  4、传递的参数发生了变化。
  5、在两次查询期间,手动去清空缓存(sqlSession.clearCache()),也会让缓存失效。
  6、在全局配置文件中如下设置:

	<setting name="localCacheScope" value="STATEMENT"/>`

  localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT。当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

2.2 Mybatis的二级缓存(默认关闭)

  MyBatis的二级缓存可以理解为存在于SqlSessionFactory的生命周期中。

2.2.1 配置二级缓存

  在MyBatis的全局配置settings中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是false,初始状态为关闭状态

  • 开启二级缓存步骤
      步骤1:全局配置,在mybatis-config.xml中配置如下:
<settings>
	<setting name="cacheEnabled"value="true"/>
</settings>

  步骤2:命名空间配置。MyBatis的二级缓存是和命名空间绑定的,所以在二级缓存的全局配置开启的情况下,具体的XxxMapper.xml文件中只需添加<cache/>元素即可开启二级缓存。示例:

<?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="tk.mybatis.simple.mapper.UserMapper">
	<cache/>
	<!--其他配置-->
</mapper>

  步骤3:要缓存的实体类需要支持序列号,即实现SerializedCache接口。

  • 二级缓存属性
      默认的二级缓存会有如下效果:

  映射语句文件中的所有SELECT语句将会被缓存。
  映射语句文件中的所有NSERT、UPDATE、DELETE语句会刷新缓存。
  缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
  根据时间表(如no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用。
  缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

  所有的这些属性都可以通过缓存元素的属性来修改,示例:

<cache
	eviction="FIFO"
	flushInterval="60000"
	size="512"
	readonly="true"/>

  这个配置的含义:创建了一个FIF0缓存,并每隔60秒刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的。
  <cache>标签可以配置的几个属性:
  1、eviction(收回策略)

  LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
  FIFO(先进先出):按对象进入缓存的顺序来移除它们。
  SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
  WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。

  2、flushInterva1(刷新间隔)。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
  3、size(引用数目)。可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024。
  3、readOnly(只读)

  设置为true:只读缓存,会给所有调用这返回缓存对象的相同实例,因此这些对象不能被修改。
  设置为false:读写缓存,会返回缓存对象的拷贝(序列化实现),这种方式比较安全,默认值。

2.2.2 使用二级缓存

  Mybatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。
  SerializedCache要求所有被序列化的对象必须实现Serializable接口,因此和数据库中表对应的实体类需要实现Serializable接口,示例:

public class SysRole implements Serializable {
	private static final long serialVersionUID = 6320941908222932112L:
	//其他属性和getter,setter方法
}

  然后Mybatis的二级缓存就生效了。

2.3 其他二级缓存框架

  MyBatis默认提供的缓存实现是基于Map实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用MyBatis的二级缓存,还可以选择一些类似EhCache的缓存框架或Redis缓存数据库等工具来保存MyBatis的二级缓存数据。

2.3.1 集成EhCache缓存

  EhCache是一个纯粹的Java进程内的缓存框架,EhCache主要的特性:

快速。
简单。
多种缓存策略。
缓存数据有内存和磁盘两级,无须担心容量问题。
缓存数据会在虚拟机重启的过程中写入磁盘。
可以通过RMI、可插入API等方式进行分布式缓存。
具有缓存和缓存管理器的侦听接口。
支持多缓存管理器实例以及一个实例的多个缓存区域。

  因为以上诸多优点,MyBatis项目开发者最早提供了EhCache的MyBatis二级缓存实现,该项目名为ehcache-cache。
  接下来就介绍一下使用MyBatis官方提供的ehcache-cache集成EhCache缓存框架。

  • 1、添加依赖
		<dependency>
		    <groupId>org.mybatis.caches</groupId>
		    <artifactId>mybatis-ehcache</artifactId>
		    <version>1.0.3</version>
		</dependency>
  • 2、配置EhCache
      在src/main/resources目录下新增ehcache.xml文件。配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd"
    updateCheck="false" monitoring="autodetect"
    dynamicConfig="true">
    
    <diskStore path="D:/cache" />
            
	<defaultCache      
		maxElementsInMemory="3000"      
		eternal="false"      
		copyOnRead="true"
		copyOnWrite="true"
		timeToIdleSeconds="3600"      
		timeToLiveSeconds="3600"      
		overflowToDisk="true"      
		diskPersistent="true"/> 
</ehcache>

  重要的是copyOnRead和copyOnWrite属性。
  copyOnRead:判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true,那就是可读写缓存,每次读取缓存时都会复制一个新的实例。
  copyOnWrite:判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使用只读缓存,可以不配置这两个属性,使用默认值false即可。

  • 3、修改具体的XxxMapper.xml中的缓存配置
      ehcache-cache提供了如下2个可选的缓存实现:EhcacheCache、LoggingEhcache。
      在这两个缓存中,第二个是带日志的缓存,由于MyBatis初始化缓存时,如果Cache不是LoggingEhcache,MyBatis便会使用Logging Ehcache装饰代理缓存,所以上面两个缓存使用时并没有区别,都会输出缓存命中率的日志。
      接下来修改具体的XxxMapper.xml文件,示例:
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
	<!--其他配置-->
</mapper>

  只通过设置type属性就可以使用EhCache缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,所以配置使用EhCache缓存的Mapper映射文件都会有一个以映射文件命名空间命名的缓存。如果想针对某一个命名空间进行配置,需要在ehcache.xml中添加一个和映射文件命名空间一致的缓存配置,示例:

<cache
	name="tk.mybatis.simple.mapper.RoleMapper"
	maxElementsInMemory="3000"
	eternal="false"
	copyOnRead="true"
	copyonWrite="true"
	timeToIdleSeconds="3600"
	timeToLiveSeconds="3600"
	overflowToDisk="true"
	diskPersistent="true"/>
2.3.2 集成Redis缓存

  MyBatis项目开发者提供了Redis的MyBatis二级缓存实现,该项目名为redis-cache。
  接下来使用MyBatis官方提供的redis–cache集成Redis数据库。

  • 1、添加依赖
<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-redis</artifactId>
	<version>1.0.0-beta2</version>
</dependency>
  • 2、配置Redis
      在src/main/resources目录下新增redis.properties文件。示例:
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=

  上面这几项是redis-cache项目提供的可以配置的参数,这里配置了服务器地址、端口和超时时间。

  • 3、修改XxxMapper.xml中的缓存配置
      redis-cache提供了l个MyBatis的缓存实现:RedisCache。修改示例:
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.redis.RedisCache"/>
	<!--其他配置-->
</mapper>

  RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。
  当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCahca缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存,这种方式会消耗更多的服务器资源。如果使用类似Redis的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。

2.4 使用二级缓存的注意事项

2.4.1 脏数据的产生和避免(二级缓存和namespace绑定,数据修改时多表关联查询会产生脏数据)

  二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据。
  MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计,使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。
  避免脏数据出现时就需要用到参照缓存。当某几个表可以作为一个业务整体时,通常是让几个会关联的ER表同时使用同一个二级缓存,这样就能解决脏数据问题。示例:

<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
	<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
	<!--其他配置-->
</mapper>

  虽然这样可以解决脏数据的问题,但是并不是所有的关联查询都可以这么解决,如果有几十个表甚至所有表都以不同的关联关系存在于各自的映射文件中时,使用参照缓存显然没有意义。

2.4.2 二级缓存适用场景

  二级缓存虽然好处很多,但并不是什么时候都可以使用。在以下场景中,推荐使用二级缓存:

1、以查询为主的应用中,只有尽可能少的增、删、改操作。
2、绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
3、可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。

  在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存

2.5 一集缓存和二级缓存的查询顺序(二级缓存开启则先查二级缓存)

  如果MyBatis使用了二级缓存,并且Mapper和select语句也配置使用了二级缓存。那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:先查二级缓存,再查一级缓存,再查数据库
  即使在一个sqlSession中,也会先查二级缓存;一个namespace中的查询更是如此。

  一集缓存和二级缓存的具体查询顺序:

  • 1、先判断二级缓存是否开启。如果没开启,再判断一级缓存是否开启;如果没开启,直接查数据库。
  • 2、如果一级缓存关闭,即使二级缓存开启也没有数据,因为二级缓存的数据从一级缓存获取。
  • 3、一般不会关闭一级缓存。
  • 4、二级缓存默认不开启。
  • 5、如果二级缓存关闭,直接判断一级缓存是否有数据,如果没有就查数据库。
  • 6、如果二级缓存开启,先判断二级缓存有没有数据,如果有就直接返回;如果没有,就查询一级缓存,如果有就返回,没有就查询数据库。

三、Mybatis插件开发

  MyBatis允许在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的接口和方法包括以下几个:

  Executor(update、query、flushStatements、commit、rollback、getTransaction、close、isClosed)
  ParameterHandler (getParameterobject,setParameters)
  ResultSetHandler(handleResultSets、handleCursorResultSets、handleOutputParameters)
  StatementHandler(prepare、parameterize、batch、update、query)

3.1 拦截器接口(Interceptor)

  MyBatis插件可以用来实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。Interceptor接口:

public interface Interceptor {
	Object intercept(Invocation invocation) throws Throwable;
	Object plugin(Object target) ;
	void setProperties(Properties properties);
}
  • setProperties
      这个方法用来传递插件的参数,可以通过参数来改变插件的行为。
      在mybatis-config.xml中,一般情况下,拦截器的配置示例:
<plugins>
	<plugin interceptor="tk.mybatis.simple.plugin.XXXInterceptor">
		<property name="propl"value="valuel"/>
		<property name="prop2"value="value2"/>
	</plugin>
</plugins>

  在配置拦截器时,plugin的interceptor属性为拦截器实现类的全限定名称,如果需要参数,可以在plugin标签内通过property标签进行配置,配置后的参数在拦截器初始化时会通过setProperties方法传递给拦截器。在拦截器中可以很方便地通过Properties取得配置的参数值。

  • plugin
      这个方法的参数target就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。该方法的实现很简单,只需要调用MyBatis提供的Plugin类的wrap静态方法就可以通过Java的动态代理拦截目标对象。这个接口方法通常的实现代码:
	@Override
	public object plugin(Object target){
		return Plugin.wrap(target,this);
	}

  P1ugin.wrap方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象,因此在上面的实现方法中不必做额外的逻辑判断。

  • intercept
      该方法是MyBatis运行时要执行的拦截方法。通过该方法的参数invocation可以得到很多有用的信息,该参数的常用方法:
@Override
public Object intercept(Invocationinvocation) throws Throwable{
	Object target = invocation.getTarget();
	Method method = invocation.getMethod();
	Object[] args = invocation.getArgs();
	Object result = invocation.proceed();
	return result;
}

  使用getTarget()方法可以获取当前被拦截的对象,使用getMethod()可以获取当前被拦截的方法,使用getArgs()方法可以返回被拦截方法中的参数。通过调用invocation.proceed();可以执行被拦截对象真正的方法,proceed()方法实际上执行了method.invoke(target,args)方法,上面的代码中没有做任何特殊处理,直接返回了执行的结果。
  当配置多个拦截器时,MyBatis会遍历所有拦截器,按顺序执行拦截器的plugin方法,被拦截的对象就会被层层代理。在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通过invocation.proceed()调用下一层的方法,直到真正的方法被执行。方法执行的结果会从最里面开始向外一层层返回,所以如果存在按顺序配置的A、B、C三个签名相同的拦截器,MyBaits会按照C>B>A>target.proceed()>A>B>C的顺序执行。如果A、B、C签名不同,就会按照MyBatis拦截对象的逻辑执行。

3.2 拦截器签名

  除了需要实现拦截器接口外,还需要给实现类配置以下的拦截器注解:@Intercepts和签名注解Signature,这两个注解用来配置拦截器要拦截的接口的方法。
  @Intercepts注解中的属性是一个@Signature(签名)数组,可以在同一个拦截器中同时拦截不同的接口和方法。
  以拦截ResultSetHandler接口的handleResultSets方法为例,配置签名:

@Intercepts({
	@Signature(
		type = ResultSetHandler.class,
		method = "handleResultSets",
		args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor

  @Signature注解包含以下三个属性:

  type:设置拦截的接口,可选值是前面提到的4个接口。
  method:设置拦截接口中的方法名,可选值是前面4个接口对应的方法,需要和接口匹配。
  args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法。

  由于MyBatis代码具体实现的原因,可以被拦截的4个接口中的方法并不是都可以被拦截的。下面具体列举出可以拦截的接口和方法。

3.2.1 Executor接口(可以拦截增删改查操作)
  • int update(MappedStatement ms, Object parameter) throws SQLException
      该方法会在所有的INSERT、UPDATE、DELETE执行时被调用,因此如果想要拦截这3类操作,可以拦截该方法。接口方法对应的签名:
@Signature(
	type = Executor.class,
	method	= "update",
	args = {MappedStatement.class,Object.class})
  • <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException
      该方法会在所有SELECT查询方法执行时被调用。通过这个接口参数可以获取很多有用的信息,因此这是最常被拦截的一个方法。接口方法对应的签名:
@Signature(
	type=  Executor.class,
	method = "query",
	args = {MappedStatement.class,Object.class,
			RowBounds.class,Result Handler.class})
  • <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException
      该方法只有在查询的返回值类型为Cursor时被调用。接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "queryCursor",
	args = {Mappedstatement.class,Object.class,RowBounds.class})
  • List<BatchResult> flushStatements() throws SQLException
      该方法只在通过SqlSession方法调用flushStatements方法或执行的接口方法中带有@Flush注解时才被调用,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "flushStatements",
	args = {})
  • void commit(boolean required) throws SQLException
      该方法只在通过SqlSession方法调用commit方法时才被调用,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "commit",
	args = {boolean.class})
  • void rollback(boolean required) throws SQLException
      该方法只在通过SqlSession方法调用rollback方法时才被调用,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "rollback",
	args = {boolean.class})
  • Transaction getTransaction()
      该方法只在通过SqlSession方法获取数据库连接时才被调用,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "getTransaction",
	args = {})
  • void close(boolean forceRollback)
      该方法只在延迟加载获取新的Executor后才会被执行,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "close",
	args = {boolean.class})
  • boolean isClosed()
      该方法只在延迟加载执行查询方法前被执行,接口方法对应的签名:
@Signature(
	type = Executor.class,
	method = "isClosed",
	args = {})
3.2.2 ParameterHandler接口(拦截参数处理)
  • Object getParameterObject()
      该方法只在执行存储过程处理出参的时候被调用。接口方法对应的签名:
@Signature(
	type = ParameterHandler.class,
	method = "getParameterobject",
	args = {})
  • void setParameters(PreparedStatement ps) throws SQLException
      该方法在所有数据库方法设置SQL参数时被调用。接口方法对应的签名:
@Signature(
	type = ParameterHandler.class,
	method = "setParameters",
	args = {PreparedStatement.class})
3.2.3 ResultSetHandler接口(拦截结果处理)
  • <E> List<E> handleResultSets(Statement stmt) throws SQLException
      该方法会在除存储过程及返回值类型为Cursor<T>以外的查询方法中被调用。接口方法对应的签名:
@Signature(
	type = ResultSetHandler.class,
	method = "handleResultSets",
	args = {Statement.class})
  • <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException
      该方法是3.4.0版本中新增加的,只会在返回值类型为Cursor<T>的查询方法中被调用,接口方法对应的签名:
@Signature(
	type = ResultSetHandler.class,
	method = "handleCursorResultSets",
	args = {Statement.class})
  • void handleOutputParameters(CallableStatement cs) throws SQLException
      该方法只在使用存储过程处理出参时被调用,接口方法对应的签名:
@Signature(
	type = ResultSetHandler.class,
	method = "handleOutputParameters",
	args = {CallableStatement.class})

  ResultSetHandler接口的第一个方法对于拦截处理MyBatis的查询结果非常有用,并且由于这个接口被调用的位置在处理二级缓存之前,因此通过这种方式处理的结果可以执行二级缓存。

3.2.4 StatementHandler接口(拦截SQL语句处理)
  • Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException
      该方法会在数据库执行前被调用,优先于当前接口中的其他方法而被执行。接口方法对应的签名:
@Signature(
	type = StatementHandler.class,
	method = "prepare",
	args = {Connection.class,Integer.class})
  • void parameterize(Statement statement) throws SQLException
      该方法在prepare方法之后执行,用于处理参数信息,接口方法对应的签名:
@Signature(
	type = StatementHandler.class,
	method = "parameterize",
	args = {Statement .class})
  • void batch(Statement statement) throws SQLException
      在全局设置配置defaultExecutorType="BATCH"时,执行数据操作才会调用该方法,接口方法对应的签名:
@Signature(
	type = StatementHandler.class,
	method = "batch",
	args = {Statement .class})
  • <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException
      执行SELECT方法时调用,接口方法对应的签名:
@Signature(
	type = StatementHandler.class,
	method = "query",
	args = {Statement .class,ResultHandler.class})
  • <E> Cursor<E> queryCursor(Statement statement) throws SQLException
      该方法是3.4.0版本中新增加的,只会在返回值类型为Cursor的查询中被调用,接口方法对应的签名:
@Signature(
	type = StatementHandler.class,
	method = "queryCursor",
	args = {Statement .class})

3.3 实现拦截器

3.3.1 下画线键值转小写驼峰形式插件

  有些人在使用MyBatis时,为了方便扩展而使用Map类型的返回值。使用Map作为返回值时,Map中的键值就是查询结果中的列名,而列名一般都是大小写字母或者下画线形式,和Java中使用的驼峰形式不一致。而且由于不同数据库查询结果列的大小写也并不一致,因此为了保证在使用Map时的属性一致,可以对Map类型的结果进行特殊处理,即将不同格式的列名转换为Java中的驼峰形式。
  这种情况下,就可以使用拦截器,通过拦截ResultSetHandler接口中的handleResultSets方法去处理Map类型的结果。拦截器实现代码示例:

/**
 * MyBatis Map 类型下画线Key 转小写驼峰形式
 *
 * @author liuzenghui
 */
@Intercepts(
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
@SuppressWarnings({ "unchecked", "rawtypes" })
public class CameHumpInterceptor implements Interceptor {
    
	@Override
    public Object intercept(Invocation invocation) throws Throwable {
        //先执行得到结果,再对结果进行处理
        List<Object> list = (List<Object>) invocation.proceed();
        for(Object object : list){
        	//如果结果是 Map 类型,就对 Map 的 Key 进行转换
            if(object instanceof Map){
                processMap((Map)object);
            } else {
                break;
            }
        }
        return list;
    }

    /**
     * 处理 Map 类型
     *
     * @param map
     */
    private void processMap(Map<String, Object> map) {
        Set<String> keySet = new HashSet<String>(map.keySet());
        for(String key : keySet){
        	//大写开头的会将整个字符串转换为小写,如果包含下划线也会处理为驼峰
        	if((key.charAt(0) >= 'A' && key.charAt(0) <= 'Z') || key.indexOf("_") >= 0){
        		Object value = map.get(key);
        		map.remove(key);
        		map.put(underlineToCamelhump(key), value);
        	}
        }
    }

    /**
     * 将下划线风格替换为驼峰风格
     *
     * @param inputString
     * @return
     */
    public static String underlineToCamelhump(String inputString) {
        StringBuilder sb = new StringBuilder();

        boolean nextUpperCase = false;
        for (int i = 0; i < inputString.length(); i++) {
            char c = inputString.charAt(i);
            if(c == '_'){
            	if (sb.length() > 0) {
                    nextUpperCase = true;
                }
            } else {
            	if (nextUpperCase) {
                    sb.append(Character.toUpperCase(c));
                    nextUpperCase = false;
                } else {
                    sb.append(Character.toLowerCase(c));
                }
            }
        }
        return sb.toString();
    }

    @Override
    public Object plugin(Object target) {
    	return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

  这个插件的功能是循环判断结果。如果是Map类型的结果,就对Map的key进行处理,处理时为了避免把已经是驼峰的值转换为纯小写,因此通过首字母是否为大写或是否包含下画线来判断(实际应用中要根据实际情况修改)。如果符合其中一个条件就转换为驼峰形式,删除对应的key值,使用新的key值来代替。当数据经过这个拦截器插件处理后,就可以保证在任何数据库中以Map作为结果值类型时,都有一致的key值。
  想要使用该插件,需要在mybatis-config.xml中配置该插件:

<plugins>
	<plugin interceptor="tk.mybatis.simple.plugin.CameHumpInterceptor"/>
</plugins>
3.3.2 分页插件(pageNum/pageSize)
  • PageHelper的使用
      现在已有成熟的分页插件:PageHelper(这个插件支持十几种数据库,并且在很多方面都进行了优化)。其使用可以分为3个步骤:
      1、引入jar包
	<dependency>
       	<groupId>com.github.pagehelper</groupId>
       	<artifactId>pagehelper</artifactId>
       	<version>5.1.7</version>
    </dependency>

  2、配置分页插件

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

  3、业务层代码

	@Test
    public void selectUserPageHelper() {
        SqlSession session = MybatisUtils.getSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        PageHelper.startPage(1, 3);
        List<User> list = mapper.getUserInfo();
        //用PageInfo将包装起来
        PageInfo page = new PageInfo(list);
        for (User map: list){
            System.out.println(map);
        }
        System.out.println("page:---"+page);
        session.close();
    }

  PageHelper比较好用,是物理分页。
  在使用PageHelper插件时,两个分页参数的传递有很多种。比如:

	//1.在业务代码中调用PageHelper.startPage方法.
	PageHelper.startPage(1, 10);	
	List<User> list = userMapper.selectIf(1);

	//2.在Mapper接口中传pageNum、pageSize参数,开发者不需要在xml处理后两个参数
    List<User> selectByPageNumSize(
            @Param("user") User user,
            @Param("pageNum") int pageNum, 
            @Param("pageSize") int pageSize);
            }

	//3.将两个分页参数存在实体类对象中.如pageNum和pageSize存在于User对象中,只要参数
	//有值,也会被分页
	public class User {
    	//...
    	//下面两个参数名和 params 配置的名字一致
    	private Integer pageNum;
    	private Integer pageSize;
	}
	//存在以下 Mapper 接口方法,开发者不需要在 xml 处理后两个参数
	public interface CountryMapper {
    	List<User> selectByPageNumSize(User user);
	}
	//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
	List<User> list = userMapper.selectByPageNumSize(user);
}

  使用PageHelper分页插件的注意事项
  使用PageHelper分页插件时,需注意,只对startPage后最近的一个查询sql有分页效果。原因是PageHelper用拦截器的方式给查询SQL添加limit子句,进而达到分页效果,在这个过程中,用到了ThreadLocal。在执行最近的一次查询后,在finally内把ThreadLocal中的分页数据给清除掉了,所以只要执行一次查询语句就会清除分页信息,故而后面的select语句自然就无效了。

  • PageHelper的使用
      示例分页插件的核心部分由两个类组成:PageInterceptor拦截器类和数据库方言接口Dialect,此处还提供了基于MySQL数据库的实现。
/**
 * Mybatis - 通用分页拦截器
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
	@Signature(
		type = Executor.class, 
		method = "query", 
		args = {MappedStatement.class, Object.class, 
				RowBounds.class, ResultHandler.class}
	)
)
public class PageInterceptor implements Interceptor {
    private static final List<ResultMapping> EMPTY_RESULTMAPPING
    		= new ArrayList<ResultMapping>(0);
    private Dialect dialect;
    private Field additionalParametersField;

	@Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取拦截方法的参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms.getId(), parameterObject, rowBounds)) {
        	ResultHandler resultHandler = (ResultHandler) args[3];
            //当前的目标对象
            Executor executor = (Executor) invocation.getTarget();
            BoundSql boundSql = ms.getBoundSql(parameterObject);
            //反射获取动态参数
            Map<String, Object> additionalParameters = 
            		(Map<String, Object>) additionalParametersField.get(boundSql);
            //判断是否需要进行 count 查询
            if (dialect.beforeCount(ms.getId(), parameterObject, rowBounds)){
            	//根据当前的 ms 创建一个返回值为 Long 类型的 ms
                MappedStatement countMs = newMappedStatement(ms, Long.class);
                //创建 count 查询的缓存 key
                CacheKey countKey = executor.createCacheKey(
                		countMs, 
                		parameterObject, 
                		RowBounds.DEFAULT, 
                		boundSql);
                //调用方言获取 count sql
                String countSql = dialect.getCountSql(
                		boundSql, 
                		parameterObject, 
                		rowBounds, 
                		countKey);
                BoundSql countBoundSql = new BoundSql(
                		ms.getConfiguration(), 
                		countSql, 
                		boundSql.getParameterMappings(), 
                		parameterObject);
                //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
                for (String key : additionalParameters.keySet()) {
                    countBoundSql.setAdditionalParameter(
                    		key, additionalParameters.get(key));
                }
                //执行 count 查询
                Object countResultList = executor.query(
                		countMs, 
                		parameterObject, 
                		RowBounds.DEFAULT, 
                		resultHandler, 
                		countKey, 
                		countBoundSql);
                Long count = (Long) ((List) countResultList).get(0);
                //处理查询总数
                dialect.afterCount(count, parameterObject, rowBounds);
                if(count == 0L){
                	//当查询总数为 0 时,直接返回空的结果
                	return dialect.afterPage(
                			new ArrayList(), 
                			parameterObject, 
                			rowBounds); 
                }
            }
            //判断是否需要进行分页查询
            if (dialect.beforePage(ms.getId(), parameterObject, rowBounds)){
            	//生成分页的缓存 key
                CacheKey pageKey = executor.createCacheKey(
                		ms, 
                		parameterObject, 
                		rowBounds, 
                		boundSql);
                //调用方言获取分页 sql
                String pageSql = dialect.getPageSql(
                		boundSql, 
                		parameterObject, 
                		rowBounds, 
                		pageKey);
                BoundSql pageBoundSql = new BoundSql(
                		ms.getConfiguration(), 
                		pageSql, 
                		boundSql.getParameterMappings(), 
                		parameterObject);
                //设置动态参数
                for (String key : additionalParameters.keySet()) {
                    pageBoundSql.setAdditionalParameter(
                    		key, additionalParameters.get(key));
                }
                //执行分页查询
                List resultList = executor.query(
                		ms, 
                		parameterObject, 
                		RowBounds.DEFAULT, 
                		resultHandler, 
                		pageKey, 
                		pageBoundSql);
                
                return dialect.afterPage(resultList, parameterObject, rowBounds);
            }
        }
        //返回默认查询
        return invocation.proceed();
    }

    /**
     * 根据现有的 ms 创建一个新的,使用新的返回值类型
     *
     * @param ms
     * @param resultType
     * @return
     */
    public MappedStatement newMappedStatement(
    		MappedStatement ms, Class<?> resultType) {
        MappedStatement.Builder builder = new MappedStatement.Builder(
        		ms.getConfiguration(), 
        		ms.getId() + "_Count", 
        		ms.getSqlSource(), 
        		ms.getSqlCommandType()
        );
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null 
        		&& ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(
            		keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        //count查询返回值int
        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        ResultMap resultMap = new ResultMap.Builder(
        		ms.getConfiguration(), 
        		ms.getId(), 
        		resultType, 
        		EMPTY_RESULTMAPPING).build();
        resultMaps.add(resultMap);
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String dialectClass = properties.getProperty("dialect");
        try {
            dialect = (Dialect) Class.forName(dialectClass).newInstance();
        } catch (Exception e) {
            throw new RuntimeException(
            		"使用 PageInterceptor 分页插件时,必须设置 dialect 属性");
        }
        dialect.setProperties(properties);
        try {
            //反射获取 BoundSql 中的 additionalParameters 属性
            additionalParametersField = BoundSql.class.getDeclaredField(
            		"additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

}

  拦截器拦截了Executor类的query接口。
  该分页插件的主要逻辑:

  先判断当前的MyBatis方法是否需要进行分页:如果不需要进行分页,就直接调用invocation.proceed()返回;
  如果需要进行分页,首先获取当前方法的BoundSql,这个对象中包含了要执行的SQL和对应的参数。通过这个对象的SQL和参数生成一个count查询的BoundSql,由于这种情况下的MappedStatement对象中的resultMap或resultType类型为当前查询结果的类型,并不适合返回count查询值,因此通过newMappedStatement方法根据当前的MappedStatement生成了一个返回值类型为Long的对象,然后通过Executor执行查询,得到了数据总数。
  得到总数后,根据dialect.afterCount判断是否继续进行分页查询,因为如果当前查询的结果为0,就不必继续进行分页查询了,而是可以直接返回空值。
  如果需要进行分页,就使用dialect获取分页查询SQL,同count查询类似,得到分页数据的结果后,通过dialect对结果进行处理并返回。

  除了主要的逻辑部分外,在setProperties中还要求必须设置dialect参数,该参数的值为Dialect实现类的全限定名称。这里进行反射实例化后,又调用了Dialect的setProperties,通过参数传递可以让Dialect实现更多可配置的功能。除了实例化dialect,这段代码还初始化了additionalParametersField,这是通过反射获取了BoundSql对象中的additionalParameters属性,在创建新的BoundSql对象中,通过这个属性反射获取了执行动态SQL时产生的动态参数。

/**
 * 数据库方言,针对不同数据库进行实现
 */
@SuppressWarnings("rawtypes")
public interface Dialect {
	/**
	 * 跳过 count 和 分页查询
	 * 
	 * @param msId 执行的  MyBatis 方法全名
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @return true 跳过,返回默认查询结果,false 执行分页查询
	 */
	boolean skip(String msId, Object parameterObject, RowBounds rowBounds);
	
	/**
	 * 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
	 * 
	 * @param msId 执行的  MyBatis 方法全名
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @return
	 */
	boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds);
	
	/**
	 * 生成 count 查询 sql
	 * 
	 * @param boundSql 绑定 SQL 对象
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @param countKey count 缓存 key
	 * @return
	 */
	String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);
	
	/**
	 * 执行完 count 查询后
	 * 
	 * @param count 查询结果总数
	 * @param parameterObject 接口参数
	 * @param rowBounds 分页参数
	 */
	void afterCount(long count, Object parameterObject, RowBounds rowBounds);
	
	/**
	 * 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
	 * 
	 * @param msId 执行的 MyBatis 方法全名
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @return
	 */
	boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds);
	
	/**
	 * 生成分页查询 sql
	 * 
	 * @param boundSql 绑定 SQL 对象
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @param pageKey 分页缓存 key
	 * @return
	 */
	String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
	
	/**
	 * 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
	 * 
	 * @param pageList 分页查询结果
	 * @param parameterObject 方法参数
	 * @param rowBounds 分页参数
	 * @return
	 */
	Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);
	
	/**
	 * 设置参数
	 * 
	 * @param properties 插件属性
	 */
	void setProperties(Properties properties);

  Dialect接口提供的方法可以控制分页逻辑及对分页结果的处理,不同的处理方式可以实现不同的效果,为了以最简单的方式实现一个可以使用的插件,代码中新增了一个PageRowBounds类,该类继承自RowBounds类,RowBounds类包含offset(偏移值)和limit(限制数)。PageRowBounds在此基础上额外增加了一个total属性用于记录查询总数。通过使用PageRowBounds方式可以很简单地处理分页参数和查询总数,这是一种最简单的实现,代码示例:

/**
 * 可以记录 total 的分页参数
 */
public class PageRowBounds extends RowBounds{
	private long total;

	public PageRowBounds() {
		super();
	}

	public PageRowBounds(int offset, int limit) {
		super(offset, limit);
	}

	public long getTotal() {
		return total;
	}

	public void setTotal(long total) {
		this.total = total;
	}
}

  有了PageRowBounds,我们便可以以最简单的逻辑实现MySQL的分页,实现类代码示例:

/**
 * MySql 实现
 */
@SuppressWarnings("rawtypes")
public class MySqlDialect implements Dialect {

	@Override
	public boolean skip(String msId, Object parameterObject, RowBounds rowBounds) {
		//这里使用 RowBounds 分页,默认没有 RowBounds 参数时,会使用 RowBounds.DEFAULT 作为默认值
		if(rowBounds != RowBounds.DEFAULT){
			return false;
		}
		return true;
	}

	@Override
	public boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds) {
		//只有使用 PageRowBounds 才能记录总数,否则查询了总数也没用
		if(rowBounds instanceof PageRowBounds){
    		return true;
    	}
		return false;
	}
	
	@Override
	public String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
		//简单嵌套实现 MySql count 查询
		return "select count(*) from (" + boundSql.getSql() + ") temp";
	}
	
    @Override
    public void afterCount(long count, Object parameterObject, RowBounds rowBounds) {
    	//记录总数,按照 beforeCount 逻辑,只有 PageRowBounds 时才会查询 count,所以这里直接强制转换
    	((PageRowBounds)rowBounds).setTotal(count);
    }
    
    @Override
	public boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds) {
		if(rowBounds != RowBounds.DEFAULT){
			return true;
		}
		return false;
	}
	
	@Override
	public String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
		//pageKey 会影响缓存,通过固定的 RowBounds 可以保证二级缓存有效
		pageKey.update("RowBounds");
		return boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
	}

	@Override
	public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
		return pageList;
	}
	
	@Override
	public void setProperties(Properties properties) {
		
	}
}

有了上面这些代码后,想要使用拦截器,还需要在mybatis-config.xml中进行如下配置:

<plugins>
	<plugin interceptor="tk.mybatis.simple.plugin.PageInterceptor">
		<property name="dialect"value="tk.mybatis.simple.plugin.MySqlDialect"/>
	</plugin>
</plugins>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值