MyBatis 入门教程之五

大纲

前言

本文的所有案例代码,若没有特别说明,默认都基于以下的表结构和 JavaBean 类编写,可以直接从 GitHub 下载对应章节 mybatis-lesson-15

1
2
3
4
5
6
7
8
CREATE TABLE `t_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` varchar(255) DEFAULT NULL,
`gender` char(1) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`dept_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee {

private Long id;
private String lastName;
private String gender;
private String email;
private Department department;

......

@Override
public String toString() {
return "Employee [id=" + id + ", lastName=" + lastName + ", gender=" + gender + ", email=" + email + ", department=" + department + "]";
}
}

动态 SQL

  • 动态 SQL 是 MyBatis 强大特性之一,极大的简化拼装 SQL 的操作。
  • 动态 SQL 标签和使用 JSTL 或其他类似基于 XML 的文本处理器相似。
  • MyBatis 采用功能强大的基于 OGNL 的表达式来简化操作,并提供了 iftrim (where, set)foreachchoose (when, otherwise) 动态 SQL 标签。

if 标签

1
2
3
4
5
public interface EmployeeMapper {

public List<Employee> getEmpsByConditionIf(Employee employee);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionIf" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null and lastName.trim() != ''">
and last_name = #{lastName}
</if>
<if test="email != null and email.trim() != ''">
and email = #{email}
</if>
<!-- OGNL 会自动对字符串与数字进行转换 -->
<if test="gender == 1 or gender == 0">
and gender = #{gender}
</if>
</select>

</mapper>

特别注意

上述的写法,如果传递进来的 JavaBean 参数的 id 属性刚好为 null 时,MyBatis 执行后会提示 SQL 存在语法错误(SQL 语句中有多余的 and 关键字),如下所示:

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'and last_name = 'Jim'

where 标签

解决上述 if 标签带来的 SQL 拼接出错问题,一般有以下两种方法:

  • 第一种方法:在 where 关键字后面加上 1 = 1,同时指定 if 标签里的内容都以 and 或者 or 关键字开头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionIf" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where 1 = 1
<if test="id != null">
and id = #{id}
</if>
<if test="lastName != null and lastName.trim() != ''">
and last_name = #{lastName}
</if>
<if test="email != null and email.trim() != ''">
and email = #{email}
</if>
<if test="gender == 1 or gender == 0">
and gender = #{gender}
</if>
</select>

</mapper>
  • 第二种方法:使用 where 标签替代 where 关键字

提示

  1. where 标签用于封装查询条件,一般是和 if 标签一起使用
  2. where 标签只会在子标签返回任何内容的情况下,才自动插入 where 关键字
  3. 使用 where 标签动态拼接 SQL 语句时,会自动将查询条件头部多出来的 and 或者 or 关键字去掉
  4. 特别注意,where 标签不会自动将查询条件尾部多出来的 and 或者 or 关键字去掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionIf" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
<!-- 利用 where 标签,将查询条件头部多出来的 and 或者 or 关键字去掉 -->
<where>
<if test="id != null">
id = #{id}
</if>
<if test="lastName != null and lastName.trim() != ''">
and last_name = #{lastName}
</if>
<if test="email != null and email.trim() != ''">
and email = #{email}
</if>
<if test="gender == 1 or gender == 0">
and gender = #{gender}
</if>
</where>
</select>

</mapper>

trim 标签

除了上述的两种方法之外,还可以使用 trim 标签,以此解决 where 标签不能自动将查询条件尾部多出来的 and 或者 or 关键字去掉的问题。trim 标签用于自定义字符串内容的截取,其标签体中的内容是 SQL 字符串拼接后的结果。trim 标签拥有以下属性:

  • prefix:给拼接后的整个字符串加一个前缀
  • prefixOverrides:去掉拼接后整个字符串头部多余的字符
  • suffix:给拼接后的整个字符串加一个后缀
  • suffixOverrides:去掉拼接后整个字符串尾部多余的字符
1
2
3
4
5
public interface EmployeeMapper {

public List<Employee> getEmpsByConditionTrim(Employee employee);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionTrim" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
<!-- 利用 trim 标签,往头部插入 where 关键字,并去掉尾部的 and 或者 or 关键字 -->
<trim prefix="where" suffixOverrides="and | or">
<if test="id != null">
id = #{id} and
</if>
<if test="lastName != null and lastName.trim() != ''">
last_name = #{lastName} and
</if>
<if test="email != null and email.trim() != ''">
email = #{email} and
</if>
<if test="gender == 1 or gender == 0">
gender = #{gender}
</if>
</trim>
</select>

</mapper>

choose 标签

choose 标签是按顺序判断其内部 when 标签中的 test 条件是否成立。它和 if - else 不太相同,choose 标签类似于 Java 中的 switch 语句用法,只要有判断条件成立,其它判断将得不到执行,如果所有条件都不成立则会执行 otherwise 标签中的内容。

1
2
3
4
5
public interface EmployeeMapper {

public List<Employee> getEmpsByConditionChoose(Employee employee);

}
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
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionChoose" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
<where>
<!-- 下述的条件判断,只有一个会被执行 -->
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="lastName != null and lastName.trim() != ''">
last_name = #{lastName}
</when>
<when test="email != null and email.trim() != ''">
email = #{email}
</when>
<otherwise>
gender = 1
</otherwise>
</choose>
</where>
</select>

</mapper>

set 标签

1
2
3
4
5
public interface EmployeeMapper {

public void updateEmp(Employee employee);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<update id="updateEmp" parameterType="com.clay.mybatis.bean.Employee">
update t_employee
set
<if test="lastName != null and lastName.trim() != ''">
last_name = #{lastName},
</if>
<if test="email != null and email.trim() != ''">
email = #{email},
</if>
<if test="gender == 1 or gender == 0">
gender = #{gender}
</if>
where id = #{id}
</update>

</mapper>

特别注意

上述 SQL 更新语句的写法,如果传递进来的 JavaBean 参数的 last_name 不为 null,且 gender 属性刚好为 null 时,MyBatis 执行后会提示 SQL 存在语法错误(SQL 语句中存在多余的逗号),如下所示:

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'where id = 1'

提示

  • set 标签用于 SQL 更新语句中,一般是和 if 标签一起使用
  • set 标签可以解析为 set 关键字,并去除 SQL 更新语句中尾部多余的逗号

为了解决上面 SQL 拼接出错的问题,可以使用 set 标签来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<update id="updateEmp" parameterType="com.clay.mybatis.bean.Employee">
update t_employee
<!-- 利用 set 标签,去掉更新语句中尾部多余的逗号 -->
<set>
<if test="lastName != null and lastName.trim() != ''">
last_name = #{lastName},
</if>
<if test="email != null and email.trim() != ''">
email = #{email},
</if>
<if test="gender == 1 or gender == 0">
gender = #{gender}
</if>
</set>
where id = #{id}
</update>

</mapper>

或者也可以使用 trim 标签来解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<update id="updateEmp" parameterType="com.clay.mybatis.bean.Employee">
update t_employee
<!-- 利用 trim 标签,往头部插入 set 关键字,并去掉尾部多余的逗号 -->
<trim prefix="set" suffixOverrides=",">
<if test="lastName != null and lastName.trim() != ''">
last_name = #{lastName},
</if>
<if test="email != null and email.trim() != ''">
email = #{email},
</if>
<if test="gender == 1 or gender == 0">
gender = #{gender}
</if>
</trim>
where id = #{id}
</update>

</mapper>

foreach 标签

属性说明

foreach 标签可用于遍历集合,并拥有以下属性:

属性必填描述
collection表示迭代集合的名称
item表示本次迭代获取的元素,若 collection 为 List、Set 或者数组,则表示其中的元素;若 collection 为 Map,则代表 Map 的 value
open表示该语句以什么字符开始,最常用的是左括弧 ( 字符,Mybatis 会将该字符拼接到整体的 SQL 语句之前,并且只拼接一次
close表示该语句以什么字符结束,最常用的是右括弧 ),MyBatis 会将该字符拼接到整体的 SQL 语句之后,并且只拼接一次
separator MyBatis 会在每次迭代后给 SQL 语句拼接上 separator 属性指定的字符(例如逗号 ,
index在 List、Set 和数组中,index 表示当前迭代的位置(索引),在 Map 中,index 代表的是 Map 的 key

在使用 foreach 标签的时候,最关键也是最容易出错的就是 collection 属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下 3 种情况:

  • 如果传入的是单参数且参数类型是一个 List 的时候,collection 的属性值默认是 list
  • 如果传入的是单参数且参数类型是一个数组的时候,collection 的属性值默认是 array
  • 如果传入的参数有多个的时候,就需要把它们封装成一个 Map 了,当然单参数也可以封装成 Map。实际上在传入参数的时候,MyBatis 的底层代码也是会自动把参数封装成一个 Map 的,Map 的 key 就是参数名,所以这个时候 collection 属性值就是传入的 List 或 Array 对象在自己封装的 Map 里面的 key

提示

在 MyBatis 中,使用 List 传递参数的详细教程可看这里

数据批量查询案例

1
2
3
4
5
public interface EmployeeMapper {

public List<Employee> getEmpsByConditionByForeach(List<Long> ids);

}
1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionByForeach" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where id in
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

</mapper>

或者使用 @Param 注解指定参数名称:

1
2
3
4
5
public interface EmployeeMapper {

public List<Employee> getEmpsByConditionByForeach(@Param("ids") List<Long> ids);

}
1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpsByConditionByForeach" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

</mapper>

MySQL 数据库

数据批量插入案例一
1
2
3
4
5
public interface EmployeeMapper {

public void addEmps(@Param("emps") List<Employee> emps);

}
1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- MySQL 第一种数据批量插入方式 -->
<insert id="addEmps">
insert into t_employee(last_name, gender, email)
values
<foreach collection="emps" item="emp" separator=",">
(#{emp.lastName}, #{emp.gender}, #{emp.email})
</foreach>
</insert>

</mapper>
数据批量插入案例二
1
2
3
4
5
public interface EmployeeMapper {

public void addEmps(@Param("emps") List<Employee> emps);

}
1
2
3
4
5
6
7
8
9
10
11
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- MySQL 第二种数据批量插入方式 -->
<insert id="addEmps">
<foreach collection="emps" item="emp">
insert into t_employee(last_name, gender, email)
values(#{emp.lastName}, #{emp.gender}, #{emp.email});
</foreach>
</insert>

</mapper>

特别注意

  1. 默认情况下 MyBatis 不允许多条语句同时执行,因此需要在连接 MySQL 的 url 中增加 allowMultiQueries=true 配置内容,否则上述 SQL 执行时会出错。
  2. 如果批量插入大量的数据,上述的批量插入方式可能会执行失败,这是由于 MySQL 对 SQL 语句的长度有限制导致的。

Oracle 数据库

Oracle 不支持使用 insert ... values(),()... 的方式批量插入数据,但支持使用 begin ... end; 或者中间表的方式来实现数据的批量插入。

数据批量插入案例一
1
2
3
4
5
public interface EmployeeMapper {

public void addEmps(@Param("emps") List<Employee> emps);

}
1
2
3
4
5
6
7
8
9
10
11
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- Oracle 第一种数据批量插入方式 -->
<insert id="addEmps" databaseId="oracle">
<foreach collection="emps" item="emp" open="begin" close="end;">
insert into t_employee(id, last_name, gender, email)
values(employees_seq.nextval, #{emp.lastName}, #{emp.gender}, #{emp.email});
</foreach>
</insert>

</mapper>

最终会发出类似下面的 SQL 语句到 Oracle 数据库:

1
2
3
4
5
begin
insert into t_employee(id, last_name, gender, email) values (1, 'Jim', '1', 'jim@gmail.com');
insert into t_employee(id, last_name, gender, email) values (2, 'Tom', '1', 'tom@gmail.com');
insert into t_employee(id, last_name, gender, email) values (3, 'Peter', '1', 'peter@gmail.com');
end;
数据批量插入案例二
1
2
3
4
5
public interface EmployeeMapper {

public void addEmps(@Param("emps") List<Employee> emps);

}
1
2
3
4
5
6
7
8
9
10
11
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- Oracle 第二种数据批量插入方式 -->
<insert id="addEmps" databaseId="oracle">
insert into t_employee(id, last_name, gender, email)
<foreach collection="emps" item="emp" separator="union" open="select employees_seq.nextval, lastName, gender, email from (" close=")">
select #{emp.lastName} lastName, #{emp.gender} gender, #{emp.email} email from dual
</foreach>
</insert>

</mapper>

最终会发出类似下面的 SQL 语句到 Oracle 数据库:

1
2
3
4
5
6
7
8
insert into t_employee(id, last_name, gender, email)
select employees_seq.nextval, lastName, gender, email from (
select 'Jim' lastName, '1' gender, 'jim@gmail.com' email from dual
union
select 'Tom' lastName, '1' gender, 'tom@gmail.com' email from dual
union
select 'Peter' lastName, '1' gender, 'peter@gmail.com' email from dual
)

script 标签

若要在带注解的 DAO 接口中使用动态 SQL 语句,则可以使用 script 标签来实现,例如:

1
2
3
4
5
6
7
8
9
10
11
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
public void updateAuthorValues(Author author);

多数据库支持

若在 MyBatis 的全局配置文件中配置了 databaseIdProvider 标签(数据库厂商标识),那么可以在动态 SQL 中使用名为 _databaseId 的变量(MyBatis 内置参数)来为不同的数据库构建特定的语句。_databaseId 内置参数的详细使用教程,可以查看这里

1
2
3
4
5
6
7
8
9
<configuration>
<!-- 数据库厂商标识 -->
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver" />
<property name="DB2" value="db2" />
<property name="MySQL" value="mysql" />
<property name="Oracle" value="oracle" />
</databaseIdProvider>
</configuration>
1
2
3
4
5
6
7
8
9
10
11
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>

插入脚本语言

MyBatis 从 3.2 版本开始支持插入脚本语言,这允许开发者插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。

  • 通过实现以下接口来插入一种脚本语言
1
2
3
4
5
6
7
public interface LanguageDriver {

ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}
  • 实现自定义语言驱动后,就可以在 mybatis-config.xml 文件中将它设置为默认语言
1
2
3
4
5
6
<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
  • 或者使用 lang 属性为特定的 SQL 语句指定语言
1
2
3
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>
  • 或者在 Mapper 接口上添加 @Lang 注解
1
2
3
4
5
6
7
public interface Mapper {

@Lang(MyLanguageDriver.class)
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();

}

提示

MyBatis 支持使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。

前面介绍的所有 xml 标签都由默认 MyBatis 语言提供,而它由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)所提供。