MyBatis 标签全览
MyBatis 最核心的设计哲学是:SQL 归你写,动态拼接归框架做。mapper XML 里的那些标签,本质上就是一套 SQL 模板语言,把条件判断、循环、复用这些逻辑从 Java 代码里剥离出来,放到离 SQL 最近的地方。
一、顶层结构标签
这几个标签定义了一个 mapper 文件的骨架,缺一不可。
<mapper>
<mapper namespace="com.example.mapper.UserMapper">
<!-- 所有 SQL 语句写在这里 -->
</mapper>
namespace 必须和对应的 Mapper 接口全限定名完全一致,MyBatis 靠这个做接口方法和 SQL 语句的绑定。
<select> / <insert> / <update> / <delete>
四个 CRUD 标签,分别对应四种 SQL 操作。常用属性:
| 属性 | 说明 |
|---|---|
id | 对应接口方法名,同一 namespace 内唯一 |
resultType | 返回值类型,写全限定名或别名 |
resultMap | 指向 <resultMap>,处理复杂映射 |
parameterType | 参数类型,大多数情况 MyBatis 能自动推断,可省略 |
useGeneratedKeys | true 时自动将主键写回入参对象 |
keyProperty | 配合 useGeneratedKeys 指定接收主键的字段名 |
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>
<select id="findById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
二、结果映射标签
当数据库列名和 Java 字段名不一致,或者涉及关联查询时,<resultMap> 是唯一正解。
<resultMap>
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="email" column="email"/>
</resultMap>
<id>标记主键列,对缓存和性能有影响,不要随便用<result>替代<result>映射普通列type是目标 Java 类型
<association> — 一对一关联
查询用户同时带出地址信息:
<resultMap id="userWithAddress" type="User">
<id property="id" column="user_id"/>
<result property="name" column="name"/>
<association property="address" javaType="Address">
<id property="id" column="addr_id"/>
<result property="city" column="city"/>
<result property="street" column="street"/>
</association>
</resultMap>
也可以用 select 属性做懒加载(N+1 要注意):
<association property="address"
select="com.example.mapper.AddressMapper.findByUserId"
column="id"
fetchType="lazy"/>
<collection> — 一对多关联
一个订单包含多个订单项:
<resultMap id="orderResultMap" type="Order">
<id property="id" column="order_id"/>
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<result property="productName" column="product_name"/>
<result property="quantity" column="quantity"/>
</collection>
</resultMap>
<discriminator> — 鉴别器
根据某个列的值决定用哪个子 resultMap,适合继承体系的实体:
<resultMap id="vehicleMap" type="Vehicle">
<id property="id" column="id"/>
<discriminator javaType="String" column="vehicle_type">
<case value="CAR" resultType="Car"/>
<case value="TRUCK" resultType="Truck"/>
</discriminator>
</resultMap>
三、动态 SQL 标签
这是 MyBatis 最精华的部分,解决了”根据参数动态拼 SQL”这个在 JDBC 时代极度痛苦的问题。
<if> — 条件判断
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
test 里写的是 OGNL 表达式,直接访问参数对象的属性,不用加 $ 或 #。
<where> — 智能 WHERE 子句
上面用 WHERE 1=1 的写法比较老土,<where> 可以更优雅地处理:
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</select>
<where> 会在有子句时自动加上 WHERE,并自动去掉开头多余的 AND 或 OR,不用操心前缀问题。
<set> — 智能 SET 子句
UPDATE 语句的好搭档,自动去掉末尾多余的逗号:
<update id="updateUser">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<choose> / <when> / <otherwise> — 多分支选择
相当于 Java 的 switch-case,只有第一个匹配的 <when> 会生效:
<select id="findByPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="name != null">
AND name = #{name}
</when>
<when test="email != null">
AND email = #{email}
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
<foreach> — 遍历集合
处理 IN 查询或批量插入的核心标签:
<!-- IN 查询 -->
<select id="findByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user (name, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
| 属性 | 说明 |
|---|---|
collection | 集合参数名,List 传 list,数组传 array,Map 传 key 名 |
item | 每次迭代的元素变量名 |
index | 当前索引(List 是下标,Map 是 key) |
open / close | 开头和结尾的字符 |
separator | 元素间的分隔符 |
注意:批量插入时单次插入量不要太大,建议每批 500~1000 条,否则会撑大 SQL 包,MySQL 默认
max_allowed_packet是 4MB。
<trim> — 通用前后缀处理
<where> 和 <set> 本质上是 <trim> 的语法糖:
<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>
<trim> 给了你完全的控制权,适合非标准的场景。
四、SQL 复用标签
<sql> 与 <include>
把可复用的 SQL 片段提取出来,避免重复:
<sql id="userColumns">
id, name, email, status, created_at
</sql>
<select id="findAll" resultType="User">
SELECT <include refid="userColumns"/>
FROM user
</select>
<select id="findById" resultType="User">
SELECT <include refid="userColumns"/>
FROM user WHERE id = #{id}
</select>
<include> 还支持传参(通过 <property>),配合 ${xxx} 插值实现更灵活的复用:
<sql id="tableName">${prefix}_user</sql>
<select id="findAll" resultType="User">
SELECT * FROM <include refid="tableName">
<property name="prefix" value="dev"/>
</include>
</select>
五、参数引用:#{} vs ${}
这个问题在面试里出现的频率极高。
#{} | ${} | |
|---|---|---|
| 底层实现 | PreparedStatement 占位符 ? | 字符串直接拼接 |
| SQL 注入风险 | 无(参数被转义) | 有(原样拼入 SQL) |
| 适用场景 | 绝大多数参数值 | 表名、列名、ORDER BY 字段等动态结构 |
<!-- 安全:用户输入的值一律用 #{} -->
SELECT * FROM user WHERE name = #{name}
<!-- 必须用 ${}:表名是运行时决定的 -->
SELECT * FROM ${tableName} WHERE id = #{id}
<!-- ORDER BY 字段名必须用 ${},但要在 Java 层做白名单校验! -->
SELECT * FROM user ORDER BY ${sortField} ${sortOrder}
如果在业务代码里收到前端传来的
sortField,一定要先用白名单校验,确认它是合法列名,再传给 MyBatis,否则有 SQL 注入风险。
六、缓存相关标签
<cache>
开启 mapper 级别的二级缓存:
<cache eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
实际项目里,MyBatis 自带的二级缓存局限性较大(多表 join 时缓存失效粒度粗、集群环境下缓存不共享),通常建议在 Service 层自己用 Redis 做缓存,关掉 MyBatis 二级缓存以免产生脏数据。
<cache-ref>
让当前 mapper 共享另一个 mapper 的缓存:
<cache-ref namespace="com.example.mapper.UserMapper"/>
七、一个完整例子串联所有标签
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 复用的列片段 -->
<sql id="orderColumns">
o.id, o.user_id, o.total_amount, o.status, o.created_at
</sql>
<!-- 复杂结果映射 -->
<resultMap id="orderDetail" type="Order">
<id property="id" column="id"/>
<result property="totalAmount" column="total_amount"/>
<result property="status" column="status"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<result property="productName" column="product_name"/>
<result property="quantity" column="quantity"/>
</collection>
</resultMap>
<!-- 动态查询 -->
<select id="queryOrders" resultMap="orderDetail">
SELECT
<include refid="orderColumns"/>,
u.name AS user_name,
i.id AS item_id, i.product_name, i.quantity
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item i ON o.id = i.order_id
<where>
<if test="userId != null">AND o.user_id = #{userId}</if>
<if test="status != null">AND o.status = #{status}</if>
<if test="startTime != null">AND o.created_at >= #{startTime}</if>
<if test="endTime != null">AND o.created_at <= #{endTime}</if>
</where>
ORDER BY o.created_at DESC
</select>
<!-- 批量更新状态 -->
<update id="batchUpdateStatus">
UPDATE orders SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
注意 XML 里的
>和<要用转义字符>和<,或者把整段 SQL 包在<![CDATA[ ... ]]>里。
小结
MyBatis 的标签体系设计得很克制,没有过度封装,核心逻辑就这几层:
- CRUD 四件套 定义语句
- resultMap 家族 处理映射
- 动态 SQL 六标签(if / where / set / choose / foreach / trim)处理条件拼接
- sql + include 处理复用
#{}做参数,${}做结构
