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 里拼接字符串。常用标签有六类,它们的分工可以用下图表示:

动态 SQL 标签分工与组合关系 参数对象 UserQuery 条件判断 <if test="..."> <choose> / <when> / <otherwise> 拼接控制 <where> / <set> / <trim> 去除多余 AND / 逗号 集合遍历 <foreach> IN / 批量插入 SQL 片段复用 <sql id="baseColumns"> ... </sql> → <include refid="baseColumns"/> 最终执行的 SQL SELECT ... FROM ... WHERE ...

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 注入风险,只应用于列名、表名等可控标识符;参数值一律使用 #{} 预编译占位。
动态 SQL 的 test 表达式里,字符串比较建议写成 test="type == 'A'",单引号内是字符,双引号包裹整个属性;若写成 type == "A" 在部分版本会因 OGNL 解析差异出错。

把 resultMap、动态标签和 include 组合起来,一条查询就能同时满足“列名映射正确、条件可选、集合可遍历、片段可复用”四个目标。实际开发中建议先写静态 SQL 验证结果,再逐步加入 if/where/foreach,每加一层都用日志确认最终 SQL 与参数是否符合预期。