MyBatis XML 映射
本章定位 :掌握 MyBatis 的 XML Mapper 结构——resultMap、CRUD 标签、动态 SQL(if/choose/foreach)、#{} 与 ${} 的区别和安全性。
定义与作用
MyBatis 是 Java 中最流行的 ORM 半自动化框架。它的核心是 Mapper XML——将 SQL 语句与 Java 接口方法绑定的 XML 配置文件。
与全自动 ORM(如 Hibernate)不同,MyBatis 要求你 手写 SQL ,但帮你处理了参数映射、结果映射、动态 SQL 拼接等繁琐工作。这种"半自动"方式让你对 SQL 有完全的控制权,同时避免了 JDBC 的手工 setParameter/getResultSet。
核心原理:MyBatis 执行流程
图解释 :MyBatis 的执行流程是:①根据接口方法名找到对应的 Mapper XML SQL 语句;②将方法参数替换到 #{} 占位符(生成 PreparedStatement);③执行 SQL;④根据 resultMap 将 ResultSet 映射为 Java 对象。
语法/结构要点
Mapper XML 骨架
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feixiang.edu.mapper.StudentMapper">
<!-- resultMap:自定义结果映射 -->
<resultMap id="studentMap" type="Student">
<id property="id" column="student_id"/>
<result property="name" column="student_name"/>
<result property="gpa" column="grade_point_avg"/>
</resultMap>
<!-- CRUD -->
<select id="findById" resultMap="studentMap">
SELECT * FROM student WHERE student_id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student (student_name, grade_point_avg)
VALUES (#{name}, #{gpa})
</insert>
<update id="update">
UPDATE student SET student_name = #{name} WHERE student_id = #{id}
</update>
<delete id="delete">
DELETE FROM student WHERE student_id = #{id}
</delete>
</mapper>
动态 SQL 标签
| 标签 | 作用 | 示例 |
|---|---|---|
if | 条件判断 | <if test="name != null">AND name = #{name}</if> |
choose/when/otherwise | 多路分支 | 优先级排序 |
where | 智能添加/去除 WHERE | 自动处理前导 AND/OR |
set | 智能添加 SET | 自动处理后缀逗号 |
foreach | 遍历集合 | IN (#{item}, #{item}, ...) |
trim | 自定义前缀/后缀/覆盖 | 更灵活的 where/set |
完整示例:小崔用动态 SQL 写教学管理查询
场景说明
飞翔科技的后端 小崔 要实现学生搜索功能——支持按姓名、专业、GPA 范围组合查询,条件全部可选。如果用拼接字符串的方式,需要几十行的 if-else,他用 MyBatis 动态 SQL 优雅解决。
Mapper XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feixiang.edu.mapper.StudentMapper">
<resultMap id="studentMap" type="com.feixiang.edu.domain.Student">
<id property="id" column="student_id"/>
<result property="name" column="student_name"/>
<result property="major" column="major"/>
<result property="gpa" column="grade_point_avg"/>
<result property="enrollYear" column="enroll_year"/>
</resultMap>
<!-- 基础查询:按 ID -->
<select id="findById" resultMap="studentMap">
SELECT student_id, student_name, major, grade_point_avg, enroll_year
FROM student
WHERE student_id = #{id}
</select>
<!-- 动态条件搜索 -->
<select id="search" resultMap="studentMap">
SELECT student_id, student_name, major, grade_point_avg, enroll_year
FROM student
<where>
<if test="name != null and name != ''">
AND student_name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="major != null and major != ''">
AND major = #{major}
</if>
<if test="minGpa != null">
AND grade_point_avg >= #{minGpa}
</if>
<if test="maxGpa != null">
AND grade_point_avg <= #{maxGpa}
</if>
<if test="enrollYear != null">
AND enroll_year = #{enrollYear}
</if>
</where>
<if test="orderBy != null">
ORDER BY ${orderBy}
<if test="desc">
DESC
</if>
</if>
</select>
<!-- 批量删除:foreach -->
<delete id="batchDelete">
DELETE FROM student
WHERE student_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<!-- 动态更新:set -->
<update id="updateSelective">
UPDATE student
<set>
<if test="name != null">student_name = #{name},</if>
<if test="major != null">major = #{major},</if>
<if test="gpa != null">grade_point_avg = #{gpa},</if>
</set>
WHERE student_id = #{id}
</update>
<!-- 多路排序 -->
<select id="listByPriority" resultMap="studentMap">
SELECT * FROM student
<where>
<choose>
<when test="priority == 'high_gpa'">
grade_point_avg >= 3.5
</when>
<when test="priority == 'new_student'">
enroll_year = #{currentYear}
</when>
<otherwise>
1 = 1
</otherwise>
</choose>
</where>
</select>
</mapper>
Java 接口
public interface StudentMapper {
Student findById(@Param("id") Long id);
List<Student> search(
@Param("name") String name,
@Param("major") String major,
@Param("minGpa") Double minGpa,
@Param("maxGpa") Double maxGpa,
@Param("enrollYear") Integer enrollYear,
@Param("orderBy") String orderBy,
@Param("desc") Boolean desc
);
int batchDelete(@Param("ids") List<Long> ids);
}
操作结果
// 搜索:CS 专业 + GPA ≥ 3.0
List<Student> result = mapper.search(
null, "CS", 3.0, null, null, "grade_point_avg", true
);
// 生成 SQL:
// SELECT ... FROM student WHERE major = 'CS' AND grade_point_avg >= 3.0
// ORDER BY grade_point_avg DESC
// 只传姓名:模糊搜索
List<Student> result2 = mapper.search("小", null, null, null, null, null, null);
// 生成 SQL:
// SELECT ... FROM student WHERE student_name LIKE '%小%'
动态 SQL 自动处理了 WHERE 子句中的前导 AND——如果所有条件都不满足,<where> 完全消失,不会产生 WHERE 空子句。
易错场景
错误一:#{} 和 ${} 混淆
#{}:预编译占位符(PreparedStatement),安全,自动加引号${}:字符串替换(Statement),有 SQL 注入风险, 仅用于ORDER BY/表名/列名等动态标识符
<!-- ❌ 危险:${orderBy} 可能注入 DROP TABLE -->
SELECT * FROM student ORDER BY ${orderBy}
<!-- ⚠️ 必须用 ${} 时,在 Java 层做白名单校验 -->
错误二:<if test="name == '小崔'"> 中字符串比较
MyBatis 的 OGNL 表达式中,字符串比较必须用双引号包裹:
<!-- ❌ -->
<if test="major == 'CS'">
<!-- ✅ -->
<if test='major == "CS"'>
面试考点
| 考点 | 参考答案要点 |
|---|---|
MyBatis 中 #{} 和 ${} 的区别? | #{} 是预编译占位符(PreparedStatement),安全防注入;${} 是字符串直接替换,可能被注入,仅用于动态表名/列名/ORDER BY |
| resultMap 和 resultType 的区别? | resultType 自动映射(要求列名=属性名);resultMap 手动定义列→属性的映射规则,处理复杂映射(关联/集合/继承) |
| MyBatis 动态 SQL 有哪些常用标签? | if(条件判断)、choose/when/otherwise(多路分支)、where(智能 WHERE)、set(智能 SET)、foreach(遍历集合)、trim(自定义裁剪) |