0%

mybatis映射文件

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1. 查询:根据 ID 获取用户 -->
<select id="getUserById" parameterType="Long" resultType="com.example.pojo.User">
SELECT id, user_name AS userName, age FROM t_user WHERE id = #{id}
</select>

<!-- 2. 插入:新增用户 -->
<insert id="insertUser" parameterType="com.example.pojo.User">
INSERT INTO t_user (user_name, age) VALUES (#{userName}, #{age})
</insert>

<!-- 3. 更新:修改用户信息 -->
<update id="updateUser" parameterType="com.example.pojo.User">
UPDATE t_user SET user_name = #{userName}, age = #{age} WHERE id = #{id}
</update>

<!-- 4. 删除:根据 ID 删除用户 -->
<delete id="deleteUser" parameterType="Long">
DELETE FROM t_user WHERE id = #{id}
</delete>
(2)事务提交方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方式 1:手动提交(推荐,支持事务回滚)
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insertUser(new User("张三", 20));
session.commit(); // 手动提交事务
} catch (Exception e) {
session.rollback(); // 异常时回滚
}

// 方式 2:自动提交(不推荐,无法回滚)
try (SqlSession session = sqlSessionFactory.openSession(true)) { // openSession(true) 开启自动提交
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insertUser(new User("李四", 22));
// 无需手动 commit,关闭时自动提交
}

插入操作扩展:获取自增主键

插入数据后,常需获取数据库生成的自增主键(如 MySQL 的 AUTO_INCREMENT、Oracle 的序列),MyBatis 提供两种实现方式。

