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 对象的映射规则。
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 属性,业务代码不用再手动组装。
分页与批量操作
分页查询是列表页的标配。MyBatis 本身不分页,但配合 PageHelper 插件一行代码搞定:
// Java 代码
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
PageHelper 会自动在 SQL 后面拼 LIMIT,再查一次 count。你不需要写两条 SQL。
批量插入用 <foreach> 在 VALUES 里展开:
<insert id="batchInsert">
INSERT INTO user (name, email) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.name}, #{u.email})
</foreach>
</insert>
批量插入比循环单条 insert 快几十倍——数据库一次事务提交,网络往返只有一次。几千条数据的批量导入必须用这种方式。
Spring Boot 集成与事务
Spring Boot 集成 MyBatis 只需要加一个 starter:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
配置文件里指定数据库连接和 XML 路径就行:
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true 让 created_at 自动映射到 createdAt,省掉一堆 resultMap。
事务管理用 Spring 的 @Transactional 注解:标在 Service 方法上,方法内多个 Mapper 调用要么全部成功,要么全部回滚。不需要手动开连接、提交、回滚——Spring 帮你管。
MyBatis 的核心价值是"SQL 自由"。你保留了写 SQL 的全部控制力,同时省掉了 JDBC 的样板代码。这就是它在 Java 后端长期流行的原因。