一、核心概念与架构设计
1. MyBatis 核心定义与定位
问题:什么是 MyBatis?它的核心价值是什么?
-
本质定义 :MyBatis 是一个半自动 ORM(Object-Relational Mapping)持久层框架
-
核心功能:
-
自定义 SQL:开发者完全控制 SQL 编写
-
存储过程支持:可调用数据库存储过程
-
高级映射:复杂对象关系映射支持
-
-
设计哲学:在 SQL 可控性和开发效率之间取得平衡
2. MyBatis 与全自动 ORM 的对比分析
问题:为什么说 MyBatis 是半自动 ORM?与 Hibernate 的根本区别?
| 特性维度 | MyBatis (半自动) | Hibernate (全自动) |
|---|---|---|
| SQL 控制 | 开发者手动编写和优化 | 框架自动生成,开发者无需关心 |
| 关联查询 | 需手动编写 JOIN 或嵌套查询 SQL | 通过对象导航自动加载 |
| 性能调优 | 直接优化 SQL 语句 | 优化 HQL、缓存配置等 |
| 学习曲线 | 较低,贴近传统 JDBC | 较高,需要理解 Session、持久化状态等概念 |
| 适用场景 | 复杂查询、高性能要求、遗留系统 | 标准 CRUD、快速开发、数据库无关性要求高 |
3. MyBatis 核心价值主张
问题:MyBatis 的主要优势有哪些?
java
// 优势体现示例
public class MyBatisAdvantages {
// 1. SQL 与代码分离
// SQL 在 XML/注解中,业务代码更清晰
@Select("SELECT * FROM users WHERE status = #{status}")
List<User> findActiveUsers(@Param("status") String status);
// 2. 精准的 SQL 控制
// 可以编写复杂查询,利用数据库特定功能
public void complexQuery() {
// 可以使用窗口函数、CTE 等高级 SQL 特性
}
// 3. 简化 JDBC 样板代码
// 自动处理连接、参数设置、结果集映射
}
二、SQL 处理机制深度解析
4. #{} 与 ${} 的底层原理剖析
问题:详细说明 #{} 和 ${} 的工作原理和区别
#{} (预编译占位符)
处理流程:
-
解析阶段 :MyBatis 解析 SQL,将
#{param}替换为? -
参数设置 :通过
PreparedStatement.setXxx()方法安全设置参数 -
SQL 执行:数据库接收预编译的 SQL 和参数值
安全机制:
sql
-- 原始 SQL
SELECT * FROM users WHERE name = #{username}
-- 转换后 SQL (发送到数据库)
SELECT * FROM users WHERE name = ?
-- 参数值单独传递 (防止 SQL 注入)
PreparedStatement.setString(1, "John'; DROP TABLE users; --")
${} (字符串替换)
处理流程:
-
直接替换 :MyBatis 将
${param}直接替换为参数值的字符串形式 -
SQL 拼接:生成完整的 SQL 语句发送到数据库
风险示例:
sql
-- 原始 SQL (危险!)
SELECT * FROM ${tableName} WHERE id = #{id}
-- 如果 tableName = "users; DROP TABLE products; --"
-- 最终 SQL
SELECT * FROM users; DROP TABLE products; -- WHERE id = ?
使用场景对比
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| WHERE 条件值 | #{} |
防止 SQL 注入 |
| 表名/列名 | ${} |
动态指定数据库对象 |
| ORDER BY 子句 | ${} |
动态排序字段 |
| LIKE 查询 | #{} + Java 拼接 |
安全且灵活 |
5. 动态 SQL 的完整技术栈
问题:MyBatis 动态 SQL 的完整标签集和执行原理
9 大动态 SQL 标签详解
1. <if> 条件判断
XML
<select id="findUsers" parameterType="map" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
2. <choose><when><otherwise> 多路选择
XML
<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE
<choose>
<when test="name != null">
name = #{name}
</when>
<when test="email != null">
email = #{email}
</when>
<otherwise>
status = 'ACTIVE'
</otherwise>
</choose>
</select>
3. <where> 智能 WHERE 子句
XML
<select id="findUsers" parameterType="map" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">name = #{name}</if>
<if test="email != null">AND email = #{email}</if>
</where>
</select>
<!-- 自动处理 AND 前缀,如果所有条件为空则省略 WHERE -->
4. <set> 智能 UPDATE 语句
XML
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
<!-- 自动处理尾部逗号 -->
5. <trim> 自定义修剪
XML
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
</trim>
<!-- 更灵活的字符串修剪 -->
6. <foreach> 循环遍历
XML
<select id="findUsersByIds" parameterType="list" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</select>
7. <bind> 变量绑定
XML
<select id="findUsers" parameterType="map" resultType="User">
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM users
WHERE name LIKE #{pattern}
</select>
OGNL 表达式引擎
执行原理:
java
// OGNL (Object-Graph Navigation Language) 示例
public class OGNLExample {
public void processDynamicSQL() {
// MyBatis 使用 OGNL 计算表达式的值
// test="name != null and name != ''"
// 支持的操作:
// - 属性访问: user.name
// - 方法调用: user.getName()
// - 数学运算: age > 18
// - 逻辑运算: name != null and age > 18
// - 集合操作: list != null and list.size() > 0
}
}
三、缓存机制深度剖析
6. 一级缓存详细工作机制
问题:一级缓存的实现原理和生命周期
作用域与生命周期
java
public class FirstLevelCacheDemo {
public void cacheBehavior() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询 - 访问数据库
User user1 = mapper.selectUser(1L);
// 第二次查询相同数据 - 从一级缓存获取
User user2 = mapper.selectUser(1L);
// user1 == user2 (同一个对象引用)
// 任何 INSERT/UPDATE/DELETE 操作都会清空一级缓存
mapper.updateUser(user1);
// 第三次查询 - 再次访问数据库(缓存已清空)
User user3 = mapper.selectUser(1L);
} finally {
sqlSession.close(); // 关闭 Session,缓存销毁
}
}
}
缓存Key生成算法
java
public class CacheKeyGenerator {
// MyBatis 生成缓存Key的要素:
// 1. MappedStatement的id
// 2. 分页参数 (RowBounds)
// 3. 查询SQL本身
// 4. 参数值
// 5. 环境id (environmentId)
public CacheKey createCacheKey() {
CacheKey cacheKey = new CacheKey();
cacheKey.update(mappedStatementId);
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// ... 参数处理
return cacheKey;
}
}
7. 二级缓存架构设计
问题:二级缓存的配置、序列化要求和作用域
配置与启用
XML
<!-- 1. 全局启用二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. Mapper XML 中配置缓存 -->
<mapper namespace="com.example.UserMapper">
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<!-- 3. 实体类必须实现 Serializable -->
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... fields
}
</mapper>
缓存策略对比
| 策略 | 描述 | 适用场景 |
|---|---|---|
| LRU (默认) | 最近最少使用 | 热点数据集中 |
| FIFO | 先进先出 | 数据访问均匀 |
| SOFT | 软引用 | 内存敏感场景 |
| WEAK | 弱引用 | 临时缓存 |
四、插件机制深度解析
8. 插件拦截原理与实现
问题:MyBatis 插件的拦截机制和实现步骤
四大可拦截组件
java
public interface PluginTargets {
// 1. Executor - SQL执行器
// 拦截方法: update, query, commit, rollback等
// 2. StatementHandler - SQL语法构建
// 拦截方法: prepare, parameterize等
// 3. ParameterHandler - 参数处理
// 拦截方法: setParameters
// 4. ResultSetHandler - 结果集处理
// 拦截方法: handleResultSets, handleOutputParameters
}
插件实现完整示例
java
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class QueryStatsPlugin implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(QueryStatsPlugin.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行原始方法
Object result = invocation.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long cost = endTime - startTime;
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
logger.info("SQL执行统计 - ID: {}, 耗时: {}ms", ms.getId(), cost);
// 慢SQL监控
if (cost > 1000) {
logger.warn("慢SQL警告: {}, 耗时: {}ms", ms.getId(), cost);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 接收配置参数
}
}
配置启用插件
XML
<plugins>
<plugin interceptor="com.example.QueryStatsPlugin">
<property name="slowQueryThreshold" value="1000"/>
</plugin>
</plugins>
五、关联映射深度技术
9. 一对一关联的两种实现模式
问题:详细说明 MyBatis 一对一关联的两种实现方式及其优劣
方式一:联合查询 (JOIN)
XML
<!-- 单次SQL查询,性能较好 -->
<resultMap id="UserDetailMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 关联对象映射 -->
<association property="profile" javaType="UserProfile">
<id property="id" column="profile_id"/>
<result property="realName" column="real_name"/>
<result property="address" column="address"/>
</association>
</resultMap>
<select id="selectUserWithProfile" resultMap="UserDetailMap">
SELECT
u.id, u.username,
p.id as profile_id, p.real_name, p.address
FROM users u
LEFT JOIN user_profiles p ON u.profile_id = p.id
WHERE u.id = #{id}
</select>
方式二:嵌套查询 (N+1查询)
XML
<!-- 分两次查询,更灵活但可能性能较差 -->
<resultMap id="UserWithProfileMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 嵌套查询 -->
<association
property="profile"
column="profile_id"
javaType="UserProfile"
select="selectUserProfileById"/>
</resultMap>
<select id="selectUser" resultMap="UserWithProfileMap">
SELECT id, username, profile_id FROM users WHERE id = #{id}
</select>
<select id="selectUserProfileById" resultType="UserProfile">
SELECT id, real_name, address FROM user_profiles WHERE id = #{profile_id}
</select>
两种方式对比
| 维度 | 联合查询 | 嵌套查询 |
|---|---|---|
| 性能 | 一次查询,性能较好 | N+1 查询问题,性能可能较差 |
| 灵活性 | SQL 相对复杂 | 查询可复用,逻辑清晰 |
| 延迟加载 | 不支持 | 支持延迟加载 |
| 适用场景 | 关联数据量小,关系简单 | 关联复杂,需要延迟加载 |
10. 延迟加载的底层实现
问题:MyBatis 延迟加载的实现原理和技术细节
代理对象生成机制
java
public class LazyLoadingDemo {
public void lazyLoadBehavior() {
User user = userMapper.selectUser(1L);
// 此时 user.profile 是代理对象,不是 null
// 第一次访问关联对象时触发查询
String address = user.getProfile().getAddress();
// 实际执行: SELECT * FROM user_profiles WHERE id = ?
}
}
配置与实现原理
XML
<!-- 启用延迟加载 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
CGLIB 代理实现:
java
public class LazyLoader implements MethodInterceptor {
private String sql;
private Object parameter;
private SqlSession sqlSession;
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 第一次调用方法时加载真实数据
if (!loaded) {
loadRealObject();
}
return method.invoke(realObject, args);
}
private void loadRealObject() {
realObject = sqlSession.selectOne(sql, parameter);
loaded = true;
}
}
六、执行器与批处理
11. 三种执行器深度对比
问题:详细说明 MyBatis 三种执行器的区别和适用场景
SimpleExecutor
java
public class SimpleExecutorBehavior {
public void demo() {
// 每次执行都创建新的 Statement
User user1 = mapper.selectUser(1L); // 创建 Statement1
User user2 = mapper.selectUser(2L); // 创建 Statement2
// Statement1.close(), Statement2.close()
}
}
// 特点:简单安全,但性能一般
ReuseExecutor
java
public class ReuseExecutorBehavior {
public void demo() {
// 复用相同 SQL 的 Statement
User user1 = mapper.selectUser(1L); // 创建 Statement (key: "selectUser")
User user2 = mapper.selectUser(2L); // 复用 Statement
User user3 = mapper.selectByName("John"); // 创建新 Statement (key: "selectByName")
// Session 关闭时统一关闭所有 Statement
}
}
// 特点:减少 Statement 创建开销
BatchExecutor
java
public class BatchExecutorBehavior {
public void demo() {
// 批量操作
for (int i = 0; i < 1000; i++) {
User user = new User("user" + i);
mapper.insertUser(user); // 添加到批处理
}
// 一次性执行所有 INSERT
List<BatchResult> results = sqlSession.flushStatements();
}
}
// 特点:批量操作,大幅提升性能
执行器选择策略
| 场景 | 推荐执行器 | 理由 |
|---|---|---|
| Web 请求 | SimpleExecutor | 避免线程安全问题 |
| 报表生成 | ReuseExecutor | SQL 重复度高 |
| 数据导入 | BatchExecutor | 批量操作需求 |
| 默认选择 | SimpleExecutor | 平衡安全与性能 |
12. 批量处理最佳实践
问题:MyBatis 批量插入的实现方式和性能优化
方式一:BatchExecutor
java
public class BatchInsertDemo {
public void batchInsert(List<User> users) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.insertUser(users.get(i));
// 分批提交,避免内存溢出
if (i % 1000 == 0) {
sqlSession.flushStatements();
}
}
sqlSession.commit(); // 提交剩余数据
} finally {
sqlSession.close();
}
}
}
方式二:foreach 批量插入
XML
<insert id="batchInsertUsers" parameterType="list">
INSERT INTO users (name, email, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.age})
</foreach>
</insert>
性能对比:
-
BatchExecutor:适合大数据量,内存控制好
-
foreach:适合中小数据量,代码简单
-
JDBC 批量:最大性能,但需要原生 JDBC
七、高级特性与最佳实践
13. 类型处理器深度应用
问题:MyBatis 类型处理器的实现原理和自定义方法
枚举类型处理
java
// 1. 定义枚举
public enum UserStatus {
ACTIVE(1), INACTIVE(0), DELETED(-1);
private final int code;
UserStatus(int code) { this.code = code; }
public int getCode() { return code; }
}
// 2. 自定义类型处理器
@MappedTypes(UserStatus.class)
@MappedJdbcTypes(JdbcType.INTEGER)
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
UserStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode()); // Java -> JDBC
}
@Override
public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return Arrays.stream(UserStatus.values())
.filter(status -> status.getCode() == code)
.findFirst()
.orElse(null); // JDBC -> Java
}
// 其他重载方法...
}
JSON 类型处理
java
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private final Class<T> type;
private final ObjectMapper objectMapper = new ObjectMapper();
public JsonTypeHandler(Class<T> type) {
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
T parameter, JdbcType jdbcType) throws SQLException {
try {
ps.setString(i, objectMapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("JSON序列化失败", e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
if (json != null) {
try {
return objectMapper.readValue(json, type);
} catch (IOException e) {
throw new SQLException("JSON反序列化失败", e);
}
}
return null;
}
}
14. 多参数传递的完整方案
问题:MyBatis 中传递多个参数的各种方法及其适用场景
方案一:@Param 注解(推荐)
java
public interface UserMapper {
List<User> findUsersByMultipleParams(
@Param("name") String name,
@Param("minAge") Integer minAge,
@Param("status") UserStatus status
);
}
XML
<select id="findUsersByMultipleParams" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">AND name = #{name}</if>
<if test="minAge != null">AND age >= #{minAge}</if>
<if test="status != null">AND status = #{status}</if>
</select>
方案二:Map 参数
java
public interface UserMapper {
List<User> findUsersByMap(Map<String, Object> params);
}
XML
<select id="findUsersByMap" parameterType="map" resultType="User">
SELECT * FROM users
WHERE name = #{name} AND age = #{age}
</select>
方案三:POJO 参数对象
java
public class UserQuery {
private String name;
private Integer minAge;
private Integer maxAge;
private UserStatus status;
// getters/setters
}
XML
<select id="findUsersByQuery" parameterType="UserQuery" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">AND name = #{name}</if>
<if test="minAge != null">AND age >= #{minAge}</if>
<if test="maxAge != null">AND age <= #{maxAge}</if>
<if test="status != null">AND status = #{status}</if>
</select>
方案对比总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| @Param | 类型安全,IDE 支持好 | 参数较多时代码冗长 | 2-5个参数 |
| Map | 灵活,易于扩展 | 类型不安全,IDE 支持差 | 动态参数 |
| POJO | 类型安全,易于维护 | 需要创建额外类 | 复杂查询条件 |