MyBatis 是什么:和 JPA 的取舍

Java 生态操作数据库有两大流派:ORM 全自动(JPA/Hibernate)和SQL 半自动(MyBatis)。JPA 让你用注解和方法名自动生成 SQL,适合快速开发简单 CRUD;MyBatis 让你自己写 SQL,框架只帮你做参数绑定和结果映射——适合复杂查询、性能敏感、SQL 需要 DBA 审核的场景。

国内互联网公司选 MyBatis 的原因很现实:SQL 可控、易优化、出问题能直接拿到 SQL 文本去数据库执行排查。JPA 生成的 SQL 是黑盒,一旦慢查询出现,你得先猜它生成了什么。MyBatis 没有这个问题——SQL 就在 XML 里,DBA 一眼能看。

MyBatis 不是 ORM,而是 SQL Mapping Framework。它不替你决定 SQL 怎么写,只帮你把 Java 对象和 SQL 参数、结果集之间的胶水代码省掉。
本文核心知识点
  • Mapper接口定义方法,XML 写对应 SQL,框架动态生成实现。
  • #{}预编译参数绑定,防 SQL 注入。
  • 动态 SQL用 if/where/foreach 标签拼条件,告别字符串拼接。
  • resultMap自定义结果集到 Java 对象的映射规则。
MyBatis 示意:蓝色渐变背景中央圆柱形数据库,左侧方块形输入框通过发光管线汇入数据库,右侧网格面板接收输出
MyBatis 是中间的桥梁:左边 Java 对象进来,SQL 在中间组装,右边结果集映射成 Java 对象回去。

Mapper 接口:不用写实现类的 DAO

传统 JDBC 时代,写一个 DAO 类要写满 ResultSet 遍历、异常处理、资源关闭。MyBatis 的 Mapper 模式把这些全部干掉:你只需要定义一个接口,MyBatis 在运行时动态生成实现类。

// UserMapper.java —— 一个接口就够了
public interface UserMapper {
    User selectById(Long id);
    List<User> selectAll();
    void insert(User user);
}

这个接口没有任何实现类。MyBatis 会在启动时扫描所有 Mapper 接口,为每个方法找对应的 XML 语句,动态生成代理对象。你注入这个接口就能直接调用方法。

方式优点缺点
XML 映射SQL 和代码分离,复杂查询清晰多一个 XML 文件要维护
注解 @Select零文件,简单查询直接写在方法上动态 SQL 写起来很丑

实践中推荐:简单查询用注解,复杂动态 SQL 用 XML。Spring Boot 项目里加 @MapperScan("com.example.mapper") 就能扫描所有 Mapper 接口。

XML 映射:写 SQL 的正确姿势

XML 映射文件和 Mapper 接口同名同包(放在 resources/mapper/ 下也可以,配好路径就行)。namespace 指向接口全限定名,每个 statement 的 id 对应方法名。

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">

  <select id="selectById" resultType="User">
    SELECT id, name, email, created_at
    FROM user
    WHERE id = #{id}
  </select>

  <insert id="insert" parameterType="User">
    INSERT INTO user (name, email)
    VALUES (#{name}, #{email})
  </insert>

</mapper>

关键细节:#{id} 是预编译参数占位符——MyBatis 会把它替换成 ?,用 PreparedStatement 绑定值,天然防 SQL 注入。如果你写 ${id},那是字符串直接拼接——只有在动态表名或排序字段这种场景才用,普通参数永远用 #{}。

resultType="User" 告诉 MyBatis 把查询结果每行映射成 User 对象。前提是 SQL 查询的列名和 User 的属性名一致;如果不一致(比如数据库列叫 created_at,Java 属性叫 createdAt),要么起别名 AS createdAt,要么用 resultMap 显式映射。

动态 SQL:if、where、foreach

这是 MyBatis 最强大的部分。业务里经常有"可选查询条件":用户可能按名字搜、可能按邮箱搜、可能都搜。传统做法是 Java 里拼 SQL 字符串,一堆 if else,拼接出 WHERE 条件。MyBatis 的动态 SQL 标签让这件事在 XML 里声明式完成。

<select id="search" resultType="User">
  SELECT id, name, email FROM user
  <where>
    <if test="name != null">
      AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </where>
  ORDER BY id DESC
</select>

<where> 标签会自动处理:如果内部所有条件都不满足,就不生成 WHERE 子句;如果有条件,自动去掉开头多余的 AND。<if test="name != null"> 判断参数是否为空,满足才拼接这段 SQL。

另一个高频标签是 <foreach>,用于 IN 查询:

<select id="selectByIds" resultType="User">
  SELECT * FROM user WHERE id IN
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

传入 List<Long> ids,MyBatis 自动展开成 IN (1, 2, 3)。批量删除、批量查询都是这个模式。

结果映射:一对一与一对多

查出来的数据经常不是单表——一个用户有多个订单,一个订单属于一个用户。MyBatis 的 resultMap 可以定义嵌套映射,一次查询把关联对象也组装好。

<resultMap id="OrderWithUserMap" type="Order">
  <id property="id" column="order_id"/>
  <result property="amount" column="amount"/>
  <association property="user" javaType="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
  </association>
</resultMap>

<select id="selectOrderDetail" resultMap="OrderWithUserMap">
  SELECT o.id AS order_id, o.amount, u.id AS user_id, u.name AS user_name
  FROM orders o JOIN user u ON o.user_id = u.id
  WHERE o.id = #{orderId}
</select>

<association> 处理一对一关联,<collection> 处理一对多。这样查出来的 Order 对象里直接就有 user 属性,业务代码不用再手动组装。