MyBatis XML 映射文件详解

适合对象:已经看懂 mybatis-config.xml,想继续理解 XML 中的 SQL 如何对应 Java 方法的同学。

官方参考:

1. XML 映射文件负责什么?

MyBatis 的 XML 映射文件,也常叫 Mapper XML,主要负责写 SQL,并把 SQL 和 Java 调用方式对应起来。

常见有两种调用思路:

调用思路 说明
字符串 SQL 标识调用 通过 session.selectOne("命名空间.SQL编号", 参数) 调用
Mapper 接口代理调用 通过 session.getMapper(接口.class) 获取代理对象,再调用接口方法

入门阶段可以先看懂这句话:

XML 的 namespace 加 SQL 标签的 id,共同决定一条 SQL 的唯一标识。

2. XML 映射文件的基本结构

Mapper XML 通常这样开头:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.bytepro.mapper.EmployeeMapper">
...
</mapper>

其中:

内容 含义
DOCTYPE mapper 使用 MyBatis Mapper XML 的 DTD 规则
<mapper> 一个 SQL 映射文件的根标签
namespace 命名空间,用来给这一组 SQL 做唯一定位

namespace 很重要。它既可以是一个普通名称,也可以是 Mapper 接口的全类名。使用接口代理方式时,通常写接口全类名。

3. 字符串 SQL 标识调用

先看一种最直观的写法:

1
2
3
4
5
6
<mapper namespace="EmployeeMapper">
<select id="selectEmployee" resultType="com.bytepro.entity.Employee">
select * from employee where id = #{id}
</select>

</mapper>

Java 中可以这样调用:

1
Employee employee = session.selectOne("EmployeeMapper.selectEmployee", 1);

匹配关系如下:

1
2
3
4
EmployeeMapper.selectEmployee
│ │
│ └── select 标签的 id
└── mapper 标签的 namespace

图示:

1
2
3
4
5
6
flowchart LR
A["session.selectOne"] --> B["EmployeeMapper.selectEmployee"]
B --> C["namespace=EmployeeMapper"]
B --> D["id=selectEmployee"]
C --> E["select * from employee where id = #{id}"]
D --> E

这种方式容易理解,但字符串写错时,编译器不一定能发现。所以实际开发中更推荐 Mapper 接口代理方式。

4. Mapper 接口代理调用

先定义 Mapper 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface EmployeeMapper {
Employee selectEmployee(Integer id);

Long insertEmployee(Employee employee);

Long updateEmployee(Employee employee);

Long deleteEmployee(Integer id);

Employee getEmployeeByMap(Map<String, Object> map);

Employee getEmployeeByIdAndName(@Param("id") Integer id, @Param("name") String name);
}

XML 中让 namespace 对应接口全类名:

1
2
3
4
5
6
<mapper namespace="com.bytepro.mapper.EmployeeMapper">
<select id="selectEmployee" resultType="emp">
select * from employee where id = #{id}
</select>

</mapper>

Java 中这样调用:

1
2
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = mapper.selectEmployee(1);

三者之间的对应关系是:

1
2
3
4
5
6
7
8
9
flowchart TD
A["接口全类名<br/>com.bytepro.mapper.EmployeeMapper"] --> B["XML namespace"]
C["接口方法名<br/>selectEmployee"] --> D["XML SQL id"]
E["方法参数<br/>Integer id"] --> F["SQL 参数<br/>#{id}"]
G["方法返回值<br/>Employee"] --> H["resultType=emp"]
B --> I["MyBatis 代理对象"]
D --> I
F --> I
H --> I

记忆方式很简单:

  • 接口全类名对应 namespace
  • 接口方法名对应 SQL 的 id
  • 方法参数对应 SQL 里的 #{}
  • 方法返回值对应 XML 里的 resultType 或 resultMap

5. select:查询数据

查询单个员工可以这样写:

1
2
3
<select id="selectEmployee" resultType="emp">
select * from employee where id = #{id}
</select>

对应接口方法:

1
Employee selectEmployee(Integer id);

几个关键点:

XML 属性或语法 含义
select 表示这是一条查询语句
id="selectEmployee" 对应接口里的 selectEmployee 方法
resultType="emp" 查询结果封装成 Employee 对象
#{id} 接收 Java 方法传入的参数

调用示例:

1
Employee employee = mapper.selectEmployee(1);

最终执行时,可以理解为查询:

1
select * from employee where id = 1

实际执行时 MyBatis 会使用预编译参数,不是简单字符串拼接。这也是 #{} 比直接拼接 SQL 更安全的原因。

6. insert:新增数据并回填自增主键

新增员工可以这样写:

