MyBatis 从入门到精通:一篇就够的实战指南(Java)
MyBatis 从入门到精通:一篇就够的实战指南(Java)
目标读者:Java 初/中/高级开发、准备面试的同学、正在从 JPA 切换到 MyBatis 的团队。
文章结构:概念 → 快速上手 → 核心原理 → 高级特性 → 性能调优 → 常见问题 → 最佳实践 → 面试题。
目录
- 1. 什么是 MyBatis?为什么选择它
- 2. 快速开始(Spring Boot 版)
- 3. 核心概念与运行机制
- 4. 映射语法与动态 SQL
- 5. 类型处理器 TypeHandler(含自定义 JSON/枚举示例)
- 6. Spring 事务与多数据源整合
- 7. 分页:LIMIT、RowBounds 与 PageHelper
- 8. 缓存机制:一级/二级缓存
- 9. 性能优化与批处理
- 10. 插件(拦截器)机制
- 11. 代码生成:MyBatis Generator 与替代方案
- 12. 常见问题排查(Troubleshooting)
- 13. 工程落地与包结构建议
- 14. 面试高频题与答题要点
- 15. 纯 MyBatis(非 Spring)最小可运行示例
- 16. 参考资料与学习路径
1. 什么是 MyBatis?为什么选择它
MyBatis 是一个轻量级持久层框架,核心价值是:
- SQL 自由:你自己写 SQL,掌控查询、索引、复杂联表、性能。
- ORM 适度:相比 JPA/Hibernate,MyBatis 不做全自动映射,避免“黑盒”,排查更直观。
- 可扩展:插件拦截器、TypeHandler、动态 SQL、二级缓存,足够灵活。
什么时候优先选 MyBatis?
- 报表/复杂查询/存储过程多、对 SQL 可控性要求强。
- 性能极致场景:需要自己精细化调优 SQL。
- 需要多租户/审计/数据权限等“跨切面”能力(拦截器很好用)。
什么时候考虑 JPA?
- 以 CRUD 为主,领域模型聚合清晰,希望更少 SQL(但也要接受性能可预期性降低)。
2. 快速开始(Spring Boot 版)
目标:5 分钟跑通一个
User
的增删改查。
2.1 数据表
CREATE TABLE `user` (`id` BIGINT PRIMARY KEY AUTO_INCREMENT,`username` VARCHAR(50) NOT NULL,`email` VARCHAR(100) NOT NULL,`status` TINYINT NOT NULL DEFAULT 1, -- 1:启用 0:停用`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2 依赖(Maven)
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>
2.3 application.yml
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: rootmvc:pathmatch:matching-strategy: ant_path_matcher
mybatis:mapper-locations: classpath:mapper/**/*.xmltype-aliases-package: com.example.demo.domainconfiguration:map-underscore-to-camel-case: truedefault-fetch-size: 100default-statement-timeout: 10
2.4 实体类
package com.example.demo.domain;import lombok.Data;
import java.time.LocalDateTime;@Data
public class User {private Long id;private String username;private String email;private Integer status;private LocalDateTime createdAt;private LocalDateTime updatedAt;
}
2.5 Mapper 接口
package com.example.demo.mapper;import com.example.demo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;@Mapper
public interface UserMapper {int insert(User user);int updateById(User user);int deleteById(@Param("id") Long id);User selectById(@Param("id") Long id);List<User> selectByUsernameLike(@Param("keyword") String keyword);
}
2.6 Mapper XML(resources/mapper/UserMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.demo.mapper.UserMapper"><resultMap id="BaseResultMap" type="com.example.demo.domain.User"><id column="id" property="id"/><result column="username" property="username"/><result column="email" property="email"/><result column="status" property="status"/><result column="created_at" property="createdAt"/><result column="updated_at" property="updatedAt"/></resultMap><sql id="Base_Column_List">id, username, email, status, created_at, updated_at</sql><insert id="insert" parameterType="com.example.demo.domain.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (username, email, status)VALUES (#{username}, #{email}, #{status})</insert><update id="updateById" parameterType="com.example.demo.domain.User">UPDATE user<set><if test="username != null">username = #{username},</if><if test="email != null">email = #{email},</if><if test="status != null">status = #{status},</if></set>WHERE id = #{id}</update><delete id="deleteById" parameterType="long">DELETE FROM user WHERE id = #{id}</delete><select id="selectById" parameterType="long" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM user WHERE id = #{id}</select><select id="selectByUsernameLike" parameterType="string" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM user<where><if test="keyword != null and keyword != ''">AND username LIKE CONCAT('%', #{keyword}, '%')</if></where>ORDER BY id DESCLIMIT 100</select></mapper>
2.7 Service & Controller(示例)
package com.example.demo.service;import com.example.demo.domain.User;
import com.example.demo.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;@Service
@RequiredArgsConstructor
public class UserService {private final UserMapper userMapper;@Transactionalpublic Long create(User user) {userMapper.insert(user);return user.getId();}@Transactionalpublic void update(User user) {userMapper.updateById(user);}public User get(Long id) {return userMapper.selectById(id);}public List<User> search(String keyword) {return userMapper.selectByUsernameLike(keyword);}
}
3. 核心概念与运行机制
MyBatis 运行时关键对象:
-
SqlSessionFactory
:会话工厂(线程安全),由配置构建,创建SqlSession
。 -
SqlSession
:一次数据库会话(非线程安全),管理 Statement/事务。 -
Mapper
接口与代理:MyBatis 使用MapperProxy
动态代理接口方法 →MappedStatement
→ 执行器(Executor
)。 -
执行器
Executor
:负责缓存、SQL 执行,类型有SIMPLE
、REUSE
、BATCH
。 -
处理器:
ParameterHandler
(参数预处理),ResultSetHandler
(结果映射),TypeHandler
(Java ↔ JDBC 类型转换)。
执行流程(简化):
Mapper
方法被调用 → 动态代理找到MappedStatement
(由 XML/注解解析)。ParameterHandler
绑定#{}
参数,生成PreparedStatement
。Executor
执行,命中缓存则返回,否则访问数据库。ResultSetHandler
将ResultSet
映射为对象(resultMap
/自动映射)。
4. 映射语法与动态 SQL
4.1 参数绑定:#{}
vs ${}
#{}
:预编译占位符,安全(防注入),自动做类型转换。${}
:字符串拼接,谨慎使用(表名/列名动态时),注意 SQL 注入风险。
4.2 resultType
vs resultMap
resultType
:直接映射到类/基本类型,适合简单查询。resultMap
:可定义<id/>
、<result/>
、<association/>
(一对一)、<collection/>
(一对多)、<discriminator/>
(鉴别器),适合复杂对象图。
4.3 关联映射:一对一 / 一对多
<!-- 一对一:User 包含 Profile -->
<resultMap id="UserWithProfile" type="User"><id column="u_id" property="id"/><result column="username" property="username"/><association property="profile" javaType="Profile" columnPrefix="p_"><id column="p_id" property="id"/><result column="p_phone" property="phone"/></association>
</resultMap><select id="selectUserWithProfile" resultMap="UserWithProfile">SELECT u.id AS u_id, u.username,p.id AS p_id, p.phone AS p_phoneFROM user u LEFT JOIN profile p ON u.id = p.user_idWHERE u.id = #{id}
</select>
<!-- 一对多:User 包含 roles 列表(通过嵌套查询或嵌套结果) -->
<resultMap id="UserWithRoles" type="User"><id column="u_id" property="id"/><result column="username" property="username"/><collection property="roles" ofType="Role"><id column="r_id" property="id"/><result column="r_name" property="name"/></collection>
</resultMap><select id="selectUserWithRoles" resultMap="UserWithRoles">SELECT u.id AS u_id, u.username,r.id AS r_id, r.name AS r_nameFROM user u LEFT JOIN user_role ur ON u.id=ur.user_idLEFT JOIN role r ON ur.role_id=r.idWHERE u.id=#{id}
</select>
建议:能一次性 JOIN 出来就不要 N+1(嵌套查询),必要时才懒加载。
4.4 动态 SQL 常用标签
<if>
/<choose-when-otherwise>
条件拼接。<where>
自动处理多余 AND/OR。<set>
动态更新字段(自动处理逗号)。<trim prefix="(" suffix=")" suffixOverrides=","/>
高级裁剪。<foreach>
循环(IN 查询、批量插入)。<bind>
绑定变量(如LIKE
模糊匹配)。
综合示例:复杂查询
<select id="queryUsers" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM user<where><if test="username != null and username != ''">AND username LIKE CONCAT('%', #{username}, '%')</if><if test="emails != null and emails.size > 0">AND email IN<foreach collection="emails" item="e" open="(" close=")" separator=",">#{e}</foreach></if><if test="statusList != null and statusList.size>0">AND status IN<foreach collection="statusList" item="s" open="(" close=")" separator=",">#{s}</foreach></if></where>ORDER BY id DESC<if test="limit != null">LIMIT #{limit}</if><if test="offset != null"> OFFSET #{offset}</if>
</select>
5. 类型处理器 TypeHandler(含自定义 JSON/枚举示例)
5.1 内置与枚举处理
- 内置
Date
,LocalDateTime
, 基本类型 等常见映射开箱即用。 - 枚举:使用
EnumTypeHandler
(存字符串)或EnumOrdinalTypeHandler
(存序号)。不建议用序号,改名/顺序调整有风险。
枚举示例
public enum UserStatus {DISABLED(0), ENABLED(1);private final int code;UserStatus(int code){this.code=code;}public int getCode(){return code;}
}
// 自定义枚举 TypeHandler:用 code 入库
@MappedJdbcTypes(JdbcType.TINYINT)
@MappedTypes(UserStatus.class)
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {ps.setInt(i, parameter.getCode());}@Overridepublic UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {int code = rs.getInt(columnName);return code==1?UserStatus.ENABLED:UserStatus.DISABLED;}@Overridepublic UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {int code = rs.getInt(columnIndex);return code==1?UserStatus.ENABLED:UserStatus.DISABLED;}@Overridepublic UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {int code = cs.getInt(columnIndex);return code==1?UserStatus.ENABLED:UserStatus.DISABLED;}
}
在 Mapper XML 中声明或通过全局配置扫描
<result column="status" property="status" typeHandler="com.example.demo.type.UserStatusTypeHandler"/>
5.2 JSON 字段映射(以 Jackson 为例)
public class Profile {private String phone;private List<String> tags;
}
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Profile.class)
public class JsonProfileTypeHandler extends BaseTypeHandler<Profile> {private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Profile parameter, JdbcType jdbcType) throws SQLException {try { ps.setString(i, MAPPER.writeValueAsString(parameter)); }catch (JsonProcessingException e) { throw new SQLException(e); }}@Overridepublic Profile getNullableResult(ResultSet rs, String columnName) throws SQLException {String json = rs.getString(columnName);if (json == null) return null;try { return MAPPER.readValue(json, Profile.class); }catch (IOException e) { throw new SQLException(e); }}@Overridepublic Profile getNullableResult(ResultSet rs, int columnIndex) throws SQLException {String json = rs.getString(columnIndex);if (json == null) return null;try { return MAPPER.readValue(json, Profile.class); }catch (IOException e) { throw new SQLException(e); }}@Overridepublic Profile getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {String json = cs.getString(columnIndex);if (json == null) return null;try { return MAPPER.readValue(json, Profile.class); }catch (IOException e) { throw new SQLException(e); }}
}
6. Spring 事务与多数据源整合
6.1 事务
- 使用
@Transactional
(默认运行时异常回滚)。 - MyBatis 与 Spring 事务整合由
SqlSessionTemplate
完成,无需手工commit/rollback
。
注意:同类内部方法调用不会触发 AOP 事务,可用 @Transactional
放到 public
对外方法,或通过 self-injection 解决。
6.2 多数据源(读写分离示例)
思路:定义两个 DataSource
,两个 SqlSessionFactory
,分别 @MapperScan
指向不同包。
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.read", sqlSessionFactoryRef = "readSqlSessionFactory")
public class ReadDataSourceConfig {@Beanpublic DataSource readDataSource() { /* 配置 HikariDataSource */ }@Beanpublic SqlSessionFactory readSqlSessionFactory(@Qualifier("readDataSource") DataSource ds) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(ds);return bean.getObject();}
}
也可使用路由数据源(AbstractRoutingDataSource)+ 拦截器/注解在运行时切换。
7. 分页:LIMIT、RowBounds 与 PageHelper
- 直接 LIMIT/OFFSET:最可控,适合大多数场景。
- RowBounds:会把所有结果查出后在内存截断,大表慎用。
- PageHelper 插件:拦截 SQL 自动改写为分页语句,并统计总数,开发效率高。
PageHelper 示例
<!-- Maven -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.7</version>
</dependency>
Page<User> page = PageHelper.startPage(1, 20).doSelectPage(() -> userMapper.selectByUsernameLike("tom"));
return new PageResult<>(page.getResult(), page.getTotal());
8. 缓存机制:一级/二级缓存
- 一级缓存(SqlSession 级别):同一会话内相同查询直接命中;会在
commit/close
后失效。 - 二级缓存(Mapper 命名空间级别):多个会话共享;需开启
<cache/>
或@CacheNamespace
;更新/插入/删除默认会清空本命名空间缓存。
二级缓存开启示例
<mapper namespace="com.example.demo.mapper.UserMapper"><cache eviction="LRU" flushInterval="600000" size="1024" readOnly="false"/><!-- 其余 SQL ... -->
</mapper>
注意:跨表更新导致数据不一致时,使用 cache-ref
引用同一缓存区域,或干脆关闭二级缓存,改用业务层缓存(如 Redis)。
9. 性能优化与批处理
9.1 批处理
ExecutorType.BATCH
:减少网络往返,成批提交。
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {UserMapper mapper = session.getMapper(UserMapper.class);for (User u: users) { mapper.insert(u); }session.commit();
}
- 批量插入 SQL(更快):
<insert id="batchInsert">INSERT INTO user (username, email, status) VALUES<foreach collection="list" item="u" separator=",">(#{u.username}, #{u.email}, #{u.status})</foreach>
</insert>
9.2 其他优化要点
- 尽量命中 覆盖索引,避免回表。
- 合理选择 JOIN vs. 子查询,规避 N+1。
- 用
<sql>
片段重用列清单,保持一致性。 - 大列表分页:使用 游标/seek 方法(
where id > ? limit ?
)替代传统 offset,提高性能。
10. 插件(拦截器)机制
MyBatis 允许对 4 大接口进行拦截:Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
。
示例:SQL 耗时打印 + 租户注入
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlCostInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler) Plugin.getTarget(invocation.getTarget());BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();long t1 = System.nanoTime();try { return invocation.proceed(); }finally {long t2 = System.nanoTime();System.out.println("SQL耗时(ms): " + (t2 - t1)/1_000_000 + " => " + sql);}}@Overridepublic Object plugin(Object target) { return Plugin.wrap(target, this); }@Overridepublic void setProperties(Properties properties) {}
}
注意:拦截器容易引入隐形开销与副作用,审慎上线,可灰度测试。
11. 代码生成:MyBatis Generator 与替代方案
11.1 MyBatis Generator(MBG)
优点:稳健、官方、能根据表结构生成 model/mapper/xml
。
缺点:生成代码风格偏老,需要二次改造;复杂 SQL 仍需手写。
MBG 配置(片段)
<generatorConfiguration><context id="MySqlContext" targetRuntime="MyBatis3Simple"><jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"connectionURL="jdbc:mysql://localhost:3306/demo"userId="root" password="root"/><javaModelGenerator targetPackage="com.example.demo.domain" targetProject="src/main/java"/><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/><javaClientGenerator targetPackage="com.example.demo.mapper" targetProject="src/main/java" type="XMLMAPPER"/><table tableName="user" domainObjectName="User"/></context>
</generatorConfiguration>
11.2 替代方案
- MyBatis-Plus:增强 CRUD、代码生成、分页、Wrapper 条件构造器(学习/上手快)。
- 自建代码生成器:基于模板(Freemarker/Beetl),按团队规范定制输出。
12. 常见问题排查(Troubleshooting)
Parameter 'xxx' not found
:方法参数未加@Param
,或 XML 使用的名字与实际不一致。Invalid bound statement (not found)
:namespace + id
未匹配;mapper-locations
扫描路径不对;包路径大小写错误。TooManyResultsException
:selectOne
返回多行;请改limit 1
或使用selectList
。- 驼峰映射不生效:未开启
map-underscore-to-camel-case
或列别名未对齐。 javaType/jdbcType
冲突:jdbcType
写NULL
可避免某些数据库驱动因null
无法推断类型而报错。- 二级缓存脏读:跨命名空间更新;使用
cache-ref
或关闭二级缓存。 - SQL 注入:使用
${}
拼接外部输入(尤其是order by
、表名)造成风险,优先#{}
或白名单校验。 - 事务不生效:同类内部调用;数据源未配置到
DataSourceTransactionManager
;异常被吞。 RowBounds
内存炸裂:大数据量分页必须改 SQL →LIMIT
或使用插件。- 批处理返回主键:
useGeneratedKeys
+keyProperty
;批量插入需注意驱动/数据库是否支持批量返回主键。
13. 工程落地与包结构建议
com.example.demo
├── common // 公共组件(拦截器、枚举、异常、工具)
├── config // 数据源、MyBatis、事务、分页插件配置
├── domain // 领域模型(DO)
├── dto|vo // 入参/出参对象
├── mapper // Mapper 接口 + XML(resources/mapper)
├── repository // 组合多个 Mapper 的仓储实现(可选)
├── service // 业务服务层(含 @Transactional)
├── web // 控制器
└── starter // 启动类
建议:
- 基础列片段统一
<sql id="Base_Column_List"/>
,减少遗漏。 - Mapper 拆分:
BaseMapper
(通用) +XxxMapper
(个性化)。 - 复杂 SQL 封装成视图/存储过程时,务必评估可维护性与迁移成本。
14. 面试高频题与答题要点
- MyBatis 与 Hibernate 区别? —— SQL 自由 vs. 自动 ORM;可控性/性能 vs. 生产力。
#{} 与 ${}
区别? —— 预编译占位符(安全) vs. 字符串拼接(有注入风险)。- 一级/二级缓存工作原理? —— 会话级/命名空间级;更新语句清缓存;
cache-ref
。 - 动态 SQL 标签 ——
if/choose/where/set/trim/foreach/bind
场景与常见坑。 - 插件能拦截哪些点? ——
Executor/StatementHandler/ParameterHandler/ResultSetHandler
;谨慎使用。 - 分页实现方案? —— LIMIT、RowBounds(慎用)、PageHelper(优点/缺点)。
- 批处理如何做? ——
ExecutorType.BATCH
vs. 多值 INSERT;差异与回滚策略。 - 如何避免 N+1? —— JOIN 一次查全;必要时懒加载但注意一致性与事务边界。
- 事务为什么没生效? —— AOP 代理、同类调用、异常类型、事务管理器配置。
- TypeHandler 的作用与自定义示例 —— 枚举/JSON 映射。
15. 纯 MyBatis(非 Spring)最小可运行示例
15.1 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><environments default="dev"><environment id="dev"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/demo"/><property name="username" value="root"/><property name="password" value="root"/></dataSource></environment></environments><mappers><mapper resource="mapper/UserMapper.xml"/></mappers>
</configuration>
15.2 启动代码
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);User u = mapper.selectById(1L);System.out.println(u);
}
16. 参考资料与学习路径
- 官方文档:熟悉 XML 标签、配置项、缓存与插件机制。
- 深入源码:
MapperProxy
、Executor
、Configuration
、MappedStatement
、BoundSql
。 - 实战演练:把线上一个复杂查询用 MyBatis 重写,做 Explain 分析 + 索引优化 + 压测。
- 扩展阅读:MyBatis-Plus、ShardingSphere(分库分表)、多租户/数据权限设计。
最后
MyBatis 的真正价值在于“可控性 + 可观测性 + 可维护性”。
掌握本文的路线与示例,配合你所在业务的真实 SQL 场景,基本可以实现从入门 → 熟练 → 精通。祝使用愉快!🚀