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

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

0. MyBatis 的初步使用

0.1 为什么要使用 MyBatis?

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

JDBC

  • SQL 夹在 Java 代码块里,耦合度高导致硬编码内伤
  • 维护不易且实际开发需求中 SQL 是有变化的,频繁修改的情况很多见

Hibernate 和 JPA

  • 长难复杂 SQL ,对于 Hibernate 而言处理也不容易
  • 内部自动生产的 SQL,不容易做特殊优化
  • 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难,导致数据库性能下降

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

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

0.2 简单使用步骤

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

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

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

4、写代码

  • 根据全局配置文件创建一个 SqlSessionFactory 对象
  • 使用 SqlSession 工厂,获取到 SqlSession 实例,然后使用它进行 CRUD (一个 SqlSession 代表和数据库的一次会话,使用后记得关闭)
  • 使用 SQL 的唯一标志来告诉 MyBatis 执行哪一个 SQL ,SQL 都是保存在 SQL 映射文件中的

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

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

1. 全局配置文件标签

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

全局配置文件约束信息:

1
2
3
4
<?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

  • 作用:使用该标签可以引入外部 properties 配置文件(数据库连接信息)的内容。

  • <properties> 标签属性:

    resource:引用类路径下的资源

    url:引用网络路径下或磁盘路径下的资源

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

1.2 settings

  • <settings> 标签包含很多重要的设置项,一个 <settings> 标签下可以包含多个 <setting> 标签, <setting> 标签用来设置一个设置项。

  • <setting> 标签属性:

    name:设置相关的参数

    value:参数的有效值。

  • 属性列表(以官方文档 mybatis – MyBatis 3 | Configuration 为准,下图仅供参考):

setting标签属性_1

setting标签属性_2

setting标签属性_3

使用示例

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

1.3 typeAliases

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

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

单个别名

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

<typeAliase> 标签属性:

  • type:指定要取别名的类型全类名,默认别名就是类名首字母小写。

  • alias:指定要取的别名。

批量取别名

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

<package> 标签属性:

  • name:指定包名(为当前包以及下面所有后代包的每一个类都取一个默认别名,默认别名是类名首字母小写)

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

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

MyBatis内建部分别名

1.4 typeHandlers

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

使用示例

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

1
2
3
4
<!--自定义枚举为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> 标签属性:

  • default:指定当前 MyBatis 使用的环境,可以达到快速切换环境。

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

1.7 environment

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

<environment> 标签属性:

  • id:指定当前环境的唯一标识。

1.8 transactionManager

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

<transactionManager> 标签属性:

  • type:事物管理器的类型。值有两种选择:JDBC 、 MANAGED ,这两个值其实是别名(分析源码可得)。

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

1.9 dataSource

作用:配置数据源。

<dataSource> 标签属性:

  • type:数据库类型。值有三种选择:UNPOOLED(不使用连接池) 、 POOLED(使用连接池) 、 JNDI ,这三个值也是别名(分析源码可得)。

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

1.10 databaseIdProvider

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

<databaseIdProvider> 标签属性:

  • type:值为 DB_VENDOR(这也是一个别名) :VendorDatabaseIdProvider.class。type 属性作用就是得到数据库厂商的标识(驱动getDatabaseProductName()),MyBatis 能够根据数据库厂商标识来执行不同的 SQL 语句。
  • 常见数据库厂商标识:MySQL、Oracle、SQL Server。

具体使用

1
2
3
4
5
6
<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 属性并指定全局配置文件中设置的数据库厂商标识别名。比如:

1
2
3
<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> 标签属性:

  • resource:引用类路径下的 SQL 映射文件(注册配置文件)。

  • url:引用网络路径下或磁盘路径下的 SQL 映射文件(注册配置文件)。

  • class:引用接口(注册接口)。

class 属性使用规范

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

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

<package> 标签

<package> 标签属性:

  • name:SQL 映射文件(或接口)的包路径。

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

