MyBatis 映射文件深度解析:从基础 CRUD 到复杂关联与动态 SQL
MyBatis 映射文件(通常命名为 XxxMapper.xml)是 SQL 语句与 Java 接口的 “桥梁”,承担着SQL 定义、参数映射、结果转换、缓存配置四大核心职责。相较于全局配置文件,映射文件更贴近业务逻辑,是 MyBatis 灵活可控的关键。本文在基础 CRUD 之上,补充动态 SQL、批量操作、复杂关联映射、缓存优化及工程最佳实践,覆盖 90% 以上的开发场景。
核心 CRUD 操作:细节与扩展
MyBatis 通过 <select>、<insert>、<update>、<delete> 标签实现 CRUD,每个标签都有丰富的属性控制 SQL 执行逻辑,需重点关注事务提交、主键生成、批量操作三大核心场景。
基础 CRUD 与事务提交
MyBatis 的 SqlSession 默认不自动提交事务,需手动调用 commit() 或开启自动提交。
(1)基础示例
1 | <!-- 1. 查询:根据 ID 获取用户 --> |
(2)事务提交方式
1 | // 方式 1:手动提交(推荐,支持事务回滚) |
插入操作扩展:获取自增主键
插入数据后,常需获取数据库生成的自增主键(如 MySQL 的 AUTO_INCREMENT、Oracle 的序列),MyBatis 提供两种实现方式。
(1)MySQL 自增主键(useGeneratedKeys)
1 | <!-- |
使用示例:
1 | User user = new User("王五", 25); |
(2)Oracle 序列(<selectKey>)
Oracle 无自增特性,需通过序列生成主键,使用 <selectKey> 在插入前获取序列值:
1 | <insert id="insertUserOracle" parameterType="com.example.pojo.User"> |
批量操作:插入 / 删除 / 更新
批量操作是高频场景(如批量导入数据),MyBatis 通过 <foreach> 标签实现,需配合 BATCH 执行器提升性能。
(1)批量插入(MySQL)
1 | <!-- 批量插入用户,参数为 List<User> --> |
(2)批量删除
1 | <!-- 批量删除用户,参数为 Long[] ids --> |
(3)批量更新(动态字段)
1 | <!-- 批量更新用户年龄,参数为 List<User>(仅更新 age 字段) --> |
(4)性能优化:启用 BATCH 执行器
批量操作需在全局配置中设置 defaultExecutorType="BATCH",避免频繁创建 PreparedStatement:
1 | <settings> |
参数处理:从简单类型到复杂参数
MyBatis 支持多种参数类型(基本类型、对象、集合、Map 等),通过 #{} 或 ${} 取值,核心是明确参数的封装规则,避免取值错误。
参数类型分类与取值方式
(1)单个基本类型 / 包装类型
接口方法:
User getUserById(Long id);映射文件:直接用#{任意名称}取值(因单个参数无需区分,推荐与参数名一致):
1
2
3<select id="getUserById" resultType="User">
SELECT * FROM t_user WHERE id = #{id} <!-- #{id} 或 #{userId} 均可 -->
</select>
(2)多个参数:@Param 注解(推荐)
多个参数默认封装为 Map,键为 param1、param2…,值为参数值,可读性差。推荐用 @Param 明确 Map 的键:
接口方法:
1
User getUserByUsernameAndAge( String username, Integer age);
映射文件:用#{注解值}取值:
1
2
3<select id="getUserByUsernameAndAge" resultType="User">
SELECT * FROM t_user WHERE user_name = #{username} AND age = #{age}
</select>
(3)Java 对象参数
参数为自定义 POJO 时,直接用 #{属性名} 取值(需与 POJO 的 getter 方法对应):
POJO 类:
1
2
3
4
5
6public class UserQuery {
private String username;
private Integer minAge;
private Integer maxAge;
// getter/setter
}接口方法:
List<User> getUserByQuery(UserQuery query);映射文件:
1
2
3
4
5<select id="getUserByQuery" resultType="User">
SELECT * FROM t_user
WHERE user_name LIKE CONCAT('%', #{username}, '%')
AND age BETWEEN #{minAge} AND #{maxAge}
</select>
(4)集合 / 数组参数
List 参数:默认封装为Map,键为list,用
#{list[索引]}或<foreach>遍历:1
2
3
4
5
6
7
8
9// 接口方法
List<User> getUserByIds(List<Long> ids);
// 映射文件
<select id="getUserByIds" resultType="User">
SELECT * FROM t_user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>数组参数:默认封装为Map,键为array,遍历方式与 List 类似:
1
2
3
4
5
6
7
8
9// 接口方法
List<User> getUserByAges(Integer[] ages);
// 映射文件
<select id="getUserByAges" resultType="User">
SELECT * FROM t_user WHERE age IN
<foreach collection="array" item="age" open="(" separator="," close=")">
#{age}
</foreach>
</select>
(5)Map 参数
直接用 #{Map的键} 取值,适合参数无固定 POJO 场景(如临时多条件查询):
接口方法:
List<User> getUserByMap(Map<String, Object> paramMap);调用方式:
1
2
3
4Map<String, Object> paramMap = new HashMap<>();
paramMap.put("username", "张三");
paramMap.put("age", 20);
List<User> userList = mapper.getUserByMap(paramMap);映射文件:
1
2
3<select id="getUserByMap" resultType="User">
SELECT * FROM t_user WHERE user_name = #{username} AND age = #{age}
</select>
#{} 与 ${} 的核心区别(重点)
两者都是参数占位符,但底层实现和安全性差异巨大,生产环境优先用 #{ }。
| 对比维度 | #{ } | ${ } |
|---|---|---|
| 底层实现 | 预编译 SQL(PreparedStatement),用 ? 占位 |
字符串直接替换,编译前替换参数 |
| SQL 注入风险 | 无(参数自动转义) | 有(直接拼接字符串,需手动过滤参数) |
| 适用场景 | 绝大多数场景(参数值、条件值) | 动态表名、动态字段名(如 ORDER BY ${field}) |
| 示例 | WHERE id = #{id} → WHERE id = ? |
SELECT * FROM ${tableName} → SELECT * FROM t_user |
安全风险示例(${ } 导致 SQL 注入)
恶意参数:若参数为1 OR 1=1,用${ }会拼接为:
1
SELECT * FROM t_user WHERE id = 1 OR 1=1 -- 查询所有用户,数据泄露
#{ } 防护:自动转义为字符串,实际执行 SQL:
1
SELECT * FROM t_user WHERE id = '1 OR 1=1' -- 无数据返回,安全
${ } 安全使用场景(需严格校验参数)
仅在动态表名 / 字段名时使用,且需确保参数来源安全(如内部枚举,非用户输入):
1 | <!-- 动态排序字段,参数仅允许 "user_name" 或 "age" --> |
结果映射:resultType 与 resultMap 深度对比
MyBatis 通过 resultType 或 resultMap 实现数据库列 → Java 对象属性的映射,两者适用场景不同,需根据复杂度选择。
resultType:简单映射(推荐用于简单场景)
resultType 直接指定返回值类型(POJO、Map、基本类型),要求数据库列名与 Java 属性名一致(或通过 SQL 别名匹配)。
(1)返回 POJO(列名与属性名一致)
POJO:
User有id、userName、age属性;SQL 别名:数据库列user_name用别名userName匹配属性:
1
2
3
4
5
6
7
8<select id="getUserById" resultType="com.example.pojo.User">
SELECT
id,
user_name AS userName, -- 别名匹配属性 userName
age
FROM t_user
WHERE id = #{id}
</select>
(2)返回 Map(无固定 POJO 场景)
返回单条记录时,Map 的键为列名,值为列值;返回多条记录时,用 List<Map<String, Object>>:
1 | <select id="getUserMapById" resultType="java.util.Map"> |
(3)返回基本类型 / 包装类型
适用于聚合查询(如计数、求和):
1 | <select id="getUserCount" resultType="java.lang.Long"> |
resultMap:复杂映射(推荐用于关联查询、列名不匹配)
resultMap 是 MyBatis 最强大的映射功能,支持自定义列与属性的映射关系、继承、关联查询(一对一 / 一对多)、鉴别器,解决 resultType 无法处理的复杂场景。
(1)基础用法:列名与属性名不匹配
1 | <!-- 1. 定义 resultMap:id 为唯一标识,type 为返回 POJO 类型 --> |
(2)进阶:resultMap 继承(减少重复配置)
若多个 resultMap 有共同配置,可通过 extends 继承:
1 | <!-- 1. 父 resultMap:定义公共映射 --> |
(3)核心:关联查询映射(一对一 / 一对多)
关联查询是业务开发的重点,MyBatis 通过 <association>(一对一)和 <collection>(一对多)标签实现。
场景 1:一对一关联(用户 → 部门)
一个用户属于一个部门(User 有 Department 属性)。
方式 1:关联查询(单 SQL 联表)
1 | <!-- 1. 定义部门的 resultMap --> |
方式 2:分步查询(多 SQL,支持延迟加载)
先查询用户,再按需查询部门(避免联表查询的性能损耗):
1 | <!-- 1. 用户的 resultMap:分步查询部门 --> |
延迟加载配置(需在全局配置中开启):
1 | <settings> |
场景 2:一对多关联(部门 → 用户)
一个部门有多个用户(Department 有 List<User> 属性):
1 | <!-- 1. 部门的 resultMap:关联用户列表 --> |
(4)高级:鉴别器(discriminator)
根据某列的值动态选择不同的映射规则(类似 Java 的 switch),适用于 “同一表不同类型数据映射到不同 POJO” 的场景(如订单表:普通订单 vs 秒杀订单):
1 | <!-- 1. 父 POJO:Order --> |
动态 SQL:灵活拼接复杂查询
动态 SQL 是 MyBatis 应对 “多条件查询、动态字段更新” 的核心功能,通过 <if>、<choose>、<where>、<set>、<foreach> 等标签实现 SQL 动态拼接,避免手动拼接字符串的繁琐与错误。
常用动态 SQL 标签
(1)<if>:条件判断
根据参数是否为空,动态添加 SQL 片段(如多条件查询):
1 | <select id="getUserByCondition" resultType="User"> |
(2)<where>:自动处理多余的 AND/OR
<where> 标签会自动去除条件前多余的 AND 或 OR,替代 WHERE 1=1:
1 | <select id="getUserByCondition" resultType="User"> |
(3)<choose>/<when>/<otherwise>:分支选择
类似 Java 的 switch-case,仅执行第一个匹配的条件:
1 | <select id="getUserByChoose" resultType="User"> |
(4)<set>:动态更新字段
<set> 标签会自动去除更新语句中多余的逗号,避免语法错误:
1 | <update id="updateUserDynamic"> |
(5)<foreach>:遍历集合
用于批量操作(如 IN 条件、批量插入),已在 “批量操作” 部分示例,此处补充动态表名场景:
1 | <!-- 动态查询多个表的数据(需确保表结构一致) --> |
(6)<trim>:自定义前缀 / 后缀
<trim> 是更灵活的标签,可自定义添加前缀、后缀,或去除多余字符:
1 | <!-- 替代 <where> 标签:添加 WHERE 前缀,去除多余的 AND/OR --> |
SQL 片段:复用重复 SQL
对于频繁使用的 SQL 片段(如字段列表、条件语句),可通过 <sql> 标签定义为片段,再用 <include> 引用,减少重复代码,提升维护性。
基础用法:复用字段列表
1 | <!-- 1. 定义 SQL 片段:id 为唯一标识 --> |
进阶:带参数的 SQL 片段
通过 <property> 标签给 SQL 片段传递参数,实现动态复用:
1 | <!-- 1. 定义带参数的 SQL 片段 --> |
缓存配置:提升查询性能
MyBatis 提供两级缓存:一级缓存(Session 级,默认开启) 和 二级缓存(Mapper 级,需手动开启),映射文件中主要配置二级缓存。
二级缓存配置(Mapper 级)
在映射文件中添加 <cache> 标签,开启当前 Mapper 的二级缓存:
1 | <!-- |
缓存引用(cache-ref)
多个 Mapper 可共享同一缓存,通过 <cache-ref> 引用其他 Mapper 的缓存配置:
1 | <!-- UserMapper.xml 定义缓存 --> |
注意事项
- 二级缓存仅缓存可序列化的 POJO(需实现
Serializable接口); - 增删改操作会自动清空缓存(
flushCache="true"),确保数据一致性; - 分布式环境下,二级缓存可能导致数据不一致,需集成 Redis 等分布式缓存(MyBatis 提供
Cache接口,可自定义 Redis 缓存实现)。
工程实践最佳实践
映射文件命名规范
- 文件名:与 Mapper 接口同名,如
UserMapper.java对应UserMapper.xml; - 路径:与 Mapper 接口同包,如
src/main/java/com/example/mapper/UserMapper.java和src/main/resources/com/example/mapper/UserMapper.xml(Spring Boot 需配置mybatis.mapper-locations指向资源路径)。
SQL 书写规范
- 避免
SELECT *,明确指定字段(减少数据传输,避免字段新增导致的映射错误); - 关键字大写(如
SELECT、FROM、WHERE),提升可读性; - 复杂 SQL 分行书写,合理缩进(如多表联查、子查询);
- 用别名统一列名与属性名(如
user_name AS userName),减少resultMap配置。
性能优化
- 批量操作启用
BATCH执行器; - 关联查询优先用分步查询 + 延迟加载(避免大表联查);
- 高频查询启用二级缓存,低频查询禁用(如实时数据);
- 避免 N+1 问题(分步查询时,用
fetchType="eager"或批量查询解决)。
常见问题排查
- 字段名不匹配:检查
resultMap配置或 SQL 别名是否正确; - N+1 问题:分步查询时,若循环访问关联对象,会执行 N+1 次 SQL,需用批量查询(如
foreach批量获取关联数据); - 延迟加载异常:
Session关闭后访问延迟加载属性,需在Session关闭前初始化关联对象(Hibernate.initialize(user.getDepartment())); - SQL 注入:确保用户输入参数用
#{},动态表名 / 字段名用${}且严格校验参数。
总结
MyBatis 映射文件是连接业务逻辑与数据库的核心,其灵活性体现在动态 SQL 应对复杂查询、resultMap 处理复杂映射、缓存配置提升性能。掌握本文涵盖的 CRUD 扩展、参数处理、关联映射、动态 SQL 及工程实践,可轻松应对 90% 以上的业务场景。
核心要点:
- 参数处理:优先用
@Param明确多参数,#{}防止 SQL 注入; - 结果映射:简单场景用
resultType,复杂场景(关联查询、列名不匹配)用resultMap; - 动态 SQL:灵活运用
<if>、<where>、<foreach>等标签,避免手动拼接 SQL; - 性能优化:批量操作、延迟加载、缓存配置是关键