本文件覆盖 MyBatis 从入门到高级的基础能力:它解决什么问题、和 JDBC / JPA 的区别、核心运行流程、全局配置、Mapper 注册、参数绑定、基本 CRUD、最小 Demo,以及入门到专家的基础面试题答案。
官方参考:
- MyBatis 3 Configuration: https://mybatis.org/mybatis-3/configuration.html
- MyBatis 3 Mapper XML: https://mybatis.org/mybatis-3/sqlmap-xml.html
1. MyBatis 解决什么问题
MyBatis 是一个半自动 ORM / SQL Mapper 框架。它不试图隐藏 SQL,而是让开发者显式编写 SQL,并负责:
- 创建和管理 JDBC
PreparedStatement。 - 把 Java 参数绑定到 SQL。
- 执行 SQL。
- 把
ResultSet映射为 Java 对象。 - 管理一级缓存、二级缓存、插件拦截、TypeHandler 等机制。
传统 JDBC 代码:
java
String sql = "select id, username, email from users where id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
return user;
}
}
}
MyBatis Mapper:
java
public interface UserMapper {
User selectById(Long id);
}
xml
<select id="selectById" parameterType="long" resultType="com.example.User">
select id, username, email
from users
where id = #{id}
</select>
MyBatis 的价值是减少 JDBC 样板代码,同时保留 SQL 控制权。
2. MyBatis、JDBC、JPA 的区别
| 维度 | JDBC | MyBatis | JPA/Hibernate |
|---|---|---|---|
| SQL 控制 | 完全手写 | 手写 SQL 为主 | 框架生成 SQL 为主 |
| 映射能力 | 手写 ResultSet | XML/注解映射 | Entity 映射 |
| 学习成本 | 低但样板多 | 中等 | 高 |
| 复杂查询 | 可控但繁琐 | 强,可直接优化 SQL | 复杂查询可能绕 |
| 性能可控性 | 高 | 高 | 依赖 ORM 策略 |
| 适用场景 | 小工具/底层 | 业务系统、复杂 SQL | 领域模型驱动、CRUD 多 |
MyBatis 适合:
- SQL 较复杂。
- 报表、搜索、条件查询多。
- 需要精确控制 SQL 性能。
- 团队熟悉数据库。
- 不希望 ORM 自动生成不可控 SQL。
不适合:
- 完全不想写 SQL。
- 领域模型关系复杂且希望 ORM 自动管理对象图。
- 业务几乎都是简单 CRUD 且团队更偏 JPA。
3. MyBatis 核心运行流程
典型流程:
text
Mapper 方法调用
-> MapperProxy
-> MappedStatement
-> SqlSource 生成 BoundSql
-> ParameterHandler 绑定参数
-> StatementHandler 执行 SQL
-> ResultSetHandler 处理结果集
-> TypeHandler 做 Java/JDBC 类型转换
关键概念:
SqlSessionFactory:创建SqlSession的工厂。SqlSession:执行 SQL、获取 Mapper、管理一级缓存的会话对象。Mapper:Java 接口,方法与 SQL statement 对应。MappedStatement:一条 SQL 语句的完整元数据。BoundSql:动态 SQL 解析后的最终 SQL 和参数映射。Executor:执行器,负责查询、更新、缓存等。
4. 最小配置 Demo:纯 MyBatis
目录:
text
mybatis-demo/
├── pom.xml
├── src/main/java/com/example/User.java
├── src/main/java/com/example/UserMapper.java
├── src/main/java/com/example/Main.java
└── src/main/resources/
├── mybatis-config.xml
└── mapper/UserMapper.xml
Maven 依赖:
xml
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
</dependencies>
实体:
java
package com.example;
public class User {
private Long id;
private String username;
private String email;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
Mapper 接口:
java
package com.example;
public interface UserMapper {
User selectById(Long id);
int insert(User user);
}
MyBatis 配置:
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<typeAlias alias="User" type="com.example.User"/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
Mapper XML:
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="selectById" parameterType="long" resultType="User">
select id, username, email
from users
where id = #{id}
</select>
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into users(username, email)
values(#{username}, #{email})
</insert>
</mapper>
启动代码:
java
package com.example;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class Main {
public static void main(String[] args) throws Exception {
try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
try (SqlSession session = factory.openSession()) {
session.update("org.apache.ibatis.jdbc.ScriptRunner"); // 示例中实际应先执行 schema.sql
}
try (SqlSession session = factory.openSession(true)) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = new User();
user.setUsername("ada");
user.setEmail("ada@example.com");
mapper.insert(user);
User loaded = mapper.selectById(user.getId());
System.out.println(loaded.getUsername());
}
}
}
}
说明:真实 Demo 中建议用 H2 初始化脚本或 Spring Boot 自动执行 schema.sql,这里重点展示 MyBatis 最小运行链路。
5. 全局配置结构
官方配置顶层结构包括:
xml
<configuration>
<properties/>
<settings/>
<typeAliases/>
<typeHandlers/>
<objectFactory/>
<plugins/>
<environments/>
<databaseIdProvider/>
<mappers/>
</configuration>
常用配置:
xml
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="false"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="logImpl" value="SLF4J"/>
</settings>
关键含义:
mapUnderscoreToCamelCase:user_name自动映射到userName。cacheEnabled:全局二级缓存开关。lazyLoadingEnabled:延迟加载开关。defaultExecutorType:默认执行器,SIMPLE、REUSE、BATCH。logImpl:日志实现。
6. Mapper XML 的核心元素
Mapper XML 常用元素:
cache:命名空间二级缓存。cache-ref:引用其他 namespace 缓存。resultMap:复杂结果映射。sql:可复用 SQL 片段。select:查询。insert:插入。update:更新。delete:删除。
示例:
xml
<mapper namespace="com.example.UserMapper">
<sql id="BaseColumns">
id, username, email, created_at
</sql>
<select id="selectAll" resultType="User">
select <include refid="BaseColumns"/>
from users
</select>
</mapper>
7. #{} 与 ${} 的区别
#{} 使用 PreparedStatement 参数绑定,安全,防 SQL 注入:
xml
where username = #{username}
${} 是字符串拼接,危险:
xml
order by ${sort}
#{} 会生成:
sql
where username = ?
${} 会直接拼接:
sql
order by created_at desc
专家实践:业务值必须使用 #{}。只有列名、排序方向、表名等无法参数化的位置才考虑 ${},并必须做白名单校验。
白名单示例:
java
public enum UserSortField {
ID("id"),
CREATED_AT("created_at");
private final String column;
UserSortField(String column) {
this.column = column;
}
public String column() {
return column;
}
}
XML:
xml
order by ${sortField.column} ${sortDirection}
其中 sortDirection 也必须由枚举或白名单生成。
8. 参数绑定
单参数:
java
User selectById(Long id);
xml
where id = #{id}
多参数建议使用 @Param:
java
List<User> selectByStatusAndKeyword(
@Param("status") String status,
@Param("keyword") String keyword
);
xml
where status = #{status}
and username like concat('%', #{keyword}, '%')
对象参数:
java
int insert(User user);
xml
values(#{username}, #{email})
Map 参数可用但不推荐在复杂业务中过度使用,因为类型不安全。
9. CRUD 最小 Demo
Mapper:
java
public interface ProductMapper {
Product selectById(Long id);
List<Product> selectAll();
int insert(Product product);
int update(Product product);
int deleteById(Long id);
}
XML:
xml
<mapper namespace="com.example.ProductMapper">
<select id="selectById" resultType="Product">
select id, name, price, stock
from products
where id = #{id}
</select>
<select id="selectAll" resultType="Product">
select id, name, price, stock
from products
order by id desc
</select>
<insert id="insert" parameterType="Product" useGeneratedKeys="true" keyProperty="id">
insert into products(name, price, stock)
values(#{name}, #{price}, #{stock})
</insert>
<update id="update" parameterType="Product">
update products
set name = #{name},
price = #{price},
stock = #{stock}
where id = #{id}
</update>
<delete id="deleteById">
delete from products
where id = #{id}
</delete>
</mapper>
10. 注解 Mapper
简单 SQL 可用注解:
java
public interface UserMapper {
@Select("select id, username, email from users where id = #{id}")
User selectById(Long id);
@Insert("""
insert into users(username, email)
values(#{username}, #{email})
""")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
}
注解适合:
- SQL 很短。
- 查询不复杂。
- Demo 或简单 CRUD。
XML 适合:
- 动态 SQL 多。
- ResultMap 复杂。
- SQL 需要审查和调优。
- 团队希望 SQL 与 Java 分离。
11. 一级缓存
一级缓存是 SqlSession 级别缓存,默认开启。
java
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User a = mapper.selectById(1L);
User b = mapper.selectById(1L);
}
同一个 SqlSession 中,两次相同查询可能命中一级缓存。
注意:
insert/update/delete会清理缓存。- 不同
SqlSession不共享一级缓存。 - Spring 集成中 SqlSession 通常由事务边界管理。
12. 入门到专家知识点清单
入门:
- MyBatis 定位。
- Mapper 接口。
- Mapper XML。
#{}参数绑定。- 基础 CRUD。
SqlSessionFactory/SqlSession。
进阶:
- 全局配置。
@Param。- 注解 Mapper。
- SQL 片段。
- 一级缓存。
- XML 与注解取舍。
高级:
MappedStatement。BoundSql。Executor。ParameterHandler。ResultSetHandler。- TypeHandler。
精通:
- Spring Boot 集成。
- 事务边界。
- 批量执行。
- 插件机制。
- 二级缓存。
- SQL 性能调优。
专家:
- Mapper 分层治理。
- SQL 安全策略。
- 多数据源。
- 读写分离。
- 分库分表边界。
- 数据访问层架构设计。
13. 面试题与完整答案
13.1 MyBatis 是 ORM 吗?
MyBatis 通常被称为半自动 ORM 或 SQL Mapper。它不像 Hibernate 那样自动生成大部分 SQL,而是由开发者显式编写 SQL,MyBatis 负责参数绑定、执行、结果映射、缓存和扩展。它更强调 SQL 可控性。
13.2 MyBatis 和 JDBC 的区别是什么?
JDBC 需要手写连接、Statement、参数绑定、ResultSet 映射、异常处理和资源释放。MyBatis 封装了这些样板代码,让开发者专注 SQL 和映射关系。同时 MyBatis 提供 Mapper 接口、动态 SQL、ResultMap、缓存和插件机制。
13.3 #{} 和 ${} 的区别是什么?
#{} 会使用 PreparedStatement 参数占位符,安全防注入;${} 是字符串拼接,会直接进入 SQL。业务值必须用 #{},只有列名、排序方向等无法参数化的位置才可能使用 ${},并且必须做白名单校验。
13.4 为什么建议多参数使用 @Param?
@Param 能给参数明确命名,使 XML 中可以用 #{status}、#{keyword} 访问,避免依赖 MyBatis 默认参数名如 param1、arg0,提高可读性和可维护性。
13.5 XML 和注解 Mapper 如何选择?
简单 SQL 可用注解,代码集中、阅读方便。复杂动态 SQL、复杂 ResultMap、SQL 需要审查或调优时优先 XML。大型项目通常 XML 更可维护,因为 SQL 可以独立组织和优化。
13.6 一级缓存是什么?
一级缓存是 SqlSession 级缓存,同一个 SqlSession 中相同查询可复用结果。它默认开启,insert/update/delete 会清理缓存。Spring 场景下一级缓存通常受事务和 SqlSession 生命周期影响。
13.7 MyBatis 的核心执行链路是什么?
Mapper 方法通过动态代理进入 MyBatis,找到对应 MappedStatement,由 SqlSource 生成 BoundSql,ParameterHandler 绑定参数,StatementHandler 执行 SQL,ResultSetHandler 映射结果,TypeHandler 负责 Java/JDBC 类型转换。
13.8 MyBatis 最大的优势和风险是什么?
优势是 SQL 可控、性能可调、复杂查询表达能力强。风险是 SQL 分散、动态 SQL 复杂后难维护、${} 容易注入、N+1 查询、Mapper 边界混乱。专家级使用 MyBatis 的关键是建立数据访问层规范。
14. 配置项扩展矩阵
MyBatis 的全局配置对运行行为影响很大。以下配置在工程中最常见。
| 配置 | 作用 | 默认倾向 | 专家建议 |
|---|---|---|---|
mapUnderscoreToCamelCase |
下划线转驼峰 | false | Java 项目常开启 |
cacheEnabled |
二级缓存总开关 | true | 开启不代表所有 Mapper 都缓存 |
lazyLoadingEnabled |
延迟加载 | false | 谨慎开启,避免隐藏 N+1 |
aggressiveLazyLoading |
激进延迟加载 | false | 通常保持 false |
defaultExecutorType |
执行器类型 | SIMPLE | 批处理单独设计 |
defaultStatementTimeout |
SQL 超时时间 | unset | 生产建议设置 |
defaultFetchSize |
JDBC fetch size | unset | 大结果集可设置 |
safeRowBoundsEnabled |
嵌套语句 RowBounds 安全 | false | 不推荐依赖 RowBounds 分页 |
localCacheScope |
一级缓存范围 | SESSION | 高一致性场景可改 STATEMENT |
jdbcTypeForNull |
null 参数 JDBC 类型 | OTHER | Oracle 等场景常需配置 |
callSettersOnNulls |
null 也调用 setter | false | Map 或特殊对象需要时开启 |
returnInstanceForEmptyRow |
空行返回空对象 | false | 嵌套映射场景谨慎开启 |
logImpl |
日志实现 | 自动发现 | 生产用 SLF4J |
示例:
xml
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="defaultStatementTimeout" value="5"/>
<setting name="defaultFetchSize" value="200"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="logImpl" value="SLF4J"/>
</settings>
15. Mapper 方法与 Statement 匹配规则
Mapper 接口:
java
package com.example.mapper;
public interface UserMapper {
User selectById(Long id);
}
XML:
xml
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="User">
select id, username from users where id = #{id}
</select>
</mapper>
匹配关系:
text
namespace + "." + statement id
= com.example.mapper.UserMapper.selectById
常见错误:
- namespace 写错。
- XML 文件未被扫描。
- 方法名和 statement id 不一致。
- Mapper 接口包名变了但 XML 没改。
- 重载方法导致 statement id 冲突。
专家建议:Mapper 方法不要重载。MyBatis statement id 只按方法名匹配,重载会造成语义混乱。
16. 参数命名深入
无 @Param 的多参数方法:
java
User select(String username, String email);
MyBatis 可能提供:
text
arg0, arg1, param1, param2
不推荐在 XML 中依赖这些名字。
推荐:
java
User select(@Param("username") String username, @Param("email") String email);
XML:
xml
where username = #{username}
and email = #{email}
集合参数:
java
List<User> selectByIds(@Param("ids") List<Long> ids);
XML:
xml
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
17. 主键回填详解
自增主键:
xml
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into users(username, email)
values(#{username}, #{email})
</insert>
插入后:
java
User user = new User();
user.setUsername("ada");
mapper.insert(user);
Long id = user.getId();
非自增主键可在业务层生成:
java
user.setId(idGenerator.nextId());
mapper.insert(user);
专家建议:
- 分布式系统优先使用雪花 ID、号段模式或数据库序列。
- 自增 ID 简单,但分库分表和数据迁移时会受限。
- 主键生成策略属于架构决策,不只是 Mapper 配置。
18. Java 类型与 JDBC 类型
参数可指定 JDBC 类型:
xml
where deleted_at is #{deletedAt,jdbcType=TIMESTAMP}
null 值场景有些数据库需要明确 JDBC 类型:
xml
#{remark,jdbcType=VARCHAR}
常见映射:
| Java | JDBC |
|---|---|
String |
VARCHAR |
Long |
BIGINT |
Integer |
INTEGER |
BigDecimal |
DECIMAL |
LocalDateTime |
TIMESTAMP |
Boolean |
BOOLEAN / BIT |
Enum |
VARCHAR / INTEGER |
19. 最小 Debug Demo:查看最终 SQL
拦截器或日志可查看 BoundSql。
java
MappedStatement ms = configuration.getMappedStatement(
"com.example.UserMapper.selectById"
);
BoundSql boundSql = ms.getBoundSql(1L);
System.out.println(boundSql.getSql());
System.out.println(boundSql.getParameterMappings());
理解 BoundSql 有助于排查:
- 动态 SQL 最终生成什么。
- 参数名是否正确。
- foreach 是否展开。
- include 是否生效。
20. 基础反模式扩展
20.1 Mapper 返回 Map
java
Map<String, Object> selectUser(Long id);
问题:
- 类型不安全。
- 字段名散落业务层。
- 重构困难。
- IDE 无法帮助检查。
只在通用报表、临时工具或字段不固定场景使用。
20.2 select *
风险:
- 表结构变化影响对象映射。
- 多取无用列。
- 覆盖索引失效。
- join 时列名冲突。
推荐明确列名。
20.3 Mapper 方法语义不清
不推荐:
java
List<User> query(Map<String, Object> params);
推荐:
java
List<User> searchActiveUsers(UserSearchQuery query);
21. 基础阶段专家题补充
21.1 为什么 MyBatis 更适合 SQL 能力强的团队?
MyBatis 把 SQL 控制权交给开发者,复杂查询、索引、分页、锁和执行计划都需要团队理解数据库。如果团队 SQL 能力弱,可能写出大量慢 SQL、N+1 查询和注入风险。MyBatis 的自由度越高,对规范和审查要求越高。
21.2 为什么不要让数据库表结构直接决定领域模型?
数据库表是持久化结构,领域模型表达业务规则。两者生命周期不同。表结构可能为了性能、历史兼容或报表做妥协,领域模型应表达业务概念。复杂系统中应通过 Repository 或 Assembler 做转换。
21.3 MyBatis 是否会自动管理对象关系?
不会像 Hibernate 那样自动管理持久化上下文和对象关系。MyBatis 可以通过 ResultMap 映射 association/collection,但查询时机和 SQL 仍由开发者设计。这让性能更可控,也要求开发者主动处理对象图加载策略。