4. MyBatis映射文件与动态SQL
Mapper XML 是 MyBatis 中承载 SQL 与结果映射的核心文件。它把 Java 接口方法与具体 SQL 语句绑定起来,同时负责把 ResultSet 的列转换成 Java 对象。理解它的基本结构、resultMap 的映射规则以及动态 SQL 标签的组合方式,是写出可维护数据访问层的关键。
一、Mapper XML 的基本结构
一个标准的 Mapper XML 由 <?xml> 声明、DTD 约束和根元素 <mapper> 组成。根元素的 namespace 必须指向对应的 Java 接口全限定名,MyBatis 通过它把接口方法与语句 id 关联起来。常见的顶层元素包括:
<resultMap>:定义结果集到对象的映射规则,可被多条语句复用。<sql>:定义可复用的 SQL 片段,通过<include>引入。<select>、<insert>、<update>、<delete>:四类 DML 语句。
每条语句的 id 必须与接口方法名一致,parameterType 可省略(MyBatis 能自动推断),resultType 或 resultMap 二选一。下面是一个最小可用的骨架:
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
</resultMap>
<select id="selectById" resultMap="userMap">
SELECT id, user_name FROM t_user WHERE id = #{id}
</select>
</mapper>
二、resultMap 映射规则
当数据库列名与 Java 属性名不一致,或需要处理关联对象、集合时,resultType 的自动映射就不够用了,此时应使用 resultMap。它的核心子元素有三类:
<id>:标记主键列,MyBatis 用它区分结果行,对嵌套映射尤其重要。<result>:普通列到属性的映射,column写列名,property写属性名。<association>与<collection>:分别处理一对一和一对多关联。
例如用户与部门是一对一关系,可以在 resultMap 中嵌套 association:
<resultMap id="userWithDept" type="User">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<association property="dept" javaType="Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
</resultMap>
需要注意:如果开启了 autoMappingBehavior=PARTIAL(默认),未显式映射的同名列会自动填充;但一旦使用 association/collection,建议把关键列都显式写出,避免因列名重复导致映射错位。
三、动态 SQL 标签
动态 SQL 让一条语句能根据参数组合出不同 SQL,避免在 Java 里拼接字符串。常用标签有六类,它们的分工可以用下图表示:
if 是最基础的判断标签,test 属性写 OGNL 表达式。例如按用户名模糊查询:
<select id="search" resultMap="userMap">
SELECT id, user_name FROM t_user
<where>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
where 会自动去掉开头多余的 AND 或 OR;如果所有 if 都不成立,它不会输出 WHERE 关键字。set 用于 update,自动去掉末尾多余的逗号:
<update id="updateSelective">
UPDATE t_user
<set>
<if test="userName != null">user_name = #{userName},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
choose/when/otherwise 相当于 Java 的 switch,多个条件只命中一个。例如排序字段只允许白名单之一:
<choose>
<when test="orderBy == 'name'">ORDER BY user_name</when>
<when test="orderBy == 'time'">ORDER BY create_time DESC</when>
<otherwise>ORDER BY id</otherwise>
</choose>
foreach 用于遍历集合,常见于 IN 查询和批量插入。collection 指定集合名,item 是元素别名,open/close/separator 控制包裹与分隔符:
<select id="selectByIds" resultMap="userMap">
SELECT id, user_name FROM t_user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
trim 是 where 和 set 的底层实现,通过 prefix、suffix、prefixOverrides、suffixOverrides 自定义裁剪规则。例如用 trim 实现 where 的等价写法:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="status != null">AND status = #{status}</if>
</trim>
四、SQL 片段复用与 include
当多个语句共享同一段列清单或条件时,用 <sql> 抽取,再用 <include> 引入,可显著降低维护成本。include 还支持 <property> 传参,让片段内部使用 ${alias} 占位:
<sql id="baseColumns">
${alias}.id, ${alias}.user_name, ${alias}.status
</sql>
<select id="selectAll" resultMap="userMap">
SELECT <include refid="baseColumns">
<property name="alias" value="u"/>
</include>
FROM t_user u
</select>
${} 时是字符串直接替换,存在 SQL 注入风险,只应用于列名、表名等可控标识符;参数值一律使用 #{} 预编译占位。test 表达式里,字符串比较建议写成 test="type == 'A'",单引号内是字符,双引号包裹整个属性;若写成 type == "A" 在部分版本会因 OGNL 解析差异出错。把 resultMap、动态标签和 include 组合起来,一条查询就能同时满足“列名映射正确、条件可选、集合可遍历、片段可复用”四个目标。实际开发中建议先写静态 SQL 验证结果,再逐步加入 if/where/foreach,每加一层都用日志确认最终 SQL 与参数是否符合预期。