封面画师: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 代码编写)。
全局配置文件约束信息:
<?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 为准,下图仅供参考)



使用示例
<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 类型提供了许多内建的类型别名,因此我们在自定义别名时应当避免与这些别名相同。比如下图的部分内建别名(全部内建别名请参考官方文档):
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> 标签属性:
- 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
具体使用
<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> 标签属性:
-
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 全局配置文件中的标签顺序并不是任意的。
全局配置文件中可以不存在某些标签,但是存在的标签必须按照以下的先后顺序进行书写:
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 中,允许增删改查直接定义以下类型的返回值:Integer、Long、Boolean、void。
对于 Boolean 类型的返回值来说,如果执行增加、删除、修改等操作时影响了一行以上的数据,那么就会返回 true。
在 Mybatis 中进行 CRUD 时,需要使用对应的标签,比如:<insert>、<delete>、<update> 和 <select>,在这些标签中都有一个名为 parameterType 的属性,这个属性是老式风格的参数映射,已被废弃,但是也可以使用,使用时指定参数类型的全类名或别名即可。既然这个属性已被废弃,因此 我们在编写时也可以不写这个属性。
在 SQL 映射文件中写 SQL 时,请不要在末尾写上分号!
2.3 插入获取主键值
对于支持主键自增的数据库
例如,MySQL 数据库支持主键自增,MyBatis 可以利用 statement.getGeneratedKeys() 获取自增主键值。
那么应该怎么做呢?
我们只需要在 <insert> 标签中添加属性:useGeneratedKeys 和 keyProperty。
其中,需要将 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> 的标签,见名识意,这个标签就是用于「查询主键」的。在这个标签中,有三个属性:
- 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 语句执行后再执行),则有:
<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:
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 映射文件中获取参数的方式。
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 时:
- 对于 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 {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> 标签进行数据查询时,通常需要设置这个标签的两个属性:
- 一个属性是 id,值是 Mapper 接口中的方法名
- 另一个属性是 resultType,值是查询结果的类型,即:接口中抽象方法返回值类型。不能和 resultMap 同时使用
对于 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> 对场景一(查询用户的同时查询用户所在的部门)进行 分步查询 :
- 先按照用户 id 查询用户信息
- 再根据查询出的用户信息中的 did 值去部门表查出部门信息
- 最后将部门信息设置到员工信息中
创建一个新的 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:
- 如果查询结果是女生,就把部门信息查询出来,否则不查询
- 如果查询结果是男生,把 username 这一列的值赋值给 email 属性
<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 接口的抽象方法如下:
// 携带了那个字段查询条件就带上这个字段的值
public List<User> getUsersByConditionIf(User user);
SQL 映射文件有以下几种编写方式:
- 使用
<if>标签 - 使用
<if>和<where>标签
<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 后面加一个 1=1 或 true,以后的条件都是 and xxxx 的格式
- MyBatis 推荐使用
<where>标签来将所有的查询条件包括在内,然后 MyBatis 就会将<where>标签中拼装的 SQL 中多出来的and或者or去掉
<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,还有一些用于分支选择的关键字 switch、case 和 default,在 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 关键词。
switch 和 case 组成一个条件选择语句,找到相同的 case 值做为入口,执行后面的程序;若所有的 case 都不满足,则找 default 入口;若未找到则退出整个 switch 。所以 default 只是一个备用的入口,有没有都无所谓。
不写 default 是建立在自己书写的条件已经包含了所有情况下,为了程序逻辑的完整性,建议写上 default。
PS:在阿里巴巴的代码规范中,switch 和 case 后强制要求书写 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:代表整个参数体
- 单个参数:_parameter 就是这个参数
- 多个参数:参数会被封装成一个 map,_parameter 就是代表这个 map
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:缓存的回收机制
-
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 中导入相关依赖:
<!-- 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 初始化
执行流程

MappedStatement:一个 MappedStatement 就代表 SQL 映射文件中一个增删改查的详细信息。
Configuration 对象封装了所有配置文件的详细信息。
总结: 把配置文件的信息解析并保存在 Configuration 对象中,返回包含了 Configuration 的 DefaultSqlSessionFactory 对象。
6.3 openSession 获取 SqlSession 对象
执行流程

总结: 返回 SqlSession 的实现类 DefaultSqlSession 对象,它里面包含了 Executor 和 Configuration,四大对象中的 Executor 会在这一步被创建。
SqlSession 和 JDBC 中的 Connection 一样,是 非线程安全 的,不能将 SqlSession 写成一个成员变量,SqlSession 代表和数据库的一次对话,每次使用都应该去重新获取,用完后必须关闭。
6.4 getMapper 获取接口代理对象
执行流程:

总结: getMapper 使用 MapperProxyFactory 创建一个 MapperProxy 的代理对象,这个代理对象包含了 DefaultSqlSession(Executor)对象。
6.5 查询实现
执行流程

查询流程总结

6.6 原理总结
1、根据配置文件(全局配置文件、SQL 映射文件)初始化 Configuration 对象
2、创建一个 DefaultSqlSession 对象,里面包含 Configuration 以及 Executor(根据全局配置文件中的 defaultExecutorType 创建出对应的 Executor)
3、DefaultSqlSession.getMapper():拿到 Mapper 接口对应的 MapperProxy
4、MapperProxy 中有 DefaultSqlSession
5、执行增删改查方法:
-
调用 DefaultSqlSession 的增删改查( 调用 Executor 的增删改查)
-
创建一个 StatementHandler 对象(同时也会创建出 ParameterHandler 和 ResultSetHandler )
-
调用 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
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 条数据):
- 非批量:(预编译 sql 次数 = 设置参数次数 = 执行次数) ==> 10000 次
- 批量:预编译一次 ==> 设置参数 10000 次 ==> 执行 1 次
实现方式:
//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 开始的索引。