1
2
3
4
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id">
insert into employee(name,gender,age,home_address)
values(#{name},#{gender},#{age},#{homeAddress})
</insert>

对应接口方法:

1
Long insertEmployee(Employee employee);

调用示例:

1
2
3
4
5
6
7
8
Employee employee = new Employee();
employee.setName("李四");
employee.setAge(20);
employee.setGender('1');
employee.setHomeAddress("安徽");

Long result = mapper.insertEmployee(employee);
session.commit();

这段 XML 中最重要的是:

1
useGeneratedKeys="true" keyProperty="id"

它们用于处理 MySQL 自增主键:

属性 含义
useGeneratedKeys="true" 使用 JDBC 获取数据库生成的主键
keyProperty="id" 把生成的主键值设置回 Employee 对象的 id 属性

也就是说,插入前 employee.getId() 可能是 null;插入成功后,MyBatis 可以把数据库生成的新 id 放回对象中。

#{name}、#{gender}、#{age}、#{homeAddress} 都来自传入的 Employee 对象属性。

7. selectKey:序列主键写法示例

有些数据库常用序列生成主键,可以使用 selectKey。例如:

1
2
3
4
5
6
7
8
<insert id="insertEmployee" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
select EMPLOYEE_SEQ.nextval from dual
</selectKey>

insert into employee(id,name,gender,age,home_address)
values(#{id},#{name},#{gender},#{age},#{homeAddress})
</insert>

这段代码表示:

  1. 先执行 selectKey 中的 SQL,拿到序列生成的主键。
  2. 把主键设置到 Employee 对象的 id 属性。
  3. 再执行真正的 insert 语句。

如果使用 MySQL 自增主键,通常重点看 useGeneratedKeys="true" 这种写法;如果使用序列主键,可以了解 selectKey。

8. update:修改数据

修改员工可以这样写:

1
2
3
4
<update id="updateEmployee">
update employee set name=#{name},gender=#{gender},age=#{age},home_address=#{homeAddress}
where id=#{id}
</update>

对应接口方法:

1
Long updateEmployee(Employee employee);

调用示例:

1
2
3
4
5
6
7
8
9
Employee employee = new Employee();
employee.setId(7);
employee.setName("李四new");
employee.setAge(29);
employee.setGender('1');
employee.setHomeAddress("安徽");

Long result = mapper.updateEmployee(employee);
session.commit();

传入的是一个完整对象,所以 XML 中可以直接通过属性名取值:

1
2
3
4
5
#{id}
#{name}
#{gender}
#{age}
#{homeAddress}

修改操作会改变数据库数据,所以执行后需要提交事务。

9. delete:删除数据

删除员工可以这样写:

1
2
3
<delete id="deleteEmployee">
delete from employee where id = #{id}
</delete>

对应接口方法:

1
Long deleteEmployee(Integer id);

调用示例:

1
2
Long result = mapper.deleteEmployee(7);
session.commit();

删除和查询单个员工都可以传入简单参数 id。区别是:

  • select 返回查询结果
  • delete 返回影响行数,并且需要提交事务

10. Map 参数:用 key 对应 SQL 参数名

如果一个查询条件由多个值组成,可以传入 Map:

1
Employee getEmployeeByMap(Map<String, Object> map);

XML:

1
2
3
<select id="getEmployeeByMap" resultType="com.bytepro.entity.Employee">
select * from employee where id = #{id} and name = #{name}
</select>

调用示例:

1
2
3
4
5
Map<String, Object> map = new HashMap<>();
map.put("id", 6);
map.put("name", "李四new");

Employee employee = mapper.getEmployeeByMap(map);

当参数是 Map 时:

Map 的 key XML 中的写法
id #{id}
name #{name}

也就是说,#{id} 会去 Map 里找 key 为 "id" 的值,#{name} 会去 Map 里找 key 为 "name" 的值。

11. 多参数:用 @Param 起名字

多个普通参数建议使用 @Param 明确命名:

1
Employee getEmployeeByIdAndName(@Param("id") Integer id, @Param("name") String name);

XML:

1
2
3
<select id="getEmployeeByIdAndName" resultType="com.bytepro.entity.Employee">
select * from employee where id = #{id} and name = #{name}
</select>

@Param 和 XML 参数的关系如下:

Java 参数 注解 XML 参数
Integer id @Param("id") #{id}
String name @Param("name") #{name}

调用示例:

1
Employee employee = mapper.getEmployeeByIdAndName(6, "李四new");

如果没有清晰的参数名,XML 里写 #{id}、#{name} 时就不够直观。@Param 的作用就是把 Java 参数名和 SQL 参数名明确绑定起来。

12. resultType:查询结果怎么变成对象

XML 中可以写完整类名:

1
resultType="com.bytepro.entity.Employee"

也可以写类型别名:

1
resultType="emp"

如果 Employee 上有注解:

1
2
3
4
5
6
7
8
@Alias("emp")
public class Employee {
private Integer id;
private String name;
private Character gender;
private Integer age;
private String homeAddress;
}

那么 emp 就表示 Employee。

再配合下划线转驼峰配置:

1
<setting name="mapUnderscoreToCamelCase" value="true"/>

数据库列 home_address 就可以映射到 Java 属性 homeAddress。

简单查询用 resultType 就够了。复杂对象关系映射,比如一对多、多表关联结果封装,通常再学习 resultMap。

13. CRUD 映射总表

Java 方法 XML 标签 SQL id 参数来源 是否需要 commit
selectEmployee(Integer id) select selectEmployee 简单参数 id 否
insertEmployee(Employee employee) insert insertEmployee Employee 对象属性 是
updateEmployee(Employee employee) update updateEmployee Employee 对象属性 是
deleteEmployee(Integer id) delete deleteEmployee 简单参数 id 是
getEmployeeByMap(Map<String, Object> map) select getEmployeeByMap Map 的 key 否
getEmployeeByIdAndName(Integer id, String name) select getEmployeeByIdAndName @Param 指定参数名 否

14. 这一篇先记住什么?

  • XML 的 namespace 用来定位一组 SQL。
  • Mapper 接口方式中,namespace 通常写接口全类名。
  • XML 中 SQL 标签的 id 要和接口方法名一致。
  • #{} 是参数占位符,用来接收 Java 传进来的值。
  • resultType 决定查询结果封装成什么对象。
  • 对象参数可以直接通过属性名取值,例如 #{homeAddress}。
  • 多个普通参数建议用 @Param 命名。
  • 新增、修改、删除后要记得 session.commit()。