MyBatis 中有哪些配置文件
在 MyBatis 体系中,Mapper XML 和 MyBatis
Config XML 是两类核心配置文件,但在 Spring Boot
开发中,MyBatis Config XML 几乎被
配置化属性也就是properties或者yaml配置文件
代替了
明确一下两类配置文件的分工
| 配置文件类型 | 核心定位 | 核心作用 | 类比 |
|---|---|---|---|
| Mapper XML | 业务 SQL 层配置 | 定义具体的 SQL 语句、参数映射、结果映射 | 业务代码(个性化) |
| MyBatis Config XML | 全局核心配置层 | 配置 MyBatis 全局规则(数据源、缓存、插件等) | 框架配置(通用规则) |
Mapper XML
Mapper XML 是 MyBatis 业务 SQL 的载体,它是将 Java 你写的 Mapper 中的接口方法与 SQL 语句绑定的,做手写 SQL + 参数 / 结果映射的,一个 Mapper XML 中的逻辑对应一个 Mapper 接口类
在 Spring Boot 中,Mapper XML 的编写逻辑完全不变,仅需在
application.yml 中指定文件位置
1 | mybatis: |
MyBatis Config XML
在非 Spring Boot 的原生 MyBatis
项目中,mybatis-config.xml 是 全局配置入口
配置文件,负责定义 MyBatis 运行的所有全局规则,例如:
1 |
|
Spring Boot 的核心思想是 约定大于配置+自动配置,所以,它将
mybatis-config.xml 中的所有配置项,拆解为
application.yml/application.properties
中的属性配置,这样无需再写 Config XML
<environments>就给spring.datasource.*,让 Spring Boot 统一管理数据源<mappers>注册 Mapper,到 Spring Boot 的配置文件就是mybatis.mapper-locations: classpath:mapper/*.xml<typeAliases>就是mybatis.type-aliases-package: hbnu.project.mybatisdemo.entity- 而 Spring Boot 的自动扫描,
@MapperScan替代<mappers>标签,自动扫描 Mapper 接口,无需手动注册。
基本上比较经常使用的就是这些,所以说,这个配置文件如何单独编写就不说了
Spring Boot 中是否完全不需要
mybatis-config.xml呢?
非必要不使用
如果非要用
mybatis-config.xml,只需在application.yml中指定文件路径即可,示例
1
2
3 mybatis:
config-location: classpath:mybatis-config.xml # 指定全局配置文件路径
mapper-locations: classpath:mapper/*.xml # 仍需指定Mapper XML路径此时
mybatis-config.xml中可保留<plugins>/<typeHandlers>等复杂配置,而数据源、别名等仍可复用application.yml的配置,优先级是mybatis-config.xml>application.yml。
如何编写Mapper XML
MyBatis 的持久层代码如何写
MyBatis的XML方式需要两部分:
- Mapper 接口:定义方法,告诉 MyBatis 你要做什么操作。
- XML 映射文件:写具体的SQL语句,告诉 MyBatis 怎么操作。
然后要告诉 MyBatis 启动后去哪找
Mapper,添加@Mapper注解或者启动类上放@MapperScan("mapper包名")
@Mapper注解这是一个 MyBatis 的 Mapper 接口,类似@Repository,需要在容器中为它创建代理对象。如果不加这个注解,Spring 就不会扫描到这个接口,也就不会生成代理对象注册到 Spring 容器中,你后面就无法通过@Autowired注入它。
最后,要在 Spring 的配置文件中配置 MyBatis 的 XML 文件路径,找到了 Mapper 要去找对应的 XML 来了解要做什么操作
Mpper XML文件的结构
Mapper XML 是 MyBatis 中业务 SQL 与 Java Mapper 接口绑定的核心载体,其结构遵循严格的 DTD 约束,且每个标签都有明确的职责边界。
一个规范的 Mapper XML 遵循 固定头部 + 必要标签 的结构,所有内容嵌套在
<mapper> 根标签内,完整骨架如下:
1 |
|
固定头部
XML 声明 + DTD 约束,这是 Mapper XML 的 语法校验基础
1
2
3
4- XML 声明:
version="1.0"指定 XML 版本,encoding="UTF-8"强制文件编码 - DTD 约束:MyBatis 官方定义的标签规则,IDE
会根据这个约束提示标签和属性,且 MyBatis
解析时会校验语法(比如
<resultMap>必须有id属性)。
- XML 声明:
根标签:
<mapper><mapper>是整个文件的根标签,唯一作用是 将 XML 与 Mapper 接口绑定,核心属性是namespace。1
<mapper namespace="hbnu.project.mybatisdemo.mapper.UserMapper">
namespace必须等于 Mapper 接口的全类名- 一个 Mapper XML 对应一个 Mapper 接口,
namespace是唯一标识,不能重复。
基础复用标签
<sql>用于提取重复的 SQL 片段,通过<include>引用1
2
3
4
5
6
7
8
9<!-- 定义SQL片段:id是唯一标识 -->
<sql id="UserColumns">id, name, email, age</sql>
<!-- 引用片段:refid对应sql的id -->
<select id="findUserById" resultMap="UserResultMap">
SELECT <include refid="UserColumns"/>
FROM users
WHERE id = #{id}
</select>当然上面这个例子只是一个简单的字符替换,
<sql>也可以使用带参数的 SQL 片段1
2
3
4
5
6
7
8
9<sql id="TablePrefix">
${prefix}_users <!-- 动态表名,仅示例,慎用${} -->
</sql>
<select id="findUserByTable" resultMap="UserResultMap">
SELECT <include refid="UserColumns"/>
FROM <include refid="TablePrefix"/>
WHERE id = #{id}
</select><sql>的id在当前 XML 中唯一即可<include>可嵌套,也可通过property传参,当然这就是比较复杂的写法了,能别这么写还是别这么写,太难看
核心映射标签:
<resultMap><resultMap>是 MyBatis 解决 数据库列名与 Java 实体属性名不一致 的标签,也是多表关联查询的关键,算是最重要的一个标签了基础结构
1
2
3
4
5
6
7
8<resultMap id="UserResultMap" type="hbnu.project.mybatisdemo.entity.User">
<!-- 主键映射:必须用<id>,这是MyBatis优化缓存/嵌套查询的关键 -->
<id column="id" property="id" jdbcType="INTEGER"/>
<!-- 普通字段映射:<result> -->
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
</resultMap>SQL 操作标签
这是 Mapper XML 的最终目的,编写具体的数据库操作 SQL,所有标签都遵循「
id绑定接口方法」的核心规则。例如新增标签
<insert>1
2
3
4
5<insert id="insertUser" parameterType="User"
useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO users (name, email, age)
VALUES (#{name}, #{email}, #{age})
</insert>其中的属性:
useGeneratedKeys="true":开启自增主键回填(MySQL/Oracle 自增主键);keyProperty="id":将生成的主键赋值到入参 User 对象的id属性;keyColumn="id":数据库表的主键列名(与 keyProperty 对应,可选);
可选标签:
<cache><cache>用于开启当前 Mapper 的二级缓存,放在<mapper>根标签下:1
2
3
4
5<cache
eviction="LRU" <!-- 缓存淘汰策略:LRU(最近最少使用)/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 自动刷新时间(毫秒),60秒 -->
size="512" <!-- 缓存最多存储512个对象 -->
readOnly="true"/> <!-- 只读缓存(性能高,避免修改缓存) -->二级缓存默认关闭,需手动开启;而且开二级缓存,实体类必须实现
Serializable接口,然后增删改操作会自动刷新缓存
<resultMap>标签详解
进一步认识<resultMap>标签
使用mybatis,有两个属性标签<resultType>、<resultMap>可以提供结果映射。
虽然<resultType>
属性在大部分情况下都够用,但是在一些特殊情况下无能为力,比如属性名和列名不一致,为一些连接的复杂语句编写映射代码。遇到这些情况,我们要使用<resultMap>标签
<resultMap>
的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
其中的属性如下
| 标签 | 属性 | 作用 |
|---|---|---|
<resultMap> |
id |
当前 resultMap 的唯一标识,后续 SQL
标签用resultMap="UserResultMap"引用 |
type |
映射的目标 Java 类,全类名 /
别名,别名需配置type-aliases-package |
|
<id> |
column |
数据库表的列名,大小写不敏感,与 SQL 查询的列名一致 |
property |
Java 实体类的属性名,大小写敏感,必须能与实体类
getter/setter对应上 |
|
jdbcType |
数据库字段类型,可选,用于类型转换,如
VARCHAR/INTEGER/DATE |
|
<result> |
同<id> |
普通字段映射,非主键 |
属性名和列名不一致是使用<resultMap>标签最常见的情况
例如
1
2
3
4
5public class User {
private int id;
private String username;
private String hashedPassword;
}1
2
3
4
5
6<select id="selectUsers" resultType="User">
select
user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>为了 解决 上述问题,我们只需要在
<resultMap>中做一下简单的对应的配置1
2
3
4
5<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>然后然后在引用它的语句中设置
<resultMap>属性就行了,去掉<reslutType>属性,用<resultMap>代替,二者只能选择其中的一个1
2
3
4
5<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
那么,多表关联映射如何写呢?包括一对一映射,多对多映射
编写多表关联映射
无论哪种关联类型,MyBatis 多表映射的本质是:
- 将多表查询的结果,通过
resultMap标签的层级结构,赋值到 Java 实体的 基本属性 + 关联属性 中。所以说通用步骤是
- 明确关联关系:先理清数据库表之间的关联字段
- 定义实体关联属性:在主实体中添加关联对象 / 集合属性
- 编写关联查询 SQL:根据需求选择联表还是分布
- 配置 resultMap:用对应标签映射关联字段到实体属性。
日常开发中多表关联主要分 3 类,resultMap
针对不同场景提供了专属标签:
| 关联类型 | 业务场景 | resultMap 核心标签 | 示例 |
|---|---|---|---|
| 一对一 | 用户 ↔︎ 用户详情(1:1) | <association> |
User 包含 UserInfo |
| 一对多 | 用户 ↔︎ 订单(1:N) | <collection> |
User 包含List<Order> |
| 多对多 | 用户 ↔︎ 角色(M:N) | <collection> |
User 包含
List<Role>(通过中间表) |
数据表和实体是这样的
1 | // 用户主实体 |
一对一关联映射
<association>一对一关联,例如 User ↔︎ UserInfo,使用
<association>标签,它表示 当前实体包含一个关联实体对象,支持两种实现方式:我先说其中的参数
属性 作用 property当前实体中关联对象的属性名(User 中的 userInfo)javaType关联对象的类型(全类名 / 别名,如 UserInfo)column数据库列名(联表查询时需注意列名冲突,用 AS起别名,如ui.id AS info_id)select关联查询的 Mapper 方法(全类名 + 方法名) fetchType加载策略: lazy(懒加载,用到时才查)/eager(立即加载),默认eager嵌套查询(分步查)
对于这样的一个方法
1
2// UserMapper.java
User findUserWithInfoById(Integer id);编写联表查询 SQL +
resultMap,整体步骤如下- 先编写联表查询 SQL,别忘了处理列名冲突,若主表和关联表有同名列,给关联表的列起别名
- 然后配置
resultMap,先映射主实体的基本属性,然后嵌套
<association>标签,映射关联实体属性 - 之后在
<select>标签中,通过resultMap属性指定上述配置的 resultMap 名称。
按这样步骤编写的例子:
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<!-- UserMapper.xml -->
<mapper namespace="hbnu.project.mybatisdemo.mapper.UserMapper">
<!-- 定义一对一关联的resultMap -->
<resultMap id="UserWithInfoResultMap" type="hbnu.project.mybatisdemo.entity.User">
<!-- 映射用户主表字段 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<!-- 一对一关联:UserInfo -->
<association property="userInfo" javaType="hbnu.project.mybatisdemo.entity.UserInfo">
<!-- 映射用户详情表字段(注意列名冲突,需别名) -->
<id column="info_id" property="id"/>
<result column="user_id" property="userId"/>
<result column="phone" property="phone"/>
<result column="address" property="address"/>
</association>
</resultMap>
<!-- 联表查询SQL(users LEFT JOIN user_info) -->
<select id="findUserWithInfoById" resultMap="UserWithInfoResultMap">
SELECT
u.id, u.name, u.email, u.age,
ui.id AS info_id, ui.user_id, ui.phone, ui.address
FROM users u
LEFT JOIN user_info ui ON u.id = ui.user_id
WHERE u.id = #{id}
</select>
</mapper>嵌套结果(联表查)。
先查主表,再根据主表结果查关联表,对于上面同样的 Mapper 接口
先编写
UserInfoMapper查询方法1
2
3
4
5
6<!-- UserInfoMapper.xml -->
<mapper namespace="hbnu.project.mybatisdemo.mapper.UserInfoMapper">
<select id="findUserInfoByUserId" parameterType="int" resultType="UserInfo">
SELECT id, user_id, phone, address FROM user_info WHERE user_id = #{userId}
</select>
</mapper>然后在
UserMapper中定义嵌套查询的resultMap- 先映射主实体的基本属性
- 嵌套
<association>标签,配置分步查询参数property:主实体中关联对象的属性名;javaType:关联实体类型;column:主表的关联字段(如id,将主表查询结果的id传给关联查询);select:关联查询的 Mapper 方法全路径(如xxx.UserInfoMapper.findUserInfoByUserId);- 可选
fetchType="lazy":开启懒加载
- 最后编写主表查询 SQL,仅查询主表字段,关联表数据通过
<association>的select分步查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- UserMapper.xml -->
<resultMap id="UserWithInfoNestedResultMap" type="User">
<!-- 主表字段映射 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<!-- 一对一嵌套查询:先查用户,再查用户详情 -->
<association
property="userInfo"
javaType="UserInfo"
column="id"
select="hbnu.project.mybatisdemo.mapper.UserInfoMapper.findUserInfoByUserId"/>
</resultMap>
<!-- 只查主表,关联表按需查询 -->
<select id="findUserWithInfoNestedById" resultMap="UserWithInfoNestedResultMap">
SELECT id, name, email, age FROM users WHERE id = #{id}
</select>
一对多关联映射
<collection>一对多关联,就例如
User ↔︎ List<Order>,使用<collection>标签,它表示 当前实体包含一个关联对象的集合,同样支持嵌套结果和嵌套查询。其实有些类似于一对一其中的参数如下
属性 作用 property当前实体中集合属性名(User 中的 orders)ofType集合中元素的类型(Order,区别于 javaType:javaType是集合类型如 List)column数据库列名(同样注意列名冲突,订单 id 起别名 order_id)嵌套结果(联表查询)
对于这样的一个 Mapper 接口方法
1
User findUserWithOrdersById(Integer id);
编写联表查询 +
resultMap,具体步骤如下- 编写联表查询 SQL,同样注意处理列名冲突
- 配置 resultMap
- 先映射主实体的基本属性,同一对一
- 然后嵌套
<collection>标签,映射关联集合:property:主实体中集合属性名(如 User 的orders);ofType:集合中元素的类型(如Order,注意不是List);- 在
<collection>内部,用<id>/<result>映射关联表字段(别名)到集合元素的属性。
- 绑定 SQL 与 resultMap
这样的步骤编写出的例子如下
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<!-- UserMapper.xml -->
<resultMap id="UserWithOrdersResultMap" type="User">
<!-- 主表字段映射 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<!-- 一对多关联:List<Order> -->
<collection
property="orders"
ofType="hbnu.project.mybatisdemo.entity.Order"> <!-- 集合中元素的类型 -->
<id column="order_id" property="id"/>
<result column="user_id" property="userId"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<!-- 联表查询:用户 + 订单 -->
<select id="findUserWithOrdersById" resultMap="UserWithOrdersResultMap">
SELECT
u.id, u.name, u.email, u.age,
o.id AS order_id, o.user_id, o.order_no, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
嵌套查询(分布加载)。步骤类似
- 编写关联表的查询 Mapper
- 配置主实体的 resultMap
- 编写主表查询 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<!-- OrderMapper.xml 先定义查询方法 -->
<mapper namespace="hbnu.project.mybatisdemo.mapper.OrderMapper">
<select id="findOrdersByUserId" parameterType="int" resultType="Order">
SELECT id, user_id, order_no, amount FROM orders WHERE user_id = #{userId}
</select>
</mapper>
<!-- UserMapper.xml 中嵌套查询 -->
<resultMap id="UserWithOrdersNestedResultMap" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<collection
property="orders"
ofType="Order"
column="id" <!-- 传递用户id给订单查询 -->
select="hbnu.project.mybatisdemo.mapper.OrderMapper.findOrdersByUserId"
fetchType="lazy"/> <!-- 懒加载,用到订单时才查 -->
</resultMap>
<select id="findUserWithOrdersNestedById" resultMap="UserWithOrdersNestedResultMap">
SELECT id, name, email, age FROM users WHERE id = #{id}
</select>
多对多关联映射(基于
<collection>)多对多关联,例如
User ↔︎ Role,本质是 一对多 + 中间表,通过中间表user_role联表查询,仍使用<collection>标签。多对多 = 「主表 ↔︎ 中间表 ↔︎ 关联表」的两次一对多,本质仍用
<collection>映射集合属性。那么,具体实现的步骤如下
- 编写三表联查 SQL:主表 + 中间表 + 关联表 JOIN,也别忘了处理列名冲突
- 然后配置
resultMap- 先映射主实体的基本属性
- 嵌套
<collection>标签,映射关联角色集合,同一对多
- 绑定 SQL 与 resultMap
那么,例如,对于这样的一个 Mapper 方法
1
User findUserWithRolesById(Integer id);
编写这样的
resultMap1
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<!-- UserMapper.xml -->
<resultMap id="UserWithRolesResultMap" type="User">
<!-- 主表字段映射 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<!-- 多对多关联:List<Role>(通过user_role中间表) -->
<collection
property="roles"
ofType="hbnu.project.mybatisdemo.entity.Role">
<id column="role_id" property="id"/>
<result column="role_name" property="roleName"/>
</collection>
</resultMap>
<!-- 联表查询:users ↔ user_role ↔ roles -->
<select id="findUserWithRolesById" resultMap="UserWithRolesResultMap">
SELECT
u.id, u.name, u.email, u.age,
r.id AS role_id, r.role_name
FROM users u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.id = #{id}
</select>
最后,一般来说,能用嵌套结果就用嵌套结果的方式,如果你用嵌套查询容易会触发 N+1 查询问题
fetchType="lazy"类似
Hibernate,是针对非核心关联数据,用到时才查询,减少初始查询的数据量;需开启
MyBatis 懒加载全局配置
1 | mybatis: |
resultType 替代
resultMap
如果数据库列名与实体属性名完全一致,或开启了驼峰转换map-underscore-to-camel-case: true,可直接用resultType,无需写<resultMap>:
1 | <select id="findUserById" parameterType="int" resultType="hbnu.project.mybatisdemo.entity.User"> |
一般情况下,这个东西好像只能是单表简单查询用,没见过多表关联用这个的
SQL操作标签详解
MyBatis 的
<insert>、<delete>、<update>、<select>
是映射文件中处理数据库增删改查的核心标签,也是日常开发中使用频率最高的组件。
在分别展开描述单个标签前,先掌握所有 SQL 操作标签的通用约束
| 通用规则 | 具体要求 |
|---|---|
| 核心绑定逻辑 | 标签 id 必须与 Mapper
接口方法名完全一致(大小写敏感),namespace
绑定接口全类名 |
| 参数绑定 | 用 #{} 做预编译参数绑定(防注入),${}
仅用于动态表名 / 列名 |
| 配置优先级 | 标签属性 > MyBatis 全局配置 > Spring Boot 配置 |
| 异常处理 | 增删改默认抛出 SQLExecutionException,查询抛出
NoResultException(可选) |
| 缓存行为 | 增删改默认刷新二级缓存(flushCache=true),查询默认不刷新(flushCache=false) |
<insert>:新增标签
用于执行数据库 INSERT 语句。进行插入数据
他不仅解决数据插入,还会对主键进行回填,是业务中创建数据的入口。
其中的属性如下
| 属性名 | 作用 | 示例 | 是否必填 |
|---|---|---|---|
id |
绑定 Mapper 接口的新增方法名 | id="insertUser" |
是 |
parameterType |
入参类型(全类名 / 别名,MyBatis 可自动推断,建议显式声明) | parameterType="User" |
否 |
useGeneratedKeys |
是否开启自增主键回填(适配 MySQL/AutoIncrement、PostgreSQL/Sequence) | useGeneratedKeys="true" |
否 |
keyProperty |
主键回填到入参实体的属性名(需配合
useGeneratedKeys) |
keyProperty="id" |
否 |
keyColumn |
数据库表的主键列名(与 keyProperty
对应,列名与属性名一致时可省略) |
keyColumn="user_id" |
否 |
flushCache |
是否刷新二级缓存(默认 true,增删改操作强制刷新) |
flushCache="true" |
否 |
timeout |
SQL 执行超时时间(秒),超时抛出
SQLTimeoutException |
timeout="10" |
否 |
插入单条数据并自动回填自增主键的例子就如下
1 | <!-- UserMapper.xml --> |
对应的 Mapper 接口
1 | // 返回值为受影响行数,主键会回填到入参 User 对象的 id 属性 |
如果你的数据库不是 MySQL,是 PostgreSQL/Oracle 那种主键序列,可以自定义主键生成
1 | <insert id="insertUserWithSequence"> |
order="BEFORE":插入前执行主键查询,适配序列,order="AFTER"是插入后执行,适配 MySQL 自增
批量插入就这样
1 | <insert id="batchInsertUser" parameterType="java.util.List"> |
对应的接口如下
1 | int batchInsertUser(List<User> userList); |
- 用
<foreach>遍历集合,拼接批量插入 SQL,避免循环单条插入
<delete>:删除标签
执行 DELETE 语句,能够解决 单条 或 批量 删除,别忘了删除条件,避免全表删除
参数比较少,也和上面的差不多
| 属性名 | 作用 | 示例 |
|---|---|---|
id |
绑定 Mapper 接口删除方法名 | id="deleteUserById" |
parameterType |
入参类型(int/List/Map/ 实体) | parameterType="java.util.List" |
timeout |
执行超时时间 | timeout="5" |
按主键单条删除是最基础的用法
例如对应的接口:int deleteUserById(Integer id);
1 | <delete id="deleteUserById" parameterType="int"> |
如果是条件删除,按非主键
1 | <delete id="deleteUserByEmail" parameterType="java.lang.String"> |
批量删除也类似,使用<foreach>
1 | <delete id="batchDeleteUser" parameterType="java.util.List"> |
<update>:更新标签
执行 UPDATE 语句,也就是字段更新 ,主要是动态部分字段更新,是业务中修改数据的核心标签。
其中的属性如下
| 属性名 | 作用 | 示例 |
|---|---|---|
id |
绑定 Mapper 接口更新方法名 | id="updateUser" |
parameterType |
入参类型(实体 / Map) | parameterType="User" |
flushCache |
刷新二级缓存(默认 true) |
flushCache="true" |
其中的基础用法,全字段更新
1 | <update id="updateUser" parameterType="User"> |
但是这样更新的话,若入参实体的 email 为
null,会把数据库的 email 覆盖为
null。
所以说,一般情况下,使用<update>
的动态部分字段更新
1 | <update id="updateUserSelective" parameterType="User"> |
<set>标签自动剔除最后一个字段后的逗号,避免 SQL 语法错误;<if>标签判断属性非空,仅更新有值的字段,解决全字段更新的null覆盖问题。
也能批量更新,也是使用<foreach>标签
1 | <update id="batchUpdateUser" parameterType="java.util.List"> |
<select>:查询标签
单条查询
我说,大哥来了
执行 SELECT 语句,是 MyBatis 中最灵活、功能最丰富的标签,单条 / 批量 / 分页 / 关联,都使用的这个
属性比较多,如下
| 属性名 | 作用 | 示例 | 是否必填 |
|---|---|---|---|
id |
绑定 Mapper 接口查询方法名 | id="findUserById" |
是 |
parameterType |
入参类型 | parameterType="int" |
否 |
resultType |
结果类型(全类名 / 别名,简单查询用) | resultType="User" |
二选一 |
resultMap |
结果映射集(复杂查询 / 多表关联用) | resultMap="UserWithOrdersResultMap" |
二选一 |
fetchSize |
每次从数据库获取的行数(优化查询性能) | fetchSize="100" |
否 |
timeout |
执行超时时间 | timeout="10" |
否 |
useCache |
是否使用二级缓存(默认 true) |
useCache="false" |
否 |
单条查询查询出单条数据,没什么别的特殊的,大家都了解了,例如对于User findUserById(Integer id);的一个
Mapper 接口,它的 XML 如下
1 | <select id="findUserById" parameterType="int" resultType="User"> |
批量查询
按条件批量查询,没什么特殊的,该怎么查就怎么查,只不过 Mapper
那边的接口要使用容器对应上,例如对于List<User> findUserByAge(Integer age);的一个
Mapper 接口,它的 XML 如下
1 | <select id="findUserByAge" parameterType="int" resultType="User"> |
<foreach> 的核心属性
| 属性名 | 作用 | 示例 |
|---|---|---|
collection |
入参集合的类型(List 填 list,数组填
array,Map 填 key 名) |
collection="list" |
item |
遍历的单个元素别名 | item="id" |
open |
拼接内容的开头字符 | open="(" |
separator |
元素之间的分隔符 | separator="," |
close |
拼接内容的结尾字符 | close=")" |
index |
遍历索引(List 是下标,Map 是 key) | index="i" |
但是对于动态查询中 IN 条件、批量 OR 条件,得多说一句
IN 条件查询多个用户
对于下面的 Mapper 中的接口方法
1
List<User> findUserByIds(List<Integer> ids);
可以这样利用
<foreach>1
2
3
4
5
6
7
8
9<select id="findUserByIds" parameterType="java.util.List" resultType="User">
SELECT id, name, email, age FROM users
<where>
id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</where>
</select>批量 OR 条件
对于下面的 Mapper 中的接口方法
1
List<User> findUserByMultiCondition(List<User> userList);
可以这样利用
<foreach>1
2
3
4
5
6
7
8<select id="findUserByMultiCondition" parameterType="java.util.List" resultType="User">
SELECT id, name, email, age FROM users
<where>
<foreach collection="list" item="user" separator="OR">
(name = #{user.name} AND age = #{user.age})
</foreach>
</where>
</select>
动态条件查询
但是,我们通常使用动态条件查询,它更灵活
1 | <select id="findUserByCondition" parameterType="User" resultType="User"> |
<where>标签类似 WHERE 关键字,只不过,具体的逻辑内容如下- 若标签内有有效内容,自动添加
WHERE关键字; - 然后会自动剔除内容开头的
AND/OR; - 若标签内无有效内容,不生成
WHERE子句,避免SELECT * FROM users WHERE语法错误
- 若标签内有有效内容,自动添加
<trim>自定义拼接规则当
<where>无法满足复杂需求时,用<trim>更灵活1
2
3
4
5
6
7
8
9<select id="findUserByCondition" parameterType="User" resultType="User">
SELECT id, name, email, age FROM users
<trim prefix="WHERE" prefixOverrides="AND | OR">
<if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="age != null">AND age = #{age}</if>
<if test="email != null">AND email = #{email}</if>
</trim>
ORDER BY id
</select><trim>属性作用 prefix给拼接后的内容加前缀(如 WHERE)prefixOverrides剔除内容开头的指定字符串 suffix给拼接后的内容加后缀(如 ORDER BY id)suffixOverrides剔除内容结尾的指定字符串(如逗号)
支持
<choose>/<when>/<otherwise>实现多条件分支:<choose>是 排他性条件,满足一个就跳过剩余,一般是多条件互斥,而非简单的多条件叠加:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<select id="findUserByPriorityCondition" parameterType="User" resultType="User">
SELECT id, name, email, age FROM users
<where>
<choose>
<!-- 优先级1:按ID精准查 -->
<when test="id != null">AND id = #{id}</when>
<!-- 优先级2:按手机号精准查 -->
<when test="phone != null and phone != ''">AND phone = #{phone}</when>
<!-- 优先级3:按姓名模糊查 -->
<when test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</when>
<!-- 默认条件:只查未删除的 -->
<otherwise>AND is_deleted = 0</otherwise>
</choose>
</where>
</select>最多只执行一个
<when>,无匹配则执行<otherwise>;<if>是 叠加条件,满足即加,<choose>是 分支条件,只选其一。
分页查询
物理分页查询也能用select实现,就是原生 SQL
语句,只不过要传入分页的数据
1 | <select id="findUserByPage" resultType="User"> |
对应的 Mapper 接口的如下
1 | List<User> findUserByPage( String name, Integer pageNum, Integer pageSize); |
分页参数通常需要安全校验,避免传入非法分页参数,如
pageNum=-1、pageSize=10000导致性能问题,可在
XML 内用 <if> 校验
1 | <select id="findUserByPage" resultType="User"> |
但是生产中很少手写分页 SQL,常用 PageHelper 插件,其核心原理是:
- 拦截
<select>标签的执行,在 SQL 前拼接分页语法; - 自动查询总条数(无需手动写
COUNT(*)); - 封装分页结果(PageInfo)。
1 | // Service 层 |
PageHelper 会修改 <select> 标签的 SQL,自动添加
LIMIT/OFFSET,无需修改 XML 就能获得分页的效果
多表关联查询
多表关联查询上面说了,不说了
最后是使用 XML
最后,要配置 MyBatis 的 XML 文件路径
必须告诉MyBatis,你的 XML 映射文件放在哪里。在
application.yml 中添加:
1 | mybatis: |
这个配置是什么意思呢?
classpath: 表示从编译后的classes目录下找(也就是resources目录下的文件编译后会放到classes里)。你可以把它理解为 项目的根目录。mapper/**Mapper.xml是一个路径通配符:mapper/是指在mapper文件夹下找,那么你的 mapper 的 xml 就应该放到resources目录下 mapper 文件夹里**表示任意多级目录(包括子文件夹)这里面使用一个*或者两个*都是可以的。*Mapper.xml表示文件名以 Mapper.xml 结尾。
因为 MyBatis 启动时,找到了你的 Mapper 接口后,需要去加载对应的 XML 文件,解析里面的 SQL 语句。如果不告诉它路径,它就会像无头苍蝇一样找不到文件,你调用 Mapper 接口时就会报错,说找不到对应的 SQL。所以这一步是必须的!
如何进行参数绑定与类型处理
首先,Mapper 接口传递过来的参数,Mapper XML 中对应的 SQL 是要进行接收和处理的,那么这个参数绑定如何进行,其中,涉及到类型问题又该如何正确处理
#{}和${}
它们是做参数绑定的,也就是 Mapper 接口方法传入的参数,就是通过这两个语法填到 XML 配置文件中的 SQL 的
两者都是进行的是单参数绑定
| 语法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
#{} |
预编译,参数占位符 | 防 SQL 注入,类型安全 | 无法动态拼接表名 / 列名 | 普通参数(值) |
${} |
字符串直接替换 | 支持动态表名 / 列名 | 有 SQL 注入风险 | 动态表名、列名(需手动过滤) |
然后来详细看看这两者
#{参数名}会将接口方法传入的参数,以「JDBC 预编译占位符(?)」的形式绑定到 SQL 中,MyBatis 会自动处理参数类型转换,能够保证类型安全,而且还能防 SQL 注入,是日常开发的首选。1
2
3
4// 单参数:删除用户
int deleteUserById(Integer id);
// 多参数:条件查询(@Param指定参数名)
List<User> findUserByNameAndAge( String name, Integer minAge);1
2
3
4
5
6
7
8
9
10
11
12
13<!-- 单参数绑定:#{id} 对应接口的 Integer id -->
<delete id="deleteUserById">
DELETE FROM users WHERE id = #{id}
</delete>
<!-- 多参数绑定:#{name}/#{minAge} 对应@Param的名称 -->
<select id="findUserByNameAndAge" resultMap="UserResultMap">
SELECT * FROM users
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="minAge != null">AND age >= #{minAge}</if>
</where>
</select>因为
#{}是占位符,只能替换 值,不能替换 SQL 关键字${参数名}会将接口方法传入的参数,以 字符串直接拼接 的形式替换到 SQL 中,不做任何转义,仅适用于 动态表名 / 列名 等无法用#{}的场景。比如你需要动态指定查询的表名
1
2// 动态表名查询
List<User> findUserByTable( String tableName, Integer id);1
2
3
4<select id="findUserByTable" resultMap="UserResultMap">
<!-- ${tableName} 直接替换为传入的表名,#{id} 用预编译 -->
SELECT * FROM ${tableName} WHERE id = #{id}
</select>${tableName}是 字符串替换,不是占位符,而#{id}仍为预编译占位符。所以说,它有 SQL 注入风险,而且无类型转换,需手动保证类型正确。安全使用
${}的前提是必须对传入的参数做严格过滤
多个参数绑定
Mapper 方法有多个参数时,MyBatis 会将参数封装为
Map,默认键为 param1、param2... 或参数索引
所以说,对于下面的 Mapper 接口,你可以这样写 XML 去做多个参数的绑定
1 | // 根据姓名和年龄查询用户 |
1 | <select id="selectUserByNameAndAge" resultType="com.example.entity.User"> |
这样做虽然可以,但是基本上没有人会这么做
我们都使用 @Param 注解,指定参数名称,提高可读性。
所以说接口都这么写
1 | // 注解指定参数名 |
XML 这样做参数绑定
1 | <select id="selectUserByNameAndAge" resultType="com.example.entity.User"> |
MyBatis 3.4+ 支持通过编译参数直接使用参数名,无需
@Param注解。
- Maven 编译插件添加参数:
1
2
3
4
5
6
7
8
9
10 ><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg> <!-- 保留参数名称 -->
</compilerArgs>
</configuration>
></plugin>
- Mapper 接口无需
@Param:
1
2 >// 直接使用参数名name/age
>List<User> selectUserByNameAndAge(String name, Integer age);
- XML 中直接使用参数名:
1 >WHERE name = #{name} AND age = #{age}但是通常没有人会这样写,这只能算是一个特性,约定大于配置还是太有用了我说
对象参数绑定
参数是自定义 JavaBean,如 User、Order,直接通过
#{属性名} 绑定。这样 MyBatis 会自己自动解析对象的 getter
方法,就好像如 #{name} 对应
getName(),而且支持嵌套属性
对于一个这样的实体类
1 | public class User { |
Mapper 接口如下
1 | // 添加用户 |
对应的 XML 就可以这样写
1 | <insert id="insertUser"> |
Map参数绑定
参数是 Map 时,通过 #{key} 绑定 Map
中的值,适合参数不固定的场景。
1 | // 动态条件查询 |
Mapper XML
1 | <select id="selectUserByMap" resultType="com.example.entity.User"> |
集合 / 数组参数绑定
例如,我们使用传递一个数组参数来批量删除,需结合 @Param
注解,配合 <foreach> 标签使用
1 | // 批量删除用户 |
1 | <delete id="deleteUserByIds"> |
List 参数也一样
1 | // 批量插入用户 |
1 | <insert id="batchInsertUser"> |
MyBatis 类型处理
我们都知道 Java 类型和数据库类型是对不上的,所以说,MyBatis 要承担这个类型转换的职责
MyBatis 内置了大部分常用类型的处理器
| Java 类型 | 数据库类型 | 对应 TypeHandler |
|---|---|---|
| String | VARCHAR/CHAR | StringTypeHandler |
| Integer/int | INT | IntegerTypeHandler |
| Date | DATE/TIMESTAMP | DateTypeHandler |
| Boolean/boolean | BIT/TINYINT | BooleanTypeHandler |
在参数绑定的时候,MyBatis 根据 Java 类型自动选择对应的 TypeHandler,将 Java 对象转为 JDBC 参数。
在结果映射的时候也类似,根据数据库字段类型和 Java 实体属性类型,自动选择 TypeHandler 转换为 Java 对象。
而当内置处理器无法满足需求时,可自定义 TypeHandler。
就对于这样的一个枚举类型,MyBatis 是没法自动处理的
1 | // 用户状态枚举 |
所以我们需要自己实现类型转换器,实现 TypeHandler 接口
1 | import org.apache.ibatis.type.BaseTypeHandler; |
然后进行注册,注册自定义 TypeHandler,虽然 XML 也能注册,但是一般使用注解注册
1 | // 在实体类属性上指定 |