(1)MySQL 自增主键(useGeneratedKeys
1
2
3
4
5
6
7
8
<!-- 
useGeneratedKeys="true":启用 JDBC 的 getGeneratedKeys() 获取自增主键
keyProperty="id":将获取的主键值赋给 User 对象的 id 属性
-->
<insert id="insertUserWithId" parameterType="com.example.pojo.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (user_name, age) VALUES (#{userName}, #{age})
</insert>

使用示例

1
2
3
User user = new User("王五", 25);
mapper.insertUserWithId(user);
System.out.println(user.getId()); // 插入后自动获取 id(如 1001)
(2)Oracle 序列(<selectKey>

Oracle 无自增特性,需通过序列生成主键,使用 <selectKey> 在插入前获取序列值:

1
2
3
4
5
6
7
8
9
10
11
<insert id="insertUserOracle" parameterType="com.example.pojo.User">
<!--
order="BEFORE":在插入 SQL 执行前获取序列值(关键,因 Oracle 需先有主键才能插入)
keyProperty="id":将序列值赋给 User 的 id 属性
resultType="Long":序列值类型
-->
<selectKey keyProperty="id" order="BEFORE" resultType="Long">
SELECT USER_SEQ.NEXTVAL FROM DUAL <!-- Oracle 序列 -->
</selectKey>
INSERT INTO t_user (id, user_name, age) VALUES (#{id}, #{userName}, #{age})
</insert>

批量操作:插入 / 删除 / 更新

批量操作是高频场景(如批量导入数据),MyBatis 通过 <foreach> 标签实现,需配合 BATCH 执行器提升性能。

(1)批量插入(MySQL)
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 批量插入用户,参数为 List<User> -->
<insert id="batchInsertUser" parameterType="java.util.List">
INSERT INTO t_user (user_name, age) VALUES
<!--
collection="list":参数为 List 时,collection 取值为 "list"(若用 @Param 则为注解值)
item="user":遍历的每个元素别名
separator=",":元素间的分隔符(逗号)
-->
<foreach collection="list" item="user" separator=",">
(#{user.userName}, #{user.age})
</foreach>
</insert>
(2)批量删除
1
2
3
4
5
6
7
<!-- 批量删除用户,参数为 Long[] ids -->
<delete id="batchDeleteUser" parameterType="java.lang.Long[]">
DELETE FROM t_user WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
(3)批量更新(动态字段)
1
2
3
4
5
6
<!-- 批量更新用户年龄,参数为 List<User>(仅更新 age 字段) -->
<update id="batchUpdateUserAge">
<foreach collection="list" item="user" separator=";">
UPDATE t_user SET age = #{user.age} WHERE id = #{user.id}
</foreach>
</update>
(4)性能优化:启用 BATCH 执行器

批量操作需在全局配置中设置 defaultExecutorType="BATCH",避免频繁创建 PreparedStatement

1
2
3
<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</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,键为 param1param2…,值为参数值,可读性差。推荐用 @Param 明确 Map 的键:

  • 接口方法:

    1
    User getUserByUsernameAndAge(@Param("username") String username, @Param("age") 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
    6
    public 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
    4
    Map<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
2
3
4
<!-- 动态排序字段,参数仅允许 "user_name" 或 "age" -->
<select id="getUserOrderBy" resultType="User">
SELECT * FROM t_user ORDER BY ${sortField} ASC
</select>

结果映射:resultType 与 resultMap 深度对比

MyBatis 通过 resultTyperesultMap 实现数据库列 → Java 对象属性的映射,两者适用场景不同,需根据复杂度选择。

resultType:简单映射(推荐用于简单场景)

resultType 直接指定返回值类型(POJO、Map、基本类型),要求数据库列名与 Java 属性名一致(或通过 SQL 别名匹配)。

(1)返回 POJO(列名与属性名一致)
  • POJOUseriduserNameage 属性;

  • 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
2
3
4
<select id="getUserMapById" resultType="java.util.Map">
SELECT id, user_name, age FROM t_user WHERE id = #{id}
</select>
<!-- 调用后返回:{id=1, user_name=张三, age=20} -->
(3)返回基本类型 / 包装类型

适用于聚合查询(如计数、求和):

1
2
3
<select id="getUserCount" resultType="java.lang.Long">
SELECT COUNT(*) FROM t_user
</select>

resultMap:复杂映射(推荐用于关联查询、列名不匹配)

resultMap 是 MyBatis 最强大的映射功能,支持自定义列与属性的映射关系、继承、关联查询(一对一 / 一对多)、鉴别器,解决 resultType 无法处理的复杂场景。

(1)基础用法:列名与属性名不匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 1. 定义 resultMap:id 为唯一标识,type 为返回 POJO 类型 -->
<resultMap id="UserResultMap" type="com.example.pojo.User">
<!-- id 标签:映射主键列(必填,提升性能) -->
<id column="user_id" property="id"/> <!-- 数据库列 user_id → 属性 id -->
<!-- result 标签:映射普通列 -->
<result column="user_name" property="userName"/> <!-- user_name → userName -->
<result column="user_age" property="age"/> <!-- user_age → age -->
</resultMap>

<!-- 2. 使用 resultMap -->
<select id="getUserById" resultMap="UserResultMap">
SELECT user_id, user_name, user_age FROM t_user WHERE user_id = #{id}
</select>
(2)进阶:resultMap 继承(减少重复配置)

若多个 resultMap 有共同配置,可通过 extends 继承:

1
2
3
4
5
6
7
8
9
10
<!-- 1. 父 resultMap:定义公共映射 -->
<resultMap id="BaseUserResultMap" type="com.example.pojo.User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>

<!-- 2. 子 resultMap:继承父配置,补充特有映射 -->
<resultMap id="UserWithAgeResultMap" type="com.example.pojo.User" extends="BaseUserResultMap">
<result column="user_age" property="age"/> <!-- 补充 age 映射 -->
</resultMap>
(3)核心:关联查询映射(一对一 / 一对多)

关联查询是业务开发的重点,MyBatis 通过 <association>(一对一)和 <collection>(一对多)标签实现。

场景 1:一对一关联(用户 → 部门)

一个用户属于一个部门(UserDepartment 属性)。

方式 1:关联查询(单 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
26
<!-- 1. 定义部门的 resultMap -->
<resultMap id="DeptResultMap" type="com.example.pojo.Department">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</resultMap>

<!-- 2. 定义用户的 resultMap,关联部门 -->
<resultMap id="UserWithDeptResultMap" type="com.example.pojo.User" extends="BaseUserResultMap">
<!--
association 标签:一对一关联
property:User 中的部门属性名(department)
javaType:部门属性的类型(Department)
resultMap:复用已定义的 DeptResultMap
-->
<association property="department" javaType="com.example.pojo.Department" resultMap="DeptResultMap"/>
</resultMap>

<!-- 3. 联表查询 SQL -->
<select id="getUserWithDept" resultMap="UserWithDeptResultMap">
SELECT
u.user_id, u.user_name,
d.dept_id, d.dept_name
FROM t_user u
LEFT JOIN t_department d ON u.dept_id = d.dept_id
WHERE u.user_id = #{id}
</select>

方式 2:分步查询(多 SQL,支持延迟加载)

先查询用户,再按需查询部门(避免联表查询的性能损耗):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 1. 用户的 resultMap:分步查询部门 -->
<resultMap id="UserWithDeptStepResultMap" type="com.example.pojo.User" extends="BaseUserResultMap">
<association
property="department"
column="dept_id" <!-- 将用户表的 dept_id 作为参数传入下一步查询 -->
select="com.example.mapper.DepartmentMapper.getDeptById" <!-- 调用部门 Mapper 的查询方法 -->
fetchType="lazy"/> <!-- 延迟加载:仅访问 department 时才执行查询 -->
</resultMap>

<!-- 2. 查询用户的 SQL -->
<select id="getUserWithDeptStep" resultMap="UserWithDeptStepResultMap">
SELECT user_id, user_name, dept_id FROM t_user WHERE user_id = #{id}
</select>

<!-- 3. 部门 Mapper 的查询(DepartmentMapper.xml) -->
<select id="getDeptById" parameterType="Long" resultType="com.example.pojo.Department">
SELECT dept_id AS id, dept_name AS name FROM t_department WHERE dept_id = #{id}
</select>

延迟加载配置(需在全局配置中开启):

1
2
3
4
<settings>
<setting name="lazyLoadingEnabled" value="true"/> <!-- 全局开启延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载,而非加载所有关联 -->
</settings>
场景 2:一对多关联(部门 → 用户)

一个部门有多个用户(DepartmentList<User> 属性):

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
<!-- 1. 部门的 resultMap:关联用户列表 -->
<resultMap id="DeptWithUsersResultMap" type="com.example.pojo.Department">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
<!--
collection 标签:一对多关联
property:部门中的用户列表属性名(userList)
ofType:列表中元素的类型(User,区别于 javaType)
column:将 dept_id 作为参数传入用户查询
select:调用用户 Mapper 的查询方法
-->
<collection
property="userList"
ofType="com.example.pojo.User"
column="dept_id"
select="com.example.mapper.UserMapper.getUserByDeptId"/>
</resultMap>

<!-- 2. 查询部门及用户的 SQL -->
<select id="getDeptWithUsers" resultMap="DeptWithUsersResultMap">
SELECT dept_id, dept_name FROM t_department WHERE dept_id = #{id}
</select>

<!-- 3. 用户 Mapper 的查询(UserMapper.xml) -->
<select id="getUserByDeptId" parameterType="Long" resultType="com.example.pojo.User">
SELECT user_id AS id, user_name AS userName FROM t_user WHERE dept_id = #{deptId}
</select>
(4)高级:鉴别器(discriminator)

根据某列的值动态选择不同的映射规则(类似 Java 的 switch),适用于 “同一表不同类型数据映射到不同 POJO” 的场景(如订单表:普通订单 vs 秒杀订单):

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
31
32
33
34
35
<!-- 1. 父 POJO:Order -->
public class Order {
private Long id;
private String orderNo;
// 共同属性
}

<!-- 2. 子 POJO:SeckillOrder(继承 Order) -->
public class SeckillOrder extends Order {
private Integer seckillId; // 秒杀订单特有属性
}

<!-- 3. 定义鉴别器 resultMap -->
<resultMap id="OrderResultMap" type="com.example.pojo.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<!--
discriminator 标签:根据 order_type 列的值选择映射
javaType:order_type 的类型(Integer)
column:判断的列名(order_type)
-->
<discriminator javaType="java.lang.Integer" column="order_type">
<!-- case 1:order_type=1 → 映射为 SeckillOrder -->
<case value="1" resultType="com.example.pojo.SeckillOrder">
<result column="seckill_id" property="seckillId"/> <!-- 补充特有属性 -->
</case>
<!-- case 2:默认 → 映射为普通 Order -->
<case value="0" resultType="com.example.pojo.Order"/>
</discriminator>
</resultMap>

<!-- 4. 查询 SQL -->
<select id="getOrderById" resultMap="OrderResultMap">
SELECT order_id, order_no, order_type, seckill_id FROM t_order WHERE order_id = #{id}
</select>

动态 SQL:灵活拼接复杂查询

动态 SQL 是 MyBatis 应对 “多条件查询、动态字段更新” 的核心功能,通过 <if><choose><where><set><foreach> 等标签实现 SQL 动态拼接,避免手动拼接字符串的繁琐与错误。

常用动态 SQL 标签

(1)<if>:条件判断

根据参数是否为空,动态添加 SQL 片段(如多条件查询):

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getUserByCondition" resultType="User">
SELECT * FROM t_user
WHERE 1=1 <!-- 避免后续条件无匹配时 SQL 语法错误 -->
<!-- 若 username 不为空,添加用户名条件 -->
<if test="username != null and username != ''">
AND user_name LIKE CONCAT('%', #{username}, '%')
</if>
<!-- 若 age 不为空,添加年龄条件 -->
<if test="age != null">
AND age = #{age}
</if>
</select>
(2)<where>:自动处理多余的 AND/OR

<where> 标签会自动去除条件前多余的 ANDOR,替代 WHERE 1=1

1
2
3
4
5
6
7
8
9
10
11
<select id="getUserByCondition" resultType="User">
SELECT * FROM t_user
<where>
<if test="username != null and username != ''">
user_name LIKE CONCAT('%', #{username}, '%') <!-- 无前置 AND -->
</if>
<if test="age != null">
AND age = #{age} <!-- 若前一个条件匹配,自动保留 AND;否则去除 -->
</if>
</where>
</select>
(3)<choose>/<when>/<otherwise>:分支选择

类似 Java 的 switch-case,仅执行第一个匹配的条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<select id="getUserByChoose" resultType="User">
SELECT * FROM t_user
<where>
<choose>
<!-- 若 username 不为空,仅按用户名查询 -->
<when test="username != null and username != ''">
user_name LIKE CONCAT('%', #{username}, '%')
</when>
<!-- 若 age 不为空,仅按年龄查询 -->
<when test="age != null">
age = #{age}
</when>
<!-- 否则查询默认条件(如年龄>18) -->
<otherwise>
age > 18
</otherwise>
</choose>
</where>
</select>
(4)<set>:动态更新字段

<set> 标签会自动去除更新语句中多余的逗号,避免语法错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<update id="updateUserDynamic">
UPDATE t_user
<set>
<if test="username != null and username != ''">
user_name = #{username}, <!-- 逗号可保留,<set> 自动去除多余的 -->
</if>
<if test="age != null">
age = #{age},
</if>
<if test="deptId != null">
dept_id = #{deptId}
</if>
</set>
WHERE id = #{id}
</update>
(5)<foreach>:遍历集合

用于批量操作(如 IN 条件、批量插入),已在 “批量操作” 部分示例,此处补充动态表名场景:

1
2
3
4
5
6
7
<!-- 动态查询多个表的数据(需确保表结构一致) -->
<select id="getAllDataFromTables" resultType="Map">
SELECT * FROM
<foreach collection="tableNames" item="table" separator="UNION ALL">
(SELECT * FROM #{table}) <!-- 注意:表名需用 ${},此处用 #{} 仅为示例,实际需用 ${table} -->
</foreach>
</select>
(6)<trim>:自定义前缀 / 后缀

<trim> 是更灵活的标签,可自定义添加前缀、后缀,或去除多余字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 替代 <where> 标签:添加 WHERE 前缀,去除多余的 AND/OR -->
<trim prefix="WHERE" prefixOverrides="AND | OR">
<if test="username != null">
AND user_name = #{username}
</if>
</trim>

<!-- 替代 <set> 标签:添加 SET 前缀,去除多余的逗号 -->
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">
user_name = #{username},
</if>
</trim>

SQL 片段:复用重复 SQL

对于频繁使用的 SQL 片段(如字段列表、条件语句),可通过 <sql> 标签定义为片段,再用 <include> 引用,减少重复代码,提升维护性。

基础用法:复用字段列表

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 1. 定义 SQL 片段:id 为唯一标识 -->
<sql id="UserColumnList">
id, user_name AS userName, age, dept_id AS deptId
</sql>

<!-- 2. 引用 SQL 片段 -->
<select id="getUserById" resultType="User">
SELECT <include refid="UserColumnList"/> FROM t_user WHERE id = #{id}
</select>

<select id="getAllUsers" resultType="User">
SELECT <include refid="UserColumnList"/> FROM t_user
</select>

进阶:带参数的 SQL 片段

通过 <property> 标签给 SQL 片段传递参数,实现动态复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1. 定义带参数的 SQL 片段 -->
<sql id="DynamicColumnList">
<if test="includeDeptId != null and includeDeptId">
id, user_name AS userName, dept_id AS deptId
</if>
<if test="includeDeptId == null or !includeDeptId">
id, user_name AS userName, age
</if>
</sql>

<!-- 2. 引用时传递参数 -->
<select id="getUserWithParam" resultType="User">
SELECT
<include refid="DynamicColumnList">
<!-- 传递参数:控制是否包含 dept_id 字段 -->
<property name="includeDeptId" value="true"/>
</include>
FROM t_user WHERE id = #{id}
</select>

缓存配置:提升查询性能

MyBatis 提供两级缓存:一级缓存(Session 级,默认开启)二级缓存(Mapper 级,需手动开启),映射文件中主要配置二级缓存。

二级缓存配置(Mapper 级)

在映射文件中添加 <cache> 标签,开启当前 Mapper 的二级缓存:

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
<!-- 
eviction:缓存回收策略(默认 LRU)
- LRU:最近最少使用,移除最长时间未使用的缓存
- FIFO:先进先出,按缓存顺序移除
flushInterval:缓存刷新间隔(毫秒,默认不清空)
readOnly:是否只读(默认 false)
- true:缓存引用,速度快但不安全
- false:序列化克隆,安全但速度慢
size:缓存最大元素数(默认 1024)
-->
<cache
eviction="LRU"
flushInterval="60000"
readOnly="false"
size="1024"/>

<!-- 单个查询是否使用缓存(默认 true) -->
<select id="getUserById" resultType="User" useCache="true">
SELECT * FROM t_user WHERE id = #{id}
</select>

<!-- 增删改操作是否清空缓存(默认 true,确保数据一致性) -->
<insert id="insertUser" flushCache="true">
INSERT INTO t_user (user_name, age) VALUES (#{userName}, #{age})
</insert>

缓存引用(cache-ref)

多个 Mapper 可共享同一缓存,通过 <cache-ref> 引用其他 Mapper 的缓存配置:

1
2
3
4
5
<!-- UserMapper.xml 定义缓存 -->
<cache id="UserCache" eviction="LRU" size="1024"/>

<!-- OrderMapper.xml 引用 UserMapper 的缓存 -->
<cache-ref namespace="com.example.mapper.UserMapper"/>

注意事项

  • 二级缓存仅缓存可序列化的 POJO(需实现 Serializable 接口);
  • 增删改操作会自动清空缓存(flushCache="true"),确保数据一致性;
  • 分布式环境下,二级缓存可能导致数据不一致,需集成 Redis 等分布式缓存(MyBatis 提供 Cache 接口,可自定义 Redis 缓存实现)。

工程实践最佳实践

映射文件命名规范

  • 文件名:与 Mapper 接口同名,如 UserMapper.java 对应 UserMapper.xml
  • 路径:与 Mapper 接口同包,如 src/main/java/com/example/mapper/UserMapper.javasrc/main/resources/com/example/mapper/UserMapper.xml(Spring Boot 需配置 mybatis.mapper-locations 指向资源路径)。

SQL 书写规范

  • 避免 SELECT *,明确指定字段(减少数据传输,避免字段新增导致的映射错误);
  • 关键字大写(如 SELECTFROMWHERE),提升可读性;
  • 复杂 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;
  • 性能优化:批量操作、延迟加载、缓存配置是关键

欢迎关注我的其它发布渠道