1.12 标签书写顺序

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

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

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

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

2.1 SQL 映射文件约束

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

SQL 映射文件约束信息:

1
2
3
4
<?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语句环境都如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()方法及相关构造函数*/
}

数据库表名与字段名:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
<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> 的标签,见名识意,这个标签就是用于“查询主键”的。在这个标签中,有三个属性:

  • keyProperty:查出的主键封装给 JavaBean 的哪一个属性。比如我想将查出的主键封装给 id 属性,那么就像上述代码那样书写即可。
  • order:用于指定 <selectKey> 包裹的 SQL 语句在插入的 SQL 语句执行之前(BEFORE)或之后(AFTER)执行。
  • resultType:指定 <selectKey> 包裹的 SQL 语句查询的数据的返回值类型。

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


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

1
2
3
4
5
6
7
8
<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 中:

  • key:param1 … paramN,或者参数的索引(0、1…)

  • value:传入的参数值

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

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

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

命名参数

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

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

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

  • key:使用 @Param 注解指定的值
  • value:传入的参数值

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

实体类参数

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

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

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

2.5 参数处理思考

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

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

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

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

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

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

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

1
public User getUserById(List<Integer> ids);

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

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

  • 对于 Collection 集合来说,key 是 collection
  • 对于 List 来说,key 是 list
  • 对于数组来说,key 是 array

