1. MyBatis框架概述与核心价值
第一次接触MyBatis是在2013年参与一个银行系统的数据层重构项目。当时团队从原生JDBC转向ORM框架选型,经过多轮技术对比后,MyBatis凭借其独特的SQL可控性最终胜出。十年间,我见证了MyBatis从2.x到3.5的版本演进,也深刻体会到它在不同业务场景下的独特优势。
MyBatis本质上是一个半自动化的持久层框架,与Hibernate等全自动ORM框架相比,它最大的特点是保留了开发者对SQL的完整控制权。这种设计理念带来几个显著优势:首先,复杂查询可以充分利用数据库特性编写高性能SQL;其次,DBA能够直接介入SQL优化;最重要的是,开发团队可以基于现有SQL技能快速上手。在电商秒杀、金融交易等需要精细控制SQL的场景中,这种半自动化特性往往成为技术选型的决定性因素。
当前最新稳定版本3.5.6在动态SQL、类型处理器等方面有显著增强。对于Java开发者而言,掌握MyBatis意味着获得了处理复杂数据访问场景的利器。本教程将从环境搭建开始,逐步深入到缓存机制、插件开发等高级特性,每个知识点都配有可运行的代码示例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境准备与基础配置
2.1 项目初始化与依赖管理
建议使用Maven构建项目,在pom.xml中配置以下核心依赖:
xml复制<dependencies>
<!-- MyBatis核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!-- JDBC驱动(以MySQL为例) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
</dependencies>
注意:实际项目中建议锁定所有依赖的版本号,避免因依赖冲突导致难以排查的问题。可以通过
<dependencyManagement>或BOM方式统一管理版本。
2.2 全局配置文件详解
mybatis-config.xml是框架的核心配置文件,典型配置如下:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 查询时打印SQL语句 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<package name="com.example.model"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
关键配置项说明:
mapUnderscoreToCamelCase:解决数据库字段user_name到Java属性userName的自动映射logImpl:开发阶段建议使用STDOUT_LOGGING,生产环境可切换为SLF4JPOOLED:使用连接池提升性能,MyBatis内置了连接池实现
3. 核心CRUD操作实践
3.1 实体类与Mapper接口设计
以用户管理为例,先定义实体类:
java复制public class User {
private Long id;
private String userName;
private Integer age;
// getters/setters省略
}
Mapper接口采用面向接口编程风格:
java复制public interface UserMapper {
@Insert("INSERT INTO user(user_name, age) VALUES(#{userName}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
@Update("UPDATE user SET user_name=#{userName}, age=#{age} WHERE id=#{id}")
int update(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int delete(Long id);
}
实操技巧:
@Options注解的useGeneratedKeys属性在MySQL自增主键场景特别有用,执行插入后会自动回填主键值到实体对象。
3.2 XML映射文件进阶用法
对于复杂SQL,推荐使用XML配置方式。创建UserMapper.xml:
xml复制<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userResultMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
</resultMap>
<select id="selectByCondition" resultMap="userResultMap">
SELECT * FROM user
<where>
<if test="userName != null">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<choose>
<when test="orderBy == 'age'">
ORDER BY age
</when>
<otherwise>
ORDER BY id
</otherwise>
</choose>
</where>
</select>
</mapper>
动态SQL元素解析:
<where>:智能处理WHERE前缀,避免出现WHERE AND语法错误<if>:条件判断,参数满足条件时拼接SQL片段<choose>:实现if-else逻辑,适合多分支场景
4. 高级特性深度解析
4.1 二级缓存配置与陷阱规避
MyBatis二级缓存默认不开启,需要在映射文件中显式配置:
xml复制<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
缓存策略对比:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| LRU | 最近最少使用 | 热点数据集中 |
| FIFO | 先进先出 | 数据均匀访问 |
| SOFT | 软引用 | 内存敏感场景 |
| WEAK | 弱引用 | 临时缓存数据 |
避坑指南:二级缓存是跨Session的,更新操作会导致整个缓存清空。在读写比例超过10:1且数据一致性要求不高的场景才建议使用。
4.2 插件开发实战
实现一个SQL执行时间统计插件:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class})
})
public class SqlCostInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
StatementHandler handler = (StatementHandler)invocation.getTarget();
System.out.printf("SQL [%s] 执行耗时 %d ms\n",
handler.getBoundSql().getSql(), cost);
}
}
}
在配置文件中注册插件:
xml复制<plugins>
<plugin interceptor="com.example.plugin.SqlCostInterceptor"/>
</plugins>
5. 企业级应用最佳实践
5.1 分页查询优化方案
传统分页的LIMIT语句在大数据量时性能较差,推荐使用游标分页:
xml复制<select id="selectByPage" resultMap="userResultMap">
SELECT * FROM user
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{pageSize}
</select>
性能对比测试结果(100万数据量):
| 分页方式 | 第1页 | 第100页 | 第10000页 |
|---|---|---|---|
| LIMIT | 5ms | 120ms | 4500ms |
| 游标 | 5ms | 8ms | 10ms |
5.2 多数据源动态切换
通过AbstractRoutingDataSource实现:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
// 使用ThreadLocal保存数据源标识
public class DataSourceContextHolder {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void set(String dsName) {
context.set(dsName);
}
public static String get() {
return context.get();
}
public static void clear() {
context.remove();
}
}
在Service层切换数据源:
java复制public List<User> getMasterSlaveData(Long id) {
try {
DataSourceContextHolder.set("master");
User user = userMapper.selectById(id);
DataSourceContextHolder.set("slave");
List<User> similarUsers = userMapper.selectSimilar(user.getAge());
return similarUsers;
} finally {
DataSourceContextHolder.clear();
}
}
6. 性能调优与问题排查
6.1 常见性能瓶颈分析
通过EXPLAIN分析慢查询:
sql复制EXPLAIN SELECT * FROM user WHERE age > 18 ORDER BY create_time DESC;
典型问题与解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 全表扫描 | 缺失索引 | 添加age和create_time复合索引 |
| 临时表 | 排序字段与WHERE条件无关 | 调整索引包含排序字段 |
| 回表查询 | SELECT * 导致覆盖索引失效 | 只查询必要字段 |
6.2 连接池参数优化
以HikariCP为例推荐配置:
properties复制# 最大连接数(建议:CPU核心数 * 2 + 有效磁盘数)
spring.datasource.hikari.maximum-pool-size=20
# 最小空闲连接
spring.datasource.hikari.minimum-idle=5
# 连接最大存活时间(分钟)
spring.datasource.hikari.max-lifetime=30
# 连接超时时间(毫秒)
spring.datasource.hikari.connection-timeout=3000
监控指标参考值:
- 活跃连接数:建议不超过最大连接数的80%
- 获取连接平均耗时:<100ms为优,>1s需告警
- 连接等待线程数:持续>0说明连接池过小
7. 复杂场景解决方案
7.1 批量插入性能优化
使用ExecutorType.BATCH模式:
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<10000; i++) {
mapper.insert(new User("user"+i, i%100));
if(i % 1000 == 0) {
session.flushStatements();
}
}
session.commit();
}
性能对比测试(插入1万条):
| 方式 | 耗时 | 内存消耗 |
|---|---|---|
| 普通模式 | 12.5s | 320MB |
| BATCH模式 | 1.8s | 45MB |
7.2 嵌套结果映射
处理一对多关联查询:
xml复制<resultMap id="blogResultMap" type="Blog">
<id property="id" column="blog_id"/>
<result property="title" column="title"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
<result property="content" column="content"/>
</collection>
</resultMap>
<select id="selectBlogWithComments" resultMap="blogResultMap">
SELECT
b.id as blog_id, b.title,
c.id as comment_id, c.content
FROM blog b LEFT JOIN comment c ON b.id = c.blog_id
WHERE b.id = #{id}
</select>
经验之谈:当关联数据超过3层时,建议拆分为多个单表查询,通过代码组装对象。N+1查询问题可以通过
@BatchSize注解缓解。
