MyBatis XML Mapper
上一篇是 MyBatis 的 CRUD 基础。这篇讲 XML Mapper 的三个高级武器:动态 SQL(对应 Eloquent 的条件拼接)、结果映射(对应模型关联)、关联查询。
动态 SQL:<if>、<where>、<foreach>
Laravel 里你写:
php
$query = Post::query();
if ($keyword) $query->where('title', 'like', "%$keyword%");
if ($status !== null) $query->where('status', $status);
return $query->get();MyBatis 的动态 SQL 写在 XML 里,效果一样但更「声明式」:
xml
<select id="search" resultType="Post">
SELECT * FROM posts
<where> <!-- 自动处理 AND/WHERE -->
<if test="keyword != null and keyword != ''">
AND title LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
</where>
ORDER BY id DESC
</select><where> 标签的妙处:子句都为空时它输出 WHERE 1=1 都不需要,首个子句开头的 AND 会自动去掉。
<foreach>:批量操作
对应 Eloquent 的 whereIn:
xml
<select id="findByIds" resultType="Post">
SELECT * FROM posts WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>java
List<Post> findByIds(@Param("ids") List<Long> ids);批量插入:
xml
<insert id="batchInsert">
INSERT INTO posts (title, content, user_id) VALUES
<foreach collection="list" item="p" separator=",">
(#{p.title}, #{p.content}, #{p.userId})
</foreach>
</insert><choose>:if / else
xml
<choose>
<when test="status != null">
AND status = #{status}
</when>
<otherwise>
AND status = 1
</otherwise>
</choose>动态 SQL 是 MyBatis 的灵魂
它是「Java 代码拼 SQL」的替代品,把条件逻辑放回 SQL 层,可读性和维护性都更好。<if> + <where> + <foreach> + <choose> 四个标签覆盖 95% 场景。
结果映射:<resultMap>
resultType="Post" 靠「列名 = 字段名」自动映射。复杂情况用 <resultMap> 精确定义:
xml
<resultMap id="postMap" type="Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<result property="userId" column="user_id"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="findById" resultMap="postMap">
SELECT * FROM posts WHERE id = #{id}
</select>TIP
开了 map-underscore-to-camel-case 后,简单映射都不用写 resultMap。只有列名和字段名对不上、或做关联映射时才需要。
关联查询:JOIN + association / collection
对应 Eloquent 的 with('user')(取帖子的同时带上作者):
xml
<resultMap id="postWithUser" type="Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
<select id="findAllWithUser" resultMap="postWithUser">
SELECT p.*, u.username
FROM posts p
JOIN users u ON u.id = p.user_id
ORDER BY p.id DESC
</select><association>:一对一(belongsTo)<collection>:一对多(hasMany)
TIP
JOIN 是 SQL 层自己写,比 JPA 的懒加载直观得多。这是很多人选 MyBatis 的原因:关联查询完全可见、可优化,没有 N+1 的黑魔法。
一对多:collection
xml
<resultMap id="userWithPosts" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="title" column="post_title"/>
</collection>
</resultMap>xml
<select id="findUserWithPosts" resultMap="userWithPosts">
SELECT u.id, u.username, p.id AS post_id, p.title AS post_title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.id = #{id}
</select>分页
手动 LIMIT(配合 分页 里的 PageHelper 更省事):
xml
<select id="findPage" resultType="Post">
SELECT * FROM posts ORDER BY id DESC
</select>java
PageHelper.startPage(page, size);
List<Post> list = postMapper.findPage();
PageInfo<Post> info = new PageInfo<>(list);代码生成:MyBatis Generator / MyBatis-Plus
国内很多项目直接用 MyBatis-Plus,它给 MyBatis 补上了「自动 CRUD + 条件构造器」(类似 Eloquent 的 Query Builder):
java
// MyBatis-Plus:继承 BaseMapper 就有 CRUD
public interface PostMapper extends BaseMapper<Post> {
}
// 条件构造器(≈ Eloquent query)
List<Post> list = postMapper.selectList(
new LambdaQueryWrapper<Post>()
.eq(Post::getStatus, 1)
.like(Post::getTitle, "Spring"));TIP
- 原生 MyBatis:SQL 全手写,掌控力最强
- MyBatis-Plus:国内主流,自带 CRUD 模板 + 分页插件 + 条件构造器,开发效率高 新手建议先学原生 MyBatis(本篇),再按团队技术栈决定要不要上 MyBatis-Plus。
XML Mapper 最佳实践清单
- XML 放
src/main/resources/mapper/,命名与 Mapper 接口一致 namespace写 Mapper 接口全限定名,id写方法名- 动态 SQL 用
<where>/<if>/<foreach>,别在 Java 代码拼字符串 - 简单映射靠
map-underscore-to-camel-case,复杂关联用<resultMap> #{xxx}预编译防注入,永远别用${}拼用户输入(${}直接拼接,有注入风险)