因此,对于上述给出的接口,我们想在 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} ${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、或者进行全局配置:

1
2
3
<settings>
<setting name = "jdbcTypeForNull" value = "NULL"/>
</settings>

3. select 标签

3.1 resultType

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

  • 一个属性是 id ,值是 Mapper 接口中的方法名
  • 另一个属性是 resultType,值是查询结果的类型,即:接口中抽象方法返回值类型。不能和 resultMap 同时使用。

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

方法返回值是 List

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

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

方法返回值是 Map

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

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

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

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

3.2 resultMap

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

1
public User findUserById (Integer id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 自定义某个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 接口中的方法如下:

1
public User getUserAndDept(Integer id);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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> 标签定义关联的单个对象的封装规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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> 对场景一(查询用户的同时查询用户所在的部门)进行 分步查询

  • 先按照用户 id 查询用户信息
  • 再根据查询出的用户信息中的 did 值去部门表查出部门信息
  • 最后将部门信息设置到员工信息中

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

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

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

1
2
3
4
5
<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 接口中分步查询的抽象方法如下:

1
public User getUserByIdStep(Integer id);

对应的 SQL 映射文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 对象的时候,都将部门信息一起查询了出来,但部门信息最好是在我们使用时才采取加载。

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

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

场景二

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

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

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

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

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

对应的 SQL 映射文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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 接口中新增一个抽象方法:

1
public List<User> getUsersByDeptId(Integer deptId);

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

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

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

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

对应的 SQL 映射文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 接口中新增一个抽象方法:

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

对应的 SQL 映射文件代码:

1
2
3
4
5
<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 可以这么写:

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

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

1
column= "{deptId = id}"

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

鉴别器

仅作了解,基本用不到!

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

比如,我们可以封装 User:

  • 如果查询结果是女生,就把部门信息查询出来,否则不查询
  • 如果查询结果是男生,把 username 这一列的值赋值给 email 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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 表达式来简化操作:

  • if
  • choose(when,otherwise)
  • trim(where,set)
  • foreach

涉及的文件

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

4.2 条件判断

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

Mapper 接口的抽象方法如下:

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

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

  • 使用 <if> 标签
  • 使用 <if><where> 标签

<if> 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--查询用户:要求携带了哪个字段查询条件就带上这个字段的值-->
<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 后面加一个 1=1 或 true ,以后的条件都是 and xxxx 的格式
  • MyBatis 推荐使用 <where> 标签来将所有的查询条件包括在内,然后MyBatis 就会将 <where> 标签中拼装的 SQL 中多出来的 and 或者 or 去掉

<where> 标签的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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 映射文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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 接口的抽象方法如下:

1
public List<User> getUsersByConditionTrim(User user);

4.4 分支选择

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

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

Mapper 接口的抽象方法如下:

1
public List<User> getUsersByConditionChoose(User user);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--如果带了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 接口的抽象方法如下:

1
public void updateUser(User user);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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 接口的抽象方法如下:

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

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

1
2
3
4
5
6
7
<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 支持以下语法:

1
values(), (), () 

Mapper 接口的抽象方法如下:

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

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

1
2
3
4
5
6
7
8
9
<!--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:代表整个参数体

  • 单个参数:_parameter 就是这个参数
  • 多个参数:参数会被封装成一个 map,_parameter 就是代表这个 map

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

<bind> 标签

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

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

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

<sql> 标签

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

1
2
3
<sql id="insertColumn">
id,username,email
</sql>

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

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

1
<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 对应的用户信息。

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


部分测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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)

使用

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

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

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

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


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

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

1、eviction:缓存的回收机制

  • LRU:最近最少使用的(默认),移除最长时间不被使用的对象

  • FIFO:先进先出,按对象进入缓存的顺序来移除它们

  • SOFT:软引用,除移基于垃圾回收器状态和软引用规则的对象

  • WEAK:弱引用,更积极地移除基于垃圾回收器和弱引用规则的对象

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

3、readOnly:是否只读。

  • true:只读。MyBatis 认为从所有缓存中获取数据的操作都是只读的,不会修改数据。MyBatis 为了加快获取速度,直接就将数据在缓存中的应用交给用户。不安全,速度快。

  • false:非只读。MyBatis认为获取的数据可能会被修改。MyBatis 会利用序列化和反序化的技术克隆一份数据。安全,速度慢。 默认值。

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,表示本地缓存作用域。

  • SESSION:使用一级缓存,当前会话所有数据保存在会话缓存中。
  • STATEMENT:相当于禁用一级缓存。

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

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

5.4 整合 EhCache

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

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

1
2
3
4
5
6
<!-- 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 映射文件中,添加以下代码:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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> ,这里还有一个标签:

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

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

6. 运行原理

以“根据员工ID查询员工信息”为例:

6.1 执行流程

1、读取配置文件

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

2、创建 SqlSessionFactory 工厂

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

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

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

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

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

5、执行增删改查方法

6、释放资源

1
2
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、执行增删改查方法:

  • 调用 DefaultSqlSession 的增删改查( 调用 Executor 的增删改查)

  • 创建一个 StatementHandler 对象(同时也会创建出 ParameterHandlerResultSetHandler

  • 调用 StatementHandler 预编译参数以及设置参数值,使用 ParameterHandler 来给 SQL 语句设置参数

  • 调用 StatementHandler 的增删改查方法

  • 使用 ResultSetHandler 封装结果

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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);
}
}

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

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

运行后控制台打印结果:

1
2
3
4
5
6
7
8
9
插件配置的信息{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 为例):

1
2
3
4
5
6
7
<configuration>
<settings>
...
<setting name="logImpl" value="STDOUT_LOGGING"/>
...
</settings>
</configuration>

有效值:

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

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

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

8.2 分页

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

1
2
3
4
5
6
7
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条数据):

  • 非批量:(预编译sql次数 = 设置参数次数 = 执行次数) ==> 10000次
  • 批量:预编译一次 ==> 设置参数10000次 ==> 执行1次

实现方式:

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

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

8.4 存储过程

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

8.5 处理枚举类型

准备

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

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

自定义枚举类:

1
2
3
public enum UserStatus {
LOGIN,LOGOUT,REMOVE;
}

测试枚举的使用:

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

在 User 实体类中添加属性:

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

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

总结

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

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

8.6 自定义类型处理器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}
}
}

编写自定义类型处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
}
}

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

1
2
3
4
5
6
7
8
9
10
<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 开始的索引。