跳到正文
返回

从 0 开始的 MyBatis 3.x 学习

发表于 更新于
浏览量: --

封面画师:T5-茨舞(微博)     封面ID:74539039

本文参考:2018 尚硅谷 雷丰阳 MyBatis 3.x

0. MyBatis 的初步使用

0.1 为什么要使用 MyBatis?

MyBatis 是一个半自动化的持久层框架。

JDBC

Hibernate 和 JPA


对于开发人员而言,核心的 SQL 还是需要自己优化。

SQL 和 Java 编码分开,功能边界清晰,一个专注业务,一个专注数据!

0.2 简单使用步骤

1、编写 xml 配置文件(全局配置文件)

2、编写 SQL 映射文件(配置了每一个 SQL,以及 SQL 的封装规则)

3、将 SQL 映射文件注册在全局配置文件中

4、写代码

当然还可以使用接口式编程,获取接口的实现类对象,MyBatis 会为接口自动创建一个代理对象,代理对象去实现 CRUD。(mapper 接口没有实现类,但是 MyBatis 会为这个接口生成一个代理对象)

如果实体类的属性 名和数据库的字段名不对应,将无法查询出结果,可以在 SQL 语句中对相关的字段名(字段名的别名设置为属性名)使用别名进行查询。

1. 全局配置文件标签

全局配置文件,其中包含了数据库连接池信息、事务管理器信息等系统运行环境信息(非必须,可以使用 Java 代码编写)。

全局配置文件约束信息:

<?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">

1.1 properties

该标签仅做了解,实际开发中会将 MyBatis 与 Spring 绑定,这些配置文件信息一般都会交给 Spring 进行管理,因此该标签的使用并不多。

1.2 settings

setting标签属性_1

setting标签属性_2

setting标签属性_3

使用示例

<settings>
    <!-- 打印sql日志 -->
    <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>

1.3 typeAliases

作用:别名处理器,能够为我们的 Java 类型取别名,别名不区分大小写

尽管别名可以让 SQL 映射文件中的代码变得简洁,但是为了方便后续阅读代码,还是建议使用全类名

单个别名

<typeAliases> 标签下可以包含多个 <typeAliase><typeAliase> 可以为某个 Java 类型取别名。

<typeAliase> 标签属性:

批量取别名

<typeAliases> 标签下可以包含一个 <package> 标签,<package> 可以为某个包下的所有类批量取别名。

<package> 标签属性:

@Alias:在批量取别名的情况下(使用了 <package> 标签的情况下),可以为某个 JavaBean 指定新的别名(该注解的位置在类上),以防止别名冲突。

MyBatis 为普通的 Java 类型提供了许多内建的类型别名,因此我们在自定义别名时应当避免与这些别名相同。比如下图的部分内建别名(全部内建别名请参考官方文档):

MyBatis内建部分别名

1.4 typeHandlers

作用:类型处理器。无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数,还是从结果集中取出一个值,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

使用示例

处理枚举类型(需要从 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中选一个来使用)时,MyBatis 默认使用的是 EnumTypeHandler,现在将处理枚举类型改为 EnumOrdinalTypeHandler

<!--自定义枚举为UserStatus-->
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" 		   javaType="com.yang.domain.UserStatus"/>
</typeHandlers>

1.5 plugins

7. 插件

1.6 environments

作用:配置 MyBatis 环境,MyBatis 可以配置多种环境。

<environments> 标签属性:

需要注意的是: <environments> 标签下需要包含的标签:<environment> 标签。而 <environment> 标签下需要包含 <transactionManager> 标签和 <dataSource> 标签。

1.7 environment

作用:配置一个具体的环境信息。这个标签下必须包含两个标签:<transactionManager><dataSource>

<environment> 标签属性:

1.8 transactionManager

作用:事物管理器。(了解)

<transactionManager> 标签属性:

自定义事物管理器:实现 TransactionFactory 接口,type 属性指定为全类名。

1.9 dataSource

作用:配置数据源。

<dataSource> 标签属性:

自定义数据源:实现 DataSourceFactory 接口,type 属性指定为全类名。

1.10 databaseIdProvider

作用:数据库厂商标识,用于支持多数据库厂商。

<databaseIdProvider> 标签属性:

具体使用

<databaseIdProvider type="DB_VENDOR">
   		<!--给数据库厂商标识MySQL设置别名为mysql-->
        <property name="MySQL" value="mysql"/>
        <property name="Oracle" value="oracle"/>	
        <property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>

最后在 SQL 映射文件下,在 sql 操作标签中加上 databaseId 属性并指定全局配置文件中设置的数据库厂商标识别名。比如:

<select id="findById" resultType="com.yang.domain.Users" databaseId = "mysql">
    select * from users where id = #{uid}
</select>

假设当前数据库环境是 MySQL,同时需要根据员工的 id 来查询对应的员工,又假设在 SQL 映射文件中针对这个需求编写了两条 SQL 语句,一条没有添加数据库厂商标识,另一条添加了 MySQL 数据库厂商标识,MyBatis 在执行 SQL 语句时会 优先执行指定更加明确的语句 ,即:添加了 MySQL 数据库厂商标识的 SQL 语句。

1.11 mappers

作用:将 SQL 映射文件注册到全局配置文件中,将 SQL 映射文件和全局配置文件进行关联( 重要 )。

<mappers> 标签下可以包含多个 <mapper> 标签,<mapper> 标签可以关联一个 SQL 映射文件。<mappers> 标签下还可以包含一个 <package> 标签,以进行批量注册。

<mapper> 标签属性:

class 属性使用规范

1、有 SQL 映射文件时,映射文件必须和接口同名,并且放在与接口同一目录下。

2、没有 SQL 映射文件时,所有的 SQL 语句都是利用注解写在接口上。class 属性值为 SQL 注解(eg:@Select)所在接口的全类名。简单的 SQL 语句可以使用注解,复杂的 SQL 语句还是写在 SQL 映射文件中。

<package> 标签

<package> 标签属性:

<package> 标签中 name 属性使用规范:存在 SQL 映射文件时,映射文件必须和接口同名,并且放在与接口同一目录下。

1.12 标签书写顺序

MyBatis 全局配置文件中的标签顺序并不是任意的。

全局配置文件中可以不存在某些标签,但是存在的标签必须按照以下的先后顺序进行书写:

properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?

2. 获取主键值与参数处理

2.1 SQL 映射文件约束

SQL 映射文件中保存了每一个 SQL 语句的信息( 必须 )。

SQL 映射文件约束信息:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

实体类与数据库字段

相关实体类:(下文中涉及的 SQL 语句环境都如下)

public class User{
    private Integer id;
    private String username;
    private String password;
    private String email;
    private String gender;
    private Department dept;
    /*省略get/set方法,toString()方法及相关构造函数*/
}

public class Department{
    private Integer id;
    private String departmentName;
    /*省略get/set方法,toString()方法及相关构造函数*/
}

数据库表名与字段名:

user table:
id | username | pwd | email | gender | did

dept table:
id | deptName

2.2 简单 CRUD

在 MyBatis 中,允许增删改查直接定义以下类型的返回值:IntegerLongBooleanvoid

对于 Boolean 类型的返回值来说,如果执行增加、删除、修改等操作时影响了一行以上的数据,那么就会返回 true

在 Mybatis 中进行 CRUD 时,需要使用对应的标签,比如:<insert><delete><update><select>,在这些标签中都有一个名为 parameterType 的属性,这个属性是老式风格的参数映射,已被废弃,但是也可以使用,使用时指定参数类型的全类名或别名即可。既然这个属性已被废弃,因此 我们在编写时也可以不写这个属性

在 SQL 映射文件中写 SQL 时,请不要在末尾写上分号!

2.3 插入获取主键值

对于支持主键自增的数据库

例如,MySQL 数据库支持主键自增,MyBatis 可以利用 statement.getGeneratedKeys() 获取自增主键值。

那么应该怎么做呢?

我们只需要在 <insert> 标签中添加属性:useGeneratedKeyskeyProperty

其中,需要将 useGeneratedKeys 设置为 "true",这表示使用自增主键获取主键值策略。

keyProperty 属性用于指定对应的主键属性,也就是 MyBatis 获取到主键值后,将这个值封装给 JavaBean 的哪个属性。比如我插入数据时想让自增的主键值封装到实体类中的 id 属性,那么需要这么写:keyProperty = "id"

对于不支持主键自增的数据库:

例如,Oracle 不支持主键自增,因此 Oracle 需要使用序列来模拟自增,即:每次插入数据的主键是从序列中拿到的值。那么如何获取到这个值呢?

假设需要向某张表插入一行数据,且这张表拥有每次自增 1 的序列主键,那么我们可以这么写插入的 SQL:

<insert id = "addUser" databaseId = "oracle">
    <!-- 编写查询主键的sql语句 -->
    <selectKey keyProperty="id" order="BEFORE" resultType="Integer">
        <!-- USER_SQL 表示自定义的序列名,nextval 表示序列的下一个值 -->
        select USER_SQL.nextval from dual
    </selectKey>
    insert into user(id,username,password) 
    values (#{id},#{username},#{password})
</insert>

在上述语句中,使用了一个名为 <selectKey> 的标签,见名识意,这个标签就是用于「查询主键」的。在这个标签中,有三个属性:

使用 order = "BEFORE" 后插入语句的执行顺序是:先运行 <selectKey> 标签查询 id 的 SQL 语句,查出 id 的值后封装给 JavaBean 的 id 属性,然后再运行插入的 SQL 语句,这样就可以取出 id 属性对应的值。


如果将 <selectKey> 标签中的 order 属性改为 AFTER (表示当前 SQL 语句在插入的 SQL 语句执行后再执行),则有:

<insert id = "addUser" databaseId = "oracle"> 
   <selectKey keyProperty="id" order="AFTER" resultType="Integer">
       <!-- currval 表示序列的当前值 -->
       select USER_SQL.currval from dual
   </selectKey>
   insert into user(id,username,password) 
   values (USER_SQL.nextval,#{username},#{password})
</insert>

使用 order = "AFTER" 后插入语句的执行顺序是:先运行插入的 SQL 语句(从序列中取出新值作为 id),然后再运行 <selectKey> 标签中查询 id 的 SQL。

实际使用中通常使用 BEFORE 方式,而不是 AFTER 方式,因为 AFTER 方式插入多条数据时会出现问题,插入多条数据时只能获取到最后一个主键值!

2.4 参数处理

使用 MyBatis 时,我们通常会编写若干个 Mapper 接口,每个 Mapper 接口还有与之对应的 SQL 映射文件。在 SQL 映射文件中获取接口中的参数时,似乎只用 #{参数名} 的方式就能够获取到。其实并不是这么简单,不同情况下参数的获取方式也不同。

单个参数

MyBatis 不会做特殊处理 ,直接使用 #{参数名} 即可取出参数值。

其实不使用 #{参数名},参数名换成任意字符串都能获取到,因为只传递了一个参数。

比如,在单个参数的情况下,使用 #{a}#{abc} 等等方式都能准确地获取到。

多个参数

MyBatis 会做特殊处理

多个参数会被封装成一个 Map,在这个 Map 中:

在 SQL 映射文件中,#{} 就是从 map 中获取指定的 key 值。我们可以使用 #{param1}#{0} 来获取多个参数中的第一个参数值。

但是参数过多的时候,使用这种方式不便于阅读,因此我们可以使用注解对参数进行命名。

2021-10-13 补充:使用最新的 MyBatis 3.5.7 版本时,方法参数名称与 SQL 映射文件中参数名称相同时也可映射成功。

命名参数

命名参数就是明确指定封装参数时 map 的 key,使用注解 @Param

public User getUserByIdAndUserName(@Param("id") Integer id, 		   
                                   @Param("username")String username);

多个参数的情况下,在接口中使用了 @Param 注解后,多个参数会被封装成一个 map,在这个 map 中:

然后在 SQL 映射文件中,就可以使用 #{指定的key} 的方式取出对应的参数值。

实体类参数

POJO:如果多个参数恰好是我们业务逻辑的数据模型,我们可以在接口参数中直接传入实体类。然后在 SQL 映射文件中使用 #{属性名} 就可以取出传入的实体类的属性值。

Map:如果传入的数据不是业务模型的数据,没有对应的 POJO,且不经常使用,我们可以传入 Map 集合。在 SQL 映射文件中使用 #{key} 就可取出 Map 集合中对应的 value 值。

TO:如果多个参数不是业务模型中的数据,但需要经常使用,推荐编写一个 TO(Transfer, Object)数据传输对象。

2.5 参数处理思考

根据下列给出的接口,写出在 SQL 映射文件中获取参数的方式。

public User getUser(@Param("id") Integer id, String username);

id:#{id} 或者 #{param1}

username:#{param2} 或 #{username}(MyBatis 3.5.7 可成功映射)

public User getUser(Integer id, @Param("u") User user);

id:#{param1} 或 #{id}(MyBatis 3.5.7 可成功映射)

username:#{param2.username} 或者 #{u.username}

public User getUserById(List<Integer> ids);

需要特别注意的是,如果接口中参数传递的是一个 Collection(List、Set) 集合或者是数组,MyBatis 也会对其进行特殊处理,也就是把传入的集合或数组封装到 Map 中。

如果我们使用 #{key} 的方式获取 Map 中的 value 时:

因此,对于上述给出的接口,我们想在 SQL 映射文件中获取第一个 id 的值时,可以使用 #{list[0]} 进行获取。

2.6 #{ } 和 ${ } 的区别

在 SQL 映射文件中获取参数值时可以使用 #{ }${ } 来获取 Map 中的值或者 POJO 对象属性的值。

#{ } 和 ${ } 的区别

1、#{ } 采用 预编译 的形式,将参数添加到 SQL 语句中,类似 PreparedStatement,相当于 jdbc 的?。使用这种方式防止 SQL 注入,并会自动添加单引号。

2、${ } 取出的值会直接封装在 SQL 语句中,这种方式不能防止 SQL 注入,会有安全问题。简单来说,这种方式就是字符串的拼接,不会自动添加单引号,因此 为字符串类型或日期类型的字段进行赋值时,需要手动加单引号

3、大多数情况下,我们取参数的值都是使用 #{ }

4、对于原生 SQL 不支持占位符的地方(表名拼接、SQL 关键字等)我们就可以使用 ${ } 进行取值。

使用 ${ } 取值的示例

比如按照年份分表拆分时,可以这么使用:select * from ${year}_salary where xxxx

对查询内容进行排序时,可以这么使用:select * from user order by name{name} {order}

上述 ${order} 指的是排序方式,比如升序(ASC)、降序(DESC)

#{ } 内的参数

MyBatis 对 #{ } 还指定了一些参数:javaType、jdbcType、mode(存储过程)、numericScale、resultMap、typeHandler、jdbcTypeName。

jdbcType 通常需要在某些特定的环境下需要被设置。在我们数据为 null 时,有些数据库(比如 Oracle…)可能无法识别 MyBatis 对 null 的默认处理。如果不进行配置就会进行报错:JdbcType OTHER(无效的类型,MyBatis 对所有 null 都映射的是原生 JDBC 的 OTHER 类型)。

可以进行如下两种设置:

1、对需要为 null 的字段加上 jdbcType=NULL。eg:#{email, jdbcType=NULL}

2、或者进行全局配置:

<settings>
    <setting name = "jdbcTypeForNull" value = "NULL"/>
</settings>

3. select 标签

3.1 resultType

使用 <select> 标签进行数据查询时,通常需要设置这个标签的两个属性:

对于 resultType 属性来说,在使用时仍有需要注意的地方。👇

方法返回值是 List

如果返回的是一个集合(List),resultType 的值是集合中元素的类型。

比如,接口返回值类型是 List<User>,那么 resultType = com.yang.pojo.User

方法返回值是 Map

返回一条记录的 Map 时 ,key 就是数据库列名,value 就是对应的值:

public Map<String, Object> getUserByIdReturnMap(Integer id); 
<select id = "getUserByIdReturnMap" resultType = "map">
    select * from u ser where id = #{id}
</select>

多条记录封装成一个 Map 时,Map<Integer, User>:key 是这条记录的主键,value 是记录封装后的 JavaBean:

@MapKey("id")   //id为User中的属性,Map中key为主键
public Map<Integer, User> getUserByUsernameLikeReturnMap(String username)
// 如果需要查询username中带有“陈”的用户,数据传递为:%陈%
<select id="getUserByUsernameLikeReturnMap" resultType="com.yang.pojo.User">
    select * from user where username like #{username}
</select>

3.2 resultMap

这个标签用于数据库字段名与自定义的 JavaBean 属性名不同的场合 ,比如:

public User findUserById (Integer id);
<!-- 自定义某个JavaBean的封装规则
	type:自定义规则的Java类型
 	id:唯一id方便引用
-->
<resultMap type = "com.yang.pojo.User" id = "MyUser">
    <!-- 指定主键列的封装规则:id定义主键底层会进行优化 -->
    <id column = "id" property = "id" />
    <!-- column:指定数据库中的那一列
 		 property:指定对应的JavaBean属性-->
    <result column = "username" property = "username" />
    <!-- 其他未指定的列会自动封装,但是建议我们写resultMap就把全部的映射规则补齐 -->
    <result column = "pwd" property = "password" />
    <result column = "gender" property = "gender" />
    <result column = "email" property = "email" />
</resultMap>

<!-- resultMap:自定义结果集映射 -->
<select id = "findUserById" resultMap = "MyUser">
    select * from user where id = #{id}
</select>

3.3 关联查询

场景一

查询用户的同时查询用户所在的部门 (级联属性封装结果集)。

在 Mapper 接口中的方法如下:

public User getUserAndDept(Integer id);

则在 SQL 映射文件中可以这样编写:

<resultMap type = "com.yang.pojo.User" id = "MyDifUser">
	<id column = "id" property = "id" />
    <result column = "username" property = "username"/>
    <result column = "pwd" property = "password" />
    <result column = "gender" property = "gender" />
    <result column = "email" property = "email" />
    <result column = "did" property = "dept.id"/>
    <result column = "deptName" property = "dept.departmentName" />
</resultMap>
<select id = "getUserAndDept" resultMap = "MyDifUser">
	SELECT u.*, d.id did, d.deptName dept_name 
    FROM user u, dept d 
    WHERE u.id = d.id AND u.id = #{id}
</select>

除了上述的写法外,<resultMap> 标签内还有另一种写法,可以使用 <association> 标签定义关联的单个对象的封装规则

<resultMap type = "com.yang.pojo.User" id = "MyDifUser_2">
	<id column = "id" property = "id" />
    <result column = "username" property = "username"/>
    <result column = "pwd" property = "password" />
    <result column = "gender" property = "gender" />
    <result column = "email" property = "email" />
    <!-- association可以指定联合的JavaBean对象
		 property = "dept":指定那个属性是联合的对象,在这里指User类中的dept属性
		 javaType:指定这个属性对象的类型【不可省略】
	-->
    <association property = "dept" javaType = "com.yang.pojo.Department">
    	<id column = "did" property = "id"/>
        <result column = "deptName" property = "departmentName"/>
    </association> 
</resultMap>

<association> 标签还有其他的用处,我们可以使用 <association> 对场景一(查询用户的同时查询用户所在的部门)进行 分步查询

创建一个新的 Mapper 接口,取名为 DepartmentMapper,其中有一个方法:

public interface DepartmentMapper{
    //根据部门id查询部门
    public Department getDeptById(Integer id);
}

那么对应的 SQL 映射文件代码如下:

<mapper namespace = "com.yang.dao.DepartmentMapper">
	<select id = "getDeptById" resultType = "com.yang.pojo.Department">
    	SELECT id, deptName departmentName FROM dept WHERE id = #{id}
    </select>
</mapper>

UserMapper 接口中分步查询的抽象方法如下:

public User getUserByIdStep(Integer id);

对应的 SQL 映射文件代码:

<resultMap type = "com.yang.pojo.User" id = "MyUserByStep">
    <id column = "id" property = "id" />
    <result column = "username" property = "username"/>
    <result column = "pwd" property = "password" />
    <result column = "gender" property = "gender" />
    <result column = "email" property = "email" />
    <!-- association 定义关联对象的封装规则
 		 select:表明 property 指定的属性是调用 select 指定的方法查出的结果
		 column:指定将哪一列的值传给 select 指定方法
		 流程:使用 select 指定的方法(这个方法传入了 column 指定的这列参数值)查出对象,并封装给 property 指定的属性。
	-->
    <association property = "dept" 
                 select = "com.yang.dao.DepartmentMapper.getDeptById"
                 column = "did">
    </association>
</resultMap>
<select id = "getUserByIdStep" resultMap = "MyUserByStep">
	SELECT * FROM user WHERE id = #{id}
</select>

除此之外,利用分步查询还可以进行 延迟加载 :比如我们每次查询 User 对象的时候,都将部门信息一起查询了出来,但部门信息最好是在我们使用时才采取加载。

要想实现延迟加载,只需要在分段查询的基础上在全局配置文件加上两个配置:

<settings>
    <!-- 开启延迟加载 -->
    <setting name = "lazyLoadingEnabled" value = "true"/>
    <!-- 禁用时,每个属性按需加载 -->
    <setting name = "aggressiveLazyLoading" value = "false"/>
</settings>

场景二

在查询部门时,将部门对应的所有员工信息也查询出来(一对多)

前提,在 Department 类中添加属性:

public class Department{
    private Integer id;
    private String departmentName;
    private List<User> users;		//添加的属性
    /*省略get/set方法,toString()方法及相关构造函数*/
}

DepartmentMapper 接口中新增一个抽象方法:

public interface DepartmentMapper{
    //根据部门id查询部门信息和所有用户
    public Department getDeptByIdPlus(Integer id);
}

对应的 SQL 映射文件代码:

<resultMap type = "com.yang.pojo.Department" id = "MyDept">
	<id column = "id" property = "id" />
    <result column = "deptName" property = "departmentName"/>
    <!-- collection 定义关联的集合类型的属性的封装规则
		 ofType:指定集合内元素的类型
 	-->
    <collection property = "users" ofType = "com.yang.pojo.User">
        <!-- 定义这个集合中元素的封装规则 -->
        <id column = "uid" property = "id" />
    	<result column = "username" property = "username"/>
    	<result column = "password" property = "password" />
        <result column = "email" property = "email" />
    	<result column = "gender" property = "gender" />
    </collection>
</resultMap>
<mapper namespace = "com.yang.dao.DepartmentMapper">
	<select id = "getDeptByIdPlus" resultMap = "MyDept">
    	SELECT d.id did, d.deptName deptName,
        	u.id uid, u.username username, 
        	u.pwd password, u.email email, u.gender gender
        FROM dept d
        LEFT JOIN user u
        ON d.id = u.did
        WHERE d.id = #{id}
    </select>
</mapper>

上面这种实现的方式属于嵌套结果集的方式,使用 <collection> 标签定义关联的集合类型的属性封装规则。

跟场景一的实现一样,可以使用 <collection> (不是 <association> )对场景二(在查询部门时,将部门对应的所有员工信息也查询出来)进行 二分步查询

UserMapper 接口中新增一个抽象方法:

public List<User> getUsersByDeptId(Integer deptId);

UserMapper 接口对应的 SQL 映射文件代码:

<mapper namespace = "com.yang.dao.UserMapper">
	<select id = "getUsersByDeptId" resultType = "com.yang.pojo.User">
    	select * from user where did = #{deptId}
    </select>
</mapper>

DepartmentMapper 接口中新增一个抽象方法:

public interface DepartmentMapper{
    //根据部门id查询部门信息和所有用户
    public Department getDeptByIdStep(Integer id);
}

对应的 SQL 映射文件代码:

<resultMap type = "com.yang.pojo.Department" id = "MyDeptStep">
	<id column = "id" property = "id" />
    <result column = "deptName" property = "departmentName" />
    <collection property = "users"
                select = "com.yang.dao.UserMapper.getUsersByDeptId"
                column = "id">     
    <!-- 下述查询到的 id 传入 select 指定 SQL 的 deptId -->
    </collection>
</resultMap>
<select id = "getDeptByIdStep" resultMap = "MyDeptStep">
    SELECT id, deptName FROM dept WHERE id = #{id}
</select>

3.4 模糊查询

DepartmentMapper 接口中新增一个抽象方法:

/**
 * 根据用户模糊查询用户信息
 */
List<User> getUserByLike(@Param("username") String username);

对应的 SQL 映射文件代码:

<select id="getUserByLike" resultType="User">
    <!-- select * from user where username like '%${username}%' -->
    <!-- select * from user where username like concat('%', #{username}, '%') -->
    select * from user where username like "%"#{username}"%"
</select>

3.5 拓展

<collection> 标签中的 column 属性需要传递多列的值时,可以将多列的值封装换成 Map 进行传递,那么 column 可以这么写:

column= "{key1 = column1, key2 = column2}"

因此上文中,<collection> 标签中的 column 属性值也可以改写为:

column= "{deptId = id}"

<collection> 标签中还有一个名为 fetchType 的属性,这个属性 默认值lazy,表示使用 延迟加载 。就算在全局配置文件中开启了 延迟加载 ,我们也可以在某个 <collection> 标签内将 fetchType 属性设置为 eager,这样就可以将这条语句设置为 立即加载 (全局配置的延迟加载并没失效)。

鉴别器

仅作了解,基本用不到!

MyBatis 可以使用 <discriminator> (鉴别器)判断某列的值,然后根据某列的值改变封装行为。

比如,我们可以封装 User:

<resultMap type = "com.yang.pojo.User" id = "MyUserDis">
    <id column = "id" property = "id" />
    <result column = "username" property = "username"/>
    <result column = "pwd" property = "password" />
    <result column = "gender" property = "gender" />
    <result column = "email" property = "email" />
    <!-- column:指定判定的列名
    javaType:列值对应的 Java 类型-->
    <discriminator javaType = "string" column = "gender">
        <!-- 女生 resultType:指定封装的结果类型 这个属性不能缺少 -->
        <case value = "0" resultType = "com.yang.pojo.User">
            <association property = "dept" 
                         select = "com.yang.dao.DepartmentMapper.getDeptById"
                         column = "did">
            </association>
        </case>
        <case value = "1" resultType = "com.yang.pojo.User">
            <id column = "id" property = "id" />
            <result column = "username" property = "username"/>
            <result column = "pwd" property = "password" />
            <result column = "gender" property = "gender" />
            <result column = "username" property = "email" />
        </case>
    </discriminator>
</resultMap>

<select id = "getUserByIdStep" resultMap = "MyUserDis">
	SELECT * FROM user WHERE id = #{id}
</select>

4. 动态 SQL

4.1 动态 SQL 简介

动态 SQL 是 MyBatis 强大的特性之一,极大的简化了我们拼接 SQL 的操作。如果条件很多,使用原生的方式拼接 SQL 简直就是煎熬!😡

动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。

MyBatis 采用功能强大的基于 OGNL 表达式来简化操作:

涉及的文件

在本节中,Mapper 接口名为 UserMapperDynamicSQL,对应的 SQL 映射文件名为 UserMapperDynamicSQL,这两个文件都位于 com.yang.dao.UserMapper 包下。

4.2 条件判断

现在有一个需求:根据传递的信息查询对应的用户,这些信息可能是多个或没有,如果没有时,就查询出全部用户。

Mapper 接口的抽象方法如下:

// 携带了那个字段查询条件就带上这个字段的值
public List<User> getUsersByConditionIf(User user);

SQL 映射文件有以下几种编写方式:

<if> 标签

<!--查询用户:要求携带了哪个字段查询条件就带上这个字段的值-->
<select id="getUsersByConditionIf" resultType="com.yang.pojo.User">
    select * from user where
    /* test:判断表达式(OGNL),从参数中取值进行判断
    遇见特殊符号应该写转义字符  */
    <if test="id!=null">
        id = #{id}
    </if>
    <if test="username!=null and username!=''">
        and username like #{username}
    </if>
    <if test="email!=null and email.trim()!=''">
        and email = #{email}
    </if>
    /*OGNL 会进行字符串与数字的判断*/
    <if test="gender==0 or gender ==1">
        and gender = #{gender}
    </if>
</select>

<if> 标签中的 test 属性的值支持 OGNL 表达式,表达式中的字段(如:id、username…)是从抽象方法的参数中获取的,而非数据库字段。

OGNL 表达式还可以进行字符串和数字的判断,比如我需要判断 gender 是否等于 0,那么就可以直接写成:

gender == 0

这似乎像一个字符串,但是它就会对其进行判断!🐮

<where> 标签

像上面那样编写好 SQL 映射文件后似乎就完成了我们的需求?

其实并没有,查询的时候如果某些条件没带 sql 可能拼装会出现问题。比如:传入的参数中 id 为空,但是 username 不为空,这个时候使用上述方法拼接 SQL 就会出现问题。

为了解决这个问题,我们可以采用以下两种方式(说实话,推荐第一种,简单粗暴):

<where> 标签的使用:

<select id="getUsersByConditionIf" resultType="com.yang.pojo.User">
    select * from user
    <where>
        <if test="id!=null">
            id = #{id}
        </if>
        <if test="username!=null and username!=''">
            and username like #{username}
        </if>
        <if test="email!=null and email.trim()!=''">
            and email = #{email}
        </if>
        /*OGNL会进行字符串与数字的判断*/
        <if test="gender==0 or gender ==1">
            and gender = #{gender}
        </if>
    </where>
</select>

但需要注意的是: <where> 标签只会去掉首位多出来的 and 或者 or,不会去掉末尾的 and 或 or。

但是有些小机灵鬼就喜欢将 and 或者 or 写在末尾,那应该怎么办呢?

4.3 字符串截取

如果只使用 <where> 标签是无法解决拼接 SQL 末尾多余的 and 或 or,在 MyBatis 中,可以使用 <trim> 标签来解决这个问题。

使用 <trim> 标签后得到的 SQL 语句是 <trim> 标签体中整个字符串拼串后的结果。

<trim> 标签提供了 4 个属性用于规范拼串得到的 SQL 语句:

1、prefix:前缀。使用这个属性后,会为拼串得到的 SQL 添加一个 前缀

2、suffix:后缀。使用这个属性后,会为拼串得到的 SQL 添加一个 后缀

3、prefixOverrides:前缀覆盖。使用这个属性后,会 去除 拼串得到的 SQL 多余的字符串。

3、suffixOverrides:后缀覆盖。使用这个属性后,会 去除 拼串得到的 SQL 多余的字符串。

如果将 and 或 or 写在了末尾,那么就可以这样书写 SQL 映射文件:

<select id="getUsersByConditionTrim" resultType="com.yang.pojo.User">
    select * from user
    <trim prefix="where" suffixOverrides="and">
        <if test="id!=null">
            id = #{id} and
        </if>
        <if test="username!=null and username!=''">
            username like #{username} and
        </if>
        <if test="email!=null and email.trim()!=''">
            email = #{email} and
        </if>
        /*OGNL会进行字符串与数字的判断*/
        <if test="gender==0 or gender ==1">
            gender = #{gender}
        </if>
    </trim>
</select>

Mapper 接口的抽象方法如下:

public List<User> getUsersByConditionTrim(User user);

4.4 分支选择

在 Java 语言中,用于条件判断的关键字是 if,还有一些用于分支选择的关键字 switchcasedefault,在 MyBatis 的动态 SQL 中,也提供了分支选择的标签:<choose><when><otherwise>

假设现在有一个需求:根据携带的条件(id、username、email)在用户表中查询用户信息,携带的条件只能使用一个。如果没有携带任何条件,就查询性别为女(gender = 0)的用户。

Mapper 接口的抽象方法如下:

public List<User> getUsersByConditionChoose(User user);

对应的 SQL 映射文件内容如下:

<!--如果带了id就用id查,如果带了username就用username查,且只会进入其中一个-->
<select id="getUsersByConditionChoose" resultType="com.yang.pojo.User">
    select * from user
    <where>
        <choose>
            <when test="id!=null and id!=''">
                id = #{id}
            </when>
            <when test="username!=null and username!=''">
                username = #{username}
            </when>
            <when test="email!=null and email!=''">
                email = #{email}
            </when>
            <otherwise>
                gender = 0
            </otherwise>
        </choose>
    </where>
</select>

题外话

在 Java 中,使用 switch 关键词时并不强制要求使用 default 关键词。

switchcase 组成一个条件选择语句,找到相同的 case 值做为入口,执行后面的程序;若所有的 case 都不满足,则找 default 入口;若未找到则退出整个 switch 。所以 default 只是一个备用的入口,有没有都无所谓。

不写 default 是建立在自己书写的条件已经包含了所有情况下,为了程序逻辑的完整性,建议写上 default

PS:在阿里巴巴的代码规范中,switchcase 后强制要求书写 default

4.5 封装修改条件

<where> 标签用于封装查询条件,而 <set> 标签可用于封装修改条件。

在查询数据时,可能会存在多出的 and 或 or,我们可以使用 <where><trim> 来解决。

在更新数据时,可能会存在多出的逗号,那么可以使用 <set><where> 来解决。

Mapper 接口的抽象方法如下:

public void updateUser(User user);

对应的 SQL 映射文件内容如下:

<update id="updateUser">
    update user
    <set> /*set标签可以取消多余的逗号*/
        <if test="username!=null and username!=''">
            username = #{username},
        </if>
        <if test="email!=null and email!=''">
            email = #{email},
        </if>
        <if test="gender==0 or gender ==1">
            gender = #{gender}
        </if>
    </set>
    where id = #{id}
</update>

除了使用 <set> 标签来解决多余的逗号外,还可以使用 <trim> 标签,使用该标签中的 suffixOverrides 属性,将其值指定为 , 即可。

4.6 遍历

现在有一个需求:接口的抽象方法接收到一个包含了多个 id 的 List 集合,需要从数据库中查询这些这些 id 对应的用户信息。

我们可以将每个 id 一一取出来,然后进行查询,这个时候就需要用到 MyBatis 中的遍历了,需要用到 <foreach> 标签。

具体使用

Mapper 接口的抽象方法如下:

public List<User> getUsersByConditionForeach(@Param("ids") List<Integer> ids);

对应的 SQL 映射文件内容如下:

<select id="getUsersByConditionForeach" resultType="com.yang.pojo.User">
        select * from user where id in
        <foreach collection="ids" item="item_id" separator=","
                open="(" close=")">
            #{item_id}
        </foreach>
    </select>

标签属性

<foreach> 中主要有以下几个属性:

1、collection:指定要遍历的集合,该属性为必选

2、item:集合中元素迭代时的别名,该属性为必选

3、separator:每个元素之间的分隔符

4、open:遍历出所有结果拼接一个开始的字符

5、close:遍历出所有结果拼接一个结束的字符

6、index:索引。遍历 list 或数组时,index 是索引,item 是当前值;遍历 map 时,index 是 map 的 key,item 是 map 的值

最后使用 #{item} 就能取出每个遍历的值,如果该元素是一个对象,可以使用 #{item.xx} 的方式取出这个对象对应的成员变量的值。

collection 属性

参考链接:mybatis之foreach用法

<foreach> 标签中,collection 属性是必选的,表示需要进行遍历的对象。该属性的属性值一般是接口抽象方法中的需要被遍历的参数。

如果这个参数是 List 对象,默认用 “list” 代替作为键;如果这个参数是数组,默认用 “array” 代替作为键。这两个默认值相当于 MyBatis 内置的别名。

在作为入参时还可以使用 @Param("keyName") 来设置键,设置 keyName 后,两个默认值 list 和 array 将会失效。

4.7 MySQL 批量保存

在 MySQL 环境下,使用 MyBatis 实现批量保存可以使用 <foreach> 进行遍历,MySQL 支持以下语法:

 values(), (), () 

Mapper 接口的抽象方法如下:

public void addUsers(@Param("users") List<User> users);

对应的 SQL 映射文件内容如下:

<!--mysql批量保存-->
<insert id="addUsers">
    insert into user(username,pwd,email,gender,did)
    values
    <foreach collection="users" item="user" separator=",">
        (#{user.username},#{user.password},#{user.email},
        #{user.gender},#{user.dept.id})
    </foreach>
</insert>

除此之外,还可以执行多条 SQL 语句,这些 SQL 语句使用分号进行隔开。

MySQL 下允许在一条语句中使用 分号 来分割多条查询,但这个功能默认是关闭的,开启这个功能需要在 jdbc.url(数据库连接属性) 中添加设置 allowMultQueries = true

4.8 其他

MyBatis 默认内置参数

1、_parameter:代表整个参数体

2、_databaseId:如果配置了 <databaseIdProvider> 标签,_databaseId 就是代表当前数据库的别名

<bind> 标签

这个标签可以将 OGNL 表达式的值绑定到一个变量中,方便后面引用这个变量的值。

比如模糊查询 username 时,就可以:

<bind name = "_username" value = "'%'+username+'%'"/>

<sql> 标签

这个标签可用于抽取可重用的 sql 片段,方便以后引用。例如:

<sql id="insertColumn">
 id,username,email
</sql>

<sql> 标签内部也可以进行动态判断,比如使用标签 <if> 进行判断。

在 IDEA 中使用 <sql> 标签时,IDEA 可能会出现这样的错误提示:

<statement> or DELIMITER expected, got 'xxxxx'

出现这样的错误提示很正常(应该属于 IDEA 的 BUG), 大多数情况下 是不影响正常使用的。

<include> 标签

这个标签可以用来引用外部定义的 <sql> 标签内的内容。

1、<include refid="insertColumn"/>

2、还可以在 <include> 标签中定义一个 <property> 标签,用于自定义属性。

3、include-property 取值的正确方式是 ${prop},不能使用 #{ } 取值。

5. 缓存

MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地进行配置和定制。缓存可以极大的提升查询效率。

5.1 一级缓存

一级缓存又被称为本地缓存,属于 SqlSession 级别的缓存,一级缓存是 一直开启,不可被关闭的 。与数据库同一次会话期间查询到的数据会被放到本地缓存中,以后如果需要获取相同的数据,直接从缓存中拿,不会再去查询数据库。通常只有在单机的情况下才有效,集群情况下会有数据不一致的问题。

一级缓存失效的情况

所谓一级缓存失效,就是在获取相同的数据时还需要向数据库发出查询请求。

1、SqlSession 不同

2、SqlSession 相同,查询条件不同(缓存中没有数据)

3、SqlSession 相同,两次查询之间执行了增删改操作(可能对当前数据有影响)

4、SqlSession 相同,手动清除了一级缓存,使用 sqlSession.clearCache()

手动修改数据库数据

直接修改数据库数据不会让一级缓存失效。这是什么意思呢?

假设先根据 id 在数据库中查询一条用户信息,然后程序休眠 10 秒,在这 10 秒内迅速去数据库修改这个 id 对应的用户信息,最后再次查询这个 id 对应的用户信息。

最终这两次得到用户信息是一样的,都是未修改前的信息。


部分测试代码:

@Test
public void testFindOne() {
    Users user1 = userDao.findById(3);
    System.out.println(user1);
    // 在这 10 秒内手动修改数据库数据
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Users user2 = userDao.findById(3);
    System.out.println(user2);
    System.out.println(user1 == user2);
}

控制台打印结果:

手动修改数据库一级缓存不失效

可以看到整个过程只执行了一条 SQL,两次查询得到的对象也是同一个,证明手动修改数据库不会令一级缓存失效。

5.2 二级缓存

二级缓存又称为全局缓存,基于 namespace 级别的缓存,一个 namespace 对应一个二级缓存,同时一个 namespace 也对应了一个 SQL 映射文件。

工作机制

1、一个会话:查询一条数据,这个数据就会被放到当前会话的 一级缓存

2、如果会话关闭,一级缓存中的数据就会被保存到二级缓存,新的会话查询信息就可以参照二级缓存中的内容

3、不同的 namespace 查询出的数据会被放在自己对应的缓存中(map)

使用

<settings>
    <!--在全局配置文件中开启二级缓存配置-->
    <setting name = "cacheEnabled" value = "true"/> 
</settings>

再去相关的 SQL 映射文件中配置使用二级缓存:<cache></cache>,加上这个标签就行。当然也可以对这个标签的一些属性进行设置。

最后实体类需要 实现序列化接口 Serializable。二级缓存必须在 SqlSession 关闭或提交之后有效。

两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效


启用二级缓存后,数据会从二级缓存中获取。首次查询出的数据会被默认放到一级缓存中,只有对话关闭 sqlSession.close() 或提交后,一级缓存中的数据 才会 转移到二级缓存中。

SQL 映射文件中 <cache> 标签属性

1、eviction:缓存的回收机制

2、flushInterval:缓存刷新间隔。缓存多久清空一次,默认不清空,单位是毫秒。默认不设置。

3、readOnly:是否只读。

4、size:缓存存放多少元素。可被设置为任意正整数,缓存的对象数目等于运行环境的可用内存资源数目。默认是 1024。

5、type:指定自定义缓存的类名。需要实现 Cache 接口。

5.3 相关设置与属性

全局配置文件中 <setting> 标签中的 cacheEnabled:设置为 false 时,关闭二级缓存,一级缓存一直可用且不可被关闭。

每个 <select> 标签默认都有一个 useCache="true" 属性。这个属性设置为 false 时,不使用二级缓存,但一级缓存依然可用。

每个增删改标签中 默认 都有 flushCache=“true”,表示执行增删改后 一、二级缓存都被清空

<select> 标签中 默认 有 flushCache=“false”,若设置为 true,每次查询后都会清空所有缓存。

sqlSession.clearCache() 表示只清空当前 sqlSession 的一级缓存,但关闭后二级缓存也无法使用,因为一级缓存已被清空了。

在全局配置文件中 <setting> 标签有一个属性 localCacheScope,表示本地缓存作用域。

缓存的顺序:二级缓存 —> 一级缓存 —> 数据库

SqlSession 关闭之后,一级缓存中数据会写入二级缓存

5.4 整合 EhCache

PS:了解即可,这年头谁还用这玩意啊,都用 Redis 了 😏

首先需要在 pom.xml 中导入相关依赖:

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>

然后前往需要启用 EhCache 缓存的 SQL 映射文件中,添加以下代码:

<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

最后还需要在类路径下编写 ehcache.xml 文件,如果在加载时未找到 /ehcache.xml 资源或该资源出现问题,则将使用默认配置。配置文件标签和参数信息可以自行查阅~ 😜

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">

    <!-- 磁盘缓存位置 -->
    <diskStore path=".tmpdir/ehcache"/>

    <!-- 默认缓存 -->
    <defaultCache
                  maxEntriesLocalHeap="10000"
                  eternal="false"
                  timeToIdleSeconds="120"
                  timeToLiveSeconds="120"
                  maxEntriesLocalDisk="10000000"
                  diskExpiryThreadIntervalSeconds="120"
                  memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>

    <!-- helloworld 缓存 -->
    <cache name="HelloWorldCache"
           maxElementsInMemory="1000"
           eternal="false"
           timeToIdleSeconds="5"
           timeToLiveSeconds="5"
           overflowToDisk="false"
           memoryStoreEvictionPolicy="LRU"/>
</ehcache>

如果每个 SQL 映射文件都要使用 EhCache,那岂不是每次都要写一遍 <cache></cache>,这里还有一个标签:

<cache-ref namespace="com.yang.dao.IUserDao"/>

这个标签表示引用缓存,它有一个名为 namespace 的属性,可以通过这个属性指定当前 SQL 映射文件使用的缓存和哪个名称空间下的缓存一样。

6. 运行原理

以「根据员工 ID 查询员工信息」为例:

6.1 执行流程

1、读取配置文件

InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");

2、创建 SqlSessionFactory 工厂

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);

3、使用工厂生产 SqlSession 对象,设置自动提交事务

SqlSession sqlSession = factory.openSession(true);
//也可在执行增删改查方法后手动提交事务
// sqlSession.commit();

4、获取接口的代理对象(MapperProxy)

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

5、执行增删改查方法

6、释放资源

sqlSession.close();
in.close();

6.2 SqlSessionFactory 初始化

执行流程

initializeSqlSessionFactory

MappedStatement:一个 MappedStatement 就代表 SQL 映射文件中一个增删改查的详细信息。

Configuration 对象封装了所有配置文件的详细信息。

总结: 把配置文件的信息解析并保存在 Configuration 对象中,返回包含了 Configuration 的 DefaultSqlSessionFactory 对象。

6.3 openSession 获取 SqlSession 对象

执行流程

getSqlSession

总结: 返回 SqlSession 的实现类 DefaultSqlSession 对象,它里面包含了 Executor 和 Configuration,四大对象中的 Executor 会在这一步被创建。

SqlSession 和 JDBC 中的 Connection 一样,是 非线程安全 的,不能将 SqlSession 写成一个成员变量,SqlSession 代表和数据库的一次对话,每次使用都应该去重新获取,用完后必须关闭。

6.4 getMapper 获取接口代理对象

执行流程:

getObject

总结: getMapper 使用 MapperProxyFactory 创建一个 MapperProxy 的代理对象,这个代理对象包含了 DefaultSqlSession(Executor)对象。

6.5 查询实现

执行流程

queryProcess

查询流程总结

processSummary

6.6 原理总结

1、根据配置文件(全局配置文件、SQL 映射文件)初始化 Configuration 对象

2、创建一个 DefaultSqlSession 对象,里面包含 Configuration 以及 Executor(根据全局配置文件中的 defaultExecutorType 创建出对应的 Executor)

3、DefaultSqlSession.getMapper():拿到 Mapper 接口对应的 MapperProxy

4、MapperProxy 中有 DefaultSqlSession

5、执行增删改查方法:

6、注意:每个四大对象创建的时候都有一个 interceptorChain.pluginAll(parameterHandler)

7. 插件

7.1 插件原理

MyBatis 四大对象创建的时候:

1、每个创建出来的对象不是直接返回的,而是调用 interceptorChain.pluginAll(parameterHandler) 后返回的

2、pluginAll() 可以获取所有的 interceptor(拦截器)(插件需要实现的接口),调用 interceptor.plugin(target); 返回 target 包装后的对象

插件机制:我们可以使用插件为目标对象创建一个代理对象(类似 AOP)。

我们的插件可以为四大对象创建出代理对象,代理对象可以拦截到四大对象的每一个执行。

7.2 插件编写

编写流程

1、编写 Interceptor 的实现类

2、使用 @Intercepts 注解完成插件签名

3、将写好的插件注册到全局配置文件中

编码实现

MyFirstPlugin.class

package com.yang.dao.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
//完成插件的签名:告诉MyBatis当前插件用来拦截哪个对象的哪个方法
@Intercepts({
        @Signature(type = StatementHandler.class, // 四大对象的哪一个
                method = "parameterize", // 拦截对象的哪个方法
                args = java.sql.Statement.class) // 方法参数列表
})
public class MyFirstPlugin implements Interceptor {  
     //intercept(拦截):拦截目标对象的目标方法的执行
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
        Object target = invocation.getTarget();
        System.out.println("当前拦截到的对象:"+ target);
        MetaObject metaObject = SystemMetaObject.forObject(target);
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        System.out.println("sql语句用的参数是: "+ value);
        // 偷梁换柱 让sql的参数变成 5 
        metaObject.setValue("parameterHandler.parameterObject",5);
        
        //可以在执行目标方法前进行一系列操作!
        
        //执行目标方法
        Object proceed = invocation.proceed();
        
        //可以在执行目标方法后进行一系列操作!
        
        //返回执行后的返回值
        return proceed;
    }

    /*
     * plugin:包装目标对象,为目标对象创建一个代理对象
     */
    public Object plugin(Object target) {
        //借助Plugin的wrap方法来使用当前Interceptor包装我们的目标对象
        System.out.println("MyFirstPlugin...plugin:MyBatis将要包装的对象"+target);
        Object wrap = Plugin.wrap(target, this);
        //返回当前target创建的动态代理
        return wrap;
    }

    /**
     *setProperties:将插件注册是的property属性设置进来
     */
    public void setProperties(Properties properties) {
        System.out.println("插件配置的信息"+properties);
    }
}

在主配置文件中注册插件:

<!--plugins注册插件-->
<plugins>
    <!-- 插件全类名 -->
    <plugin interceptor="com.yang.dao.MyFirstPlugin">
        <property name="username" value="root"/> 		<!--相关插件信息-->
        <property name="password" value="123456"/>
    </plugin>
</plugins>

运行后控制台打印结果:

插件配置的信息{password=123456, username=root}
MyFirstPlugin...plugin:MyBatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@77e9807f
MyFirstPlugin...plugin:MyBatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@2c34f934
MyFirstPlugin...plugin:MyBatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@548a102f
MyFirstPlugin...plugin:MyBatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@17c386de
MyFirstPlugin...intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@17c386de
sql语句用的参数是3
Users{id=5, username='123456', password='789'}

注意

如果有多个插件就会产生多层代理:

多个插件执行顺序

8. 其他

8.1 日志

使用日志:

在全局配置文件中添加(以使用 STDOUT_LOGGING 为例):

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    ...
  </settings>
</configuration>

有效值:

SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

Java 代码中使用(以 log4j 为例):

static Logger logger = logger.getLogger("xxxxxx.class");	//当前类的class
logger.info("xxxx");
logger.debug("xxxx");
logger.error("xxxx");
...

8.2 分页

方法一: SQL 语句使用 limit 子句:

select * from tableName limit i,n
# tableName:表名
# i:为查询结果的索引值(默认从0开始),当i=0时可省略i
# n:为查询结果返回的数量
# i与n之间使用英文逗号","隔开

limit n 等同于 limit 0,n

方法二: 使用 PageHelper 插件进行分页(项目中需要先导入相关依赖):PageHelper官网

8.3 批量

为什么要批量执行?数据库无法接收太长的拼接 SQL 语句,因此需要批量执行。

批量与非批量的比较(假设插入 1w 条数据):

实现方式:

//ExecutorType.BATCH表示将重用语句并执行批量更新
//true表示自动提交事务
SqlSession sqlSession = factory.openSession(ExecutorType.BATCH, true);

与 Spring 整合时参考视频:尚硅谷 MyBatis 3.x P84

8.4 存储过程

参考视频:尚硅谷 MyBatis 3.x P85

8.5 处理枚举类型

准备

假设原始数据库用户表中再添加一列 userStatus,用于保存用户的状态,最终用户表形式为:

user table:
id | username | pwd | email | gender | userStatus | did

自定义枚举类:

public enum UserStatus {
    LOGIN,LOGOUT,REMOVE;
}

测试枚举的使用:

@Test // junit 注解
public void testEnumUse(){
    UserStatus login = UserStatus.LOGIN;
    //索引从0开始
    System.out.println("枚举的索引: "+login.ordinal());
    System.out.println("枚举的名字: "+login.name());
}

在 User 实体类中添加属性:

//用户状态
private UserStatus userStatus = UserStatus.LOGOUT;

尝试在数据库中添加一个用户,并得到结果…

总结

进行测试后发现,MyBatis 默认使用的是 EnumTypeHandler,保存到数据库的是枚举名。

按照 1.4 typeHandlers 更改使用的类后,保存到数据库的枚举的索引(索引从 0 开始)。

8.6 自定义类型处理器

8.5 处理枚举类型 的基础上进行修改,使数据库保存的是我们自定义的状态码,最后返回的是枚举名:

package com.yang.domain;
/**
 * 希望数据库保存的是状态码(100、200、300...)
 */
public enum UserStatus {

    LOGIN(100, "用户登录"), LOGOUT(200, "用户登出"), REMOVE(300, "用户不存在");

    private Integer code;
    private String msg;

    private UserStatus(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    //按照状态码返回枚举对象
    public static UserStatus getUserStatusByCode(Integer code) {
        switch (code) {
            case 100:
                return LOGIN;
            case 200:
                return LOGOUT;
            case 300:
                return REMOVE;
            default:
                return REMOVE;
        }
    }
}

编写自定义类型处理器:

package com.yang.typehandler;
/**
 * 实现TypeHandler接口或者继承BaseTypeHandler
 */
public class MyEnumUserStatusTypeHandler implements TypeHandler<UserStatus> {
    /**
     *定义当前数据库如何保存到数据库
     */
    public void setParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
        System.out.println("要保存的状态码是: " + parameter.getCode());
        // 状态码是整型,数据库定义的类型是字符串类型,因此需要 toString() 方法转换
        ps.setString(i,parameter.getCode().toString());
    }

    public UserStatus getResult(ResultSet rs, String columnName) throws SQLException {
        //需要根据从数据库拿到的枚举状态码返回一个枚举对象
        int code = rs.getInt(columnName);
        System.out.println("从数据库中获取的状态码: "+code);
        UserStatus status = UserStatus.getUserStatusByCode(code);
        return status;
    }

    public UserStatus getResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        System.out.println("从数据库中获取的状态码: "+code);
        UserStatus status = UserStatus.getUserStatusByCode(code);
        return status;
    }

    public UserStatus getResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        System.out.println("从数据库中获取的状态码: "+code);
        UserStatus status = UserStatus.getUserStatusByCode(code);
        return status;
    }
}

最后记得在全局配置文件中进行配置:

<typeHandlers>
    <!--使用自定义类型处理器有以下两种方式:-->
    <!--1.配置我们自定义的TypeHandler-->
    <typeHandler handler="com.yang.typehandler.MyEnumUserStatusTypeHandler" 															javaType="com.yang.domain.UserStatus"/>
    <!--2.也可以在处理某个字段的时候告诉MyBatis用什么类型处理器
            保存:#{userStatus, typeHandler=xxxx}
            查询:使用resultMap。其中<result>标签中添加typeHandler属性并设置值。
          注意:如果在参数位置修改TypeHandler,应该保证保存数据和查询数据用的TypeHandler是一样的。
    -->
</typeHandlers>

经过上面的编写后,可以让枚举保存到数据库的数据是自定义的状态码,而不是枚举从 0 开始的索引。

如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
HBase的安装
下一篇
从 0 开始的 SpringMVC 学习