跳到主要内容

MyBatis框架详解

MyBatis是一款优秀的持久层框架,支持自定义SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集,通过简单的XML配置或注解,将接口与SQL语句进行映射,专注于SQL本身,赋予开发者更多的控制权和灵活性。

核心价值

MyBatis = 灵活SQL + 简化JDBC + 优雅映射 + 动态查询

  • 🚀 半自动ORM:手写SQL,自动映射结果集,控制与灵活并存
  • 🎯 动态SQL:强大的条件判断、循环、分支能力,生成复杂查询
  • 💡 优雅设计:接口与实现分离,仅需定义接口,无需实现类
  • 🔧 可插拔架构:插件机制支持自定义拦截和处理,易于扩展
  • 🛡️ 缓存支持:一级缓存与二级缓存,提升查询性能

1. MyBatis核心原理与架构

MyBatis是一个优秀的持久层框架,其核心设计理念是将SQL与代码分离,同时提供简单而强大的映射机制。理解MyBatis的架构和原理对于正确高效地使用它至关重要。

1.1 MyBatis工作原理

MyBatis的工作流程可以概括为以下几个关键步骤:

SqlSessionFactory构建过程
java
1// 1. 创建SqlSessionFactoryBuilder对象
2SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
3
4// 2. 加载mybatis-config.xml配置文件
5InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
6
7// 3. 构建SqlSessionFactory对象
8SqlSessionFactory factory = builder.build(inputStream);

在构建阶段,MyBatis会完成以下工作:

  1. 解析配置文件中的标签
  2. 创建Configuration对象
  3. 加载映射文件和注解配置
  4. 初始化内置组件
  5. 创建SqlSessionFactory实例

MyBatis通过动态代理技术实现了只需定义接口而无需编写实现类的便利特性。当我们调用Mapper接口方法时,MyBatis会拦截该调用,找到对应的SQL语句并执行,然后将结果映射为指定的对象。

1.2 核心组件剖析

MyBatis的核心组件构成了其完整的功能体系,每个组件负责特定的功能:

核心组件主要功能生命周期关键特性
Configuration全局配置信息全局唯一包含所有配置和映射信息
SqlSessionFactory创建SqlSession全局唯一线程安全,可重用
SqlSession提供SQL执行方法请求级别非线程安全,短暂
Executor执行器请求级别维护缓存,执行SQL
StatementHandler处理SQL语句方法级别创建Statement对象
ParameterHandler处理SQL参数方法级别设置预编译参数
ResultSetHandler处理结果集映射方法级别将结果集转为对象
TypeHandler类型转换全局可用Java与JDBC类型转换
MappedStatement映射语句全局可用封装SQL和映射信息
组件关系要点
  • SqlSessionFactory:单例模式,应用级生命周期
  • SqlSession:非线程安全,需要在方法作用域内使用
  • Mapper:由SqlSession创建的代理对象,依赖于SqlSession生命周期

1.3 执行流程详解

MyBatis的执行流程可以详细分解为以下步骤:

  1. 初始化阶段

    • 解析配置文件创建Configuration对象
    • 加载Mapper映射文件或注解
    • 解析SQL语句创建MappedStatement对象
    • 构建SqlSessionFactory实例
  2. 执行阶段

    • 通过SqlSessionFactory创建SqlSession
    • 通过SqlSession获取Mapper代理对象
    • 调用Mapper方法执行SQL
    • SqlSession将请求转交给Executor执行器
    • Executor调用StatementHandler处理SQL语句
    • ParameterHandler设置参数
    • 执行SQL语句
    • ResultSetHandler将结果集转换为对象
    • 返回执行结果给调用者
完整执行流程示例
1// 1. 读取MyBatis配置文件
2String resource = "mybatis-config.xml";
3InputStream inputStream = Resources.getResourceAsStream(resource);
4
5// 2. 构建SqlSessionFactory
6SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
7
8// 3. 获取SqlSession
9try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
10 // 4. 获取Mapper代理对象
11 UserMapper mapper = sqlSession.getMapper(UserMapper.class);
12
13 // 5. 执行SQL
14 User user = mapper.getUserById(1);
15
16 // 6. 处理结果
17 System.out.println(user);
18
19 // 7. 提交事务
20 sqlSession.commit();
21}

当调用mapper.getUserById(1)时,MyBatis内部会:

  1. 通过动态代理拦截该方法调用
  2. 根据方法签名找到对应的MappedStatement
  3. 创建SqlSource生成SQL语句
  4. 设置参数并执行SQL
  5. 将结果集映射为User对象
  6. 返回结果给调用者

2. 配置体系详解

MyBatis的配置体系是其灵活性和可扩展性的关键。全局配置文件是MyBatis配置的入口,包含了多种配置元素,可以根据不同环境和需求进行定制。

2.1 全局配置文件

MyBatis全局配置文件(通常命名为mybatis-config.xml)结构清晰,各元素有严格的顺序要求:

mybatis-config.xml完整示例
1<?xml version="1.0" encoding="UTF-8" ?>
2<!DOCTYPE configuration
3PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4"http://mybatis.org/dtd/mybatis-3-config.dtd">
5<configuration>
6<!-- 引入外部属性文件 -->
7<properties resource="db.properties">
8 <property name="username" value="dev_user"/>
9 <property name="password" value="dev_password"/>
10</properties>
11
12<!-- 全局设置 -->
13<settings>
14 <setting name="cacheEnabled" value="true"/>
15 <setting name="lazyLoadingEnabled" value="true"/>
16 <setting name="mapUnderscoreToCamelCase" value="true"/>
17</settings>
18
19<!-- 类型别名 -->
20<typeAliases>
21 <package name="com.example.model"/>
22</typeAliases>
23
24<!-- 类型处理器 -->
25<typeHandlers>
26 <typeHandler handler="com.example.handler.JsonTypeHandler"/>
27</typeHandlers>
28
29<!-- 插件 -->
30<plugins>
31 <plugin interceptor="com.example.plugin.PageInterceptor">
32 <property name="dialect" value="mysql"/>
33 </plugin>
34</plugins>
35
36<!-- 环境配置 -->
37<environments default="development">
38 <environment id="development">
39 <transactionManager type="JDBC"/>
40 <dataSource type="POOLED">
41 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
42 <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
43 <property name="username" value="root"/>
44 <property name="password" value="password"/>
45 </dataSource>
46 </environment>
47</environments>
48
49<!-- 数据库厂商标识 -->
50<databaseIdProvider type="DB_VENDOR">
51 <property name="MySQL" value="mysql"/>
52 <property name="Oracle" value="oracle"/>
53 <property name="SQL Server" value="sqlserver"/>
54</databaseIdProvider>
55
56<!-- 映射器 -->
57<mappers>
58 <mapper resource="com/example/mapper/UserMapper.xml"/>
59 <package name="com.example.mapper"/>
60</mappers>
61</configuration>
注意

配置元素必须按照上述顺序定义,否则MyBatis会抛出异常!

2.2 环境与数据源配置

MyBatis支持多环境配置,可以为不同的开发阶段(开发、测试、生产)配置不同的数据源和事务管理器。

多环境配置
xml
1<environments default="development">
2 <!-- 开发环境 -->
3 <environment id="development">
4 <transactionManager type="JDBC"/>
5 <dataSource type="POOLED">
6 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
7 <property name="url" value="jdbc:mysql://localhost:3306/dev_db"/>
8 <property name="username" value="dev_user"/>
9 <property name="password" value="dev_password"/>
10 </dataSource>
11 </environment>
12
13 <!-- 测试环境 -->
14 <environment id="test">
15 <transactionManager type="JDBC"/>
16 <dataSource type="POOLED">
17 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
18 <property name="url" value="jdbc:mysql://test-server:3306/test_db"/>
19 <property name="username" value="test_user"/>
20 <property name="password" value="test_password"/>
21 </dataSource>
22 </environment>
23
24 <!-- 生产环境 -->
25 <environment id="production">
26 <transactionManager type="MANAGED"/>
27 <dataSource type="JNDI">
28 <property name="data_source" value="java:comp/env/jdbc/ProductionDB"/>
29 </dataSource>
30 </environment>
31</environments>

在实际项目中,可以通过在构建SqlSessionFactory时指定环境ID来选择使用哪个环境:

java
1String resource = "mybatis-config.xml";
2InputStream inputStream = Resources.getResourceAsStream(resource);
3// 使用production环境
4SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream, "production");

2.3 类型处理器与别名

类型别名和类型处理器是MyBatis简化代码和扩展类型转换能力的两个重要机制。

类型别名为Java类型设置一个简短的名字,减少冗长类名的重复输入。

类型别名配置
xml
1<typeAliases>
2 <!-- 单个别名定义 -->
3 <typeAlias alias="User" type="com.example.model.User"/>
4
5 <!-- 批量定义包下所有类的别名 -->
6 <package name="com.example.model"/>
7</typeAliases>

MyBatis内置了常见Java类型的别名:

别名映射的类型
stringString
longLong
intInteger
booleanBoolean
dateDate
decimalBigDecimal
mapMap
listList

2.4 插件与日志配置

MyBatis提供了强大的插件机制和灵活的日志配置,可以扩展核心功能并监控SQL执行情况。

MyBatis插件是通过拦截器模式实现的,可以拦截核心组件的方法调用:

插件配置
xml
1<plugins>
2 <!-- 分页插件 -->
3 <plugin interceptor="com.github.pagehelper.PageInterceptor">
4 <property name="helperDialect" value="mysql"/>
5 <property name="reasonable" value="true"/>
6 </plugin>
7
8 <!-- SQL性能监控插件 -->
9 <plugin interceptor="com.example.plugin.SqlPerformanceInterceptor">
10 <property name="slowSqlThreshold" value="1000"/>
11 </plugin>
12</plugins>

自定义插件需要实现Interceptor接口,并通过@Intercepts注解指定拦截点:

SQL执行时间监控插件
java
1@Intercepts({
2 @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
3 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
4})
5public class SqlPerformanceInterceptor implements Interceptor {
6 private long slowSqlThreshold;
7
8 @Override
9 public Object intercept(Invocation invocation) throws Throwable {
10 MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
11 Object parameter = invocation.getArgs()[1];
12
13 BoundSql boundSql = ms.getBoundSql(parameter);
14 String sql = boundSql.getSql();
15
16 long start = System.currentTimeMillis();
17 Object result = invocation.proceed();
18 long end = System.currentTimeMillis();
19 long time = end - start;
20
21 if (time > slowSqlThreshold) {
22 String logMsg = String.format("Slow SQL: %s, Time: %dms", sql, time);
23 System.err.println(logMsg);
24 }
25
26 return result;
27 }
28
29 @Override
30 public Object plugin(Object target) {
31 return Plugin.wrap(target, this);
32 }
33
34 @Override
35 public void setProperties(Properties properties) {
36 this.slowSqlThreshold = Long.parseLong(properties.getProperty("slowSqlThreshold", "1000"));
37 }
38}
配置最佳实践
  1. 属性外部化:使用properties文件存储数据库连接信息
  2. 灵活环境配置:为不同环境(开发、测试、生产)配置不同数据源
  3. 启用驼峰命名映射:设置mapUnderscoreToCamelCase为true
  4. 合理使用缓存:根据业务需求配置二级缓存
  5. 适当的日志级别:开发环境使用DEBUG级别,生产环境使用INFO级别

3. 映射文件与SQL构建

MyBatis的核心功能是将SQL语句与Java方法关联起来,提供了XML和注解两种方式来定义SQL映射关系。这种分离式设计使得SQL语句和Java代码能够独立管理和优化。

3.1 XML映射文件详解

XML映射文件是MyBatis中定义SQL语句的主要方式,提供了丰富的配置选项和灵活的表达能力。

完整的XML映射文件示例
1<?xml version="1.0" encoding="UTF-8" ?>
2<!DOCTYPE mapper
3PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5<mapper namespace="com.example.mapper.UserMapper">
6<!-- 缓存配置 -->
7<cache
8 eviction="LRU"
9 flushInterval="60000"
10 size="512"
11 readOnly="false"/>
12
13<!-- 可重用SQL片段 -->
14<sql id="Base_Column_List">
15 id, username, email, phone, create_time, update_time
16</sql>
17
18<!-- 结果映射 -->
19<resultMap id="UserResultMap" type="com.example.model.User">
20 <id property="id" column="id" />
21 <result property="username" column="username" />
22 <result property="email" column="email" />
23 <result property="phone" column="phone" />
24 <result property="createTime" column="create_time" />
25 <result property="updateTime" column="update_time" />
26 <!-- 一对一关联 -->
27 <association property="profile" javaType="com.example.model.UserProfile">
28 <id property="id" column="profile_id" />
29 <result property="address" column="address" />
30 <result property="avatar" column="avatar" />
31 </association>
32 <!-- 一对多关联 -->
33 <collection property="orders" ofType="com.example.model.Order">
34 <id property="id" column="order_id" />
35 <result property="orderNo" column="order_no" />
36 <result property="amount" column="amount" />
37 </collection>
38</resultMap>
39
40<!-- 查询语句 -->
41<select id="getUserById" resultMap="UserResultMap" parameterType="long">
42 SELECT
43 u.*,
44 p.id as profile_id,
45 p.address,
46 p.avatar
47 FROM user u
48 LEFT JOIN user_profile p ON u.id = p.user_id
49 WHERE u.id = #{id}
50</select>
51
52<!-- 插入语句 -->
53<insert id="insertUser" parameterType="com.example.model.User" useGeneratedKeys="true" keyProperty="id">
54 INSERT INTO user (username, email, phone, create_time)
55 VALUES (#{username}, #{email}, #{phone}, #{createTime})
56</insert>
57
58<!-- 更新语句 -->
59<update id="updateUser" parameterType="com.example.model.User">
60 UPDATE user
61 SET username = #{username},
62 email = #{email},
63 phone = #{phone},
64 update_time = #{updateTime}
65 WHERE id = #{id}
66</update>
67
68<!-- 删除语句 -->
69<delete id="deleteUser" parameterType="long">
70 DELETE FROM user WHERE id = #{id}
71</delete>
72</mapper>

查询语句是最常用的SQL操作,MyBatis提供了丰富的配置选项:

查询语句详解
xml
1<select
2 id="findUserByCondition" <!-- 唯一标识,对应Mapper接口方法名 -->
3 parameterType="map" <!-- 参数类型 -->
4 resultType="User" <!-- 结果类型 -->
5 resultMap="UserResultMap" <!-- 结果映射引用 -->
6 flushCache="false" <!-- 是否刷新缓存 -->
7 useCache="true" <!-- 是否使用缓存 -->
8 timeout="10000" <!-- 超时时间(毫秒) -->
9 fetchSize="100" <!-- 结果集大小 -->
10 statementType="PREPARED" <!-- 语句类型(STATEMENT/PREPARED/CALLABLE) -->
11 resultSetType="FORWARD_ONLY"> <!-- 结果集类型 -->
12 SELECT
13 <include refid="Base_Column_List"/>
14 FROM user
15 <where>
16 <if test="username != null">
17 username LIKE CONCAT('%', #{username}, '%')
18 </if>
19 <if test="email != null">
20 AND email = #{email}
21 </if>
22 <if test="startTime != null">
23 AND create_time >= #{startTime}
24 </if>
25 </where>
26 ORDER BY create_time DESC
27 LIMIT #{offset}, #{limit}
28</select>

3.2 注解映射方式

MyBatis支持使用Java注解来定义SQL映射,适合于简单的SQL语句,不需要XML文件。

注解映射示例
java
1public interface UserMapper {
2
3 @Select("SELECT * FROM user WHERE id = #{id}")
4 User getUserById(Long id);
5
6 @Insert("INSERT INTO user (username, email, phone, create_time) " +
7 "VALUES (#{username}, #{email}, #{phone}, #{createTime})")
8 @Options(useGeneratedKeys = true, keyProperty = "id")
9 int insertUser(User user);
10
11 @Update("UPDATE user SET username = #{username}, email = #{email}, " +
12 "phone = #{phone}, update_time = #{updateTime} WHERE id = #{id}")
13 int updateUser(User user);
14
15 @Delete("DELETE FROM user WHERE id = #{id}")
16 int deleteUser(Long id);
17
18 // 结果映射
19 @Select("SELECT * FROM user WHERE id = #{id}")
20 @Results(id = "userResultMap", value = {
21 @Result(property = "id", column = "id", id = true),
22 @Result(property = "username", column = "username"),
23 @Result(property = "email", column = "email"),
24 @Result(property = "createTime", column = "create_time"),
25 @Result(property = "updateTime", column = "update_time")
26 })
27 User getUserWithMapping(Long id);
28
29 // 一对一关联
30 @Select("SELECT u.*, p.id as profile_id, p.address, p.avatar FROM user u " +
31 "LEFT JOIN user_profile p ON u.id = p.user_id WHERE u.id = #{id}")
32 @Results({
33 @Result(property = "id", column = "id", id = true),
34 @Result(property = "username", column = "username"),
35 @Result(property = "email", column = "email"),
36 @Result(property = "profile", javaType = UserProfile.class,
37 column = "id", one = @One(select = "getProfileByUserId"))
38 })
39 User getUserWithProfile(Long id);
40
41 @Select("SELECT * FROM user_profile WHERE user_id = #{userId}")
42 UserProfile getProfileByUserId(Long userId);
43
44 // 一对多关联
45 @Select("SELECT * FROM user WHERE id = #{id}")
46 @Results({
47 @Result(property = "id", column = "id", id = true),
48 @Result(property = "username", column = "username"),
49 @Result(property = "orders", javaType = List.class,
50 column = "id", many = @Many(select = "getOrdersByUserId"))
51 })
52 User getUserWithOrders(Long id);
53
54 @Select("SELECT * FROM orders WHERE user_id = #{userId}")
55 List<Order> getOrdersByUserId(Long userId);
56}

3.3 两种映射方式对比

XML映射和注解映射各有优缺点,选择合适的方式取决于项目需求和团队偏好。

特性XML映射注解映射说明
可读性⭐⭐⭐⭐⭐⭐⭐⭐XML更适合大型复杂SQL
维护性⭐⭐⭐⭐⭐⭐⭐XML分离SQL与代码,更易维护
动态SQL⭐⭐⭐⭐⭐⭐⭐XML对动态SQL支持更全面
复杂映射⭐⭐⭐⭐⭐⭐⭐⭐XML适合复杂结果映射
开发效率⭐⭐⭐⭐⭐⭐⭐⭐注解简单快捷
SQL重用⭐⭐⭐⭐⭐⭐⭐XML可提取公共SQL片段
编译时检查⭐⭐⭐⭐⭐⭐注解可在编译时检查
适用场景复杂SQL,多表关联简单CRUD,单表操作-
最佳实践建议
  1. 混合使用:根据SQL复杂度选择合适的方式
  2. 简单操作:单表的CRUD操作使用注解
  3. 复杂查询:多表关联、动态条件使用XML
  4. 团队统一:项目中保持一致的风格,避免混乱

3.4 SQL构建器使用

对于中等复杂度的SQL,MyBatis提供了SQL构建器API,可以通过Java代码动态构建SQL语句,比XML更灵活,比注解更强大。

SQL构建器示例
java
1public interface UserMapper {
2
3 @SelectProvider(type = UserSqlProvider.class, method = "findByCondition")
4 List<User> findByCondition(UserQuery query);
5
6 @InsertProvider(type = UserSqlProvider.class, method = "insert")
7 @Options(useGeneratedKeys = true, keyProperty = "id")
8 int insert(User user);
9
10 @UpdateProvider(type = UserSqlProvider.class, method = "update")
11 int update(User user);
12
13 @DeleteProvider(type = UserSqlProvider.class, method = "deleteById")
14 int deleteById(Long id);
15}
16
17class UserSqlProvider {
18
19 public String findByCondition(UserQuery query) {
20 return new SQL() {{
21 SELECT("id, username, email, phone, status, create_time, update_time");
22 FROM("user");
23
24 if (query.getUsername() != null) {
25 WHERE("username LIKE CONCAT('%', #{username}, '%')");
26 }
27
28 if (query.getStatus() != null) {
29 WHERE("status = #{status}");
30 }
31
32 if (query.getStartTime() != null) {
33 WHERE("create_time >= #{startTime}");
34 }
35
36 if (query.getEndTime() != null) {
37 WHERE("create_time <= #{endTime}");
38 }
39
40 ORDER_BY("create_time DESC");
41 }}.toString();
42 }
43
44 public String insert(User user) {
45 return new SQL() {{
46 INSERT_INTO("user");
47 VALUES("username", "#{username}");
48 VALUES("email", "#{email}");
49 VALUES("phone", "#{phone}");
50 VALUES("password", "#{password}");
51 VALUES("status", "#{status}");
52 VALUES("create_time", "#{createTime}");
53 }}.toString();
54 }
55
56 public String update(User user) {
57 return new SQL() {{
58 UPDATE("user");
59
60 if (user.getUsername() != null) {
61 SET("username = #{username}");
62 }
63
64 if (user.getEmail() != null) {
65 SET("email = #{email}");
66 }
67
68 if (user.getPhone() != null) {
69 SET("phone = #{phone}");
70 }
71
72 if (user.getStatus() != null) {
73 SET("status = #{status}");
74 }
75
76 SET("update_time = #{updateTime}");
77 WHERE("id = #{id}");
78 }}.toString();
79 }
80
81 public String deleteById(Long id) {
82 return new SQL() {{
83 DELETE_FROM("user");
84 WHERE("id = #{id}");
85 }}.toString();
86 }
87}

SQL构建器API的主要优势:

  1. 类型安全:使用Java代码构建SQL,可以在编译时检查
  2. 代码重用:可以封装常用的SQL片段为方法
  3. 灵活动态:比注解更灵活,比XML更直观
  4. 易于调试:可以在构建过程中添加日志或断点

SQL构建器的方法列表:

  • SELECT(String): 添加列到SELECT子句
  • SELECT_DISTINCT(String): 添加DISTINCT列到SELECT子句
  • FROM(String): 添加表到FROM子句
  • JOIN(String): 添加JOIN子句
  • INNER_JOIN(String): 添加INNER JOIN子句
  • LEFT_OUTER_JOIN(String): 添加LEFT OUTER JOIN子句
  • RIGHT_OUTER_JOIN(String): 添加RIGHT OUTER JOIN子句
  • WHERE(String): 添加WHERE条件
  • OR(): 添加OR连接符
  • AND(): 添加AND连接符
  • GROUP_BY(String): 添加GROUP BY子句
  • HAVING(String): 添加HAVING条件
  • ORDER_BY(String): 添加ORDER BY子句
  • INSERT_INTO(String): 设置INSERT目标表
  • VALUES(String, String): 添加INSERT值
  • UPDATE(String): 设置UPDATE目标表
  • SET(String): 添加SET子句
  • DELETE_FROM(String): 设置DELETE目标表

4. 动态SQL与复杂查询

动态SQL是MyBatis最强大的特性之一,允许根据参数条件动态生成不同的SQL语句,极大地简化了复杂查询的构建过程。

4.1 条件查询构建

条件查询是最常见的动态SQL场景,MyBatis提供了多种标签来处理条件逻辑。

if标签是最基本的条件标签,根据测试条件决定是否包含标签体内的SQL片段:

if标签示例
xml
1<select id="findUsers" resultType="User">
2 SELECT * FROM user
3 WHERE 1=1
4 <if test="username != null and username != ''">
5 AND username LIKE CONCAT('%', #{username}, '%')
6 </if>
7 <if test="status != null">
8 AND status = #{status}
9 </if>
10 <if test="startDate != null">
11 AND create_time >= #{startDate}
12 </if>
13 <if test="endDate != null">
14 AND create_time <= #{endDate}
15 </if>
16</select>

OGNL表达式是MyBatis动态SQL的核心,用于条件测试和参数访问:

OGNL表达式描述示例
属性访问访问JavaBean属性username != null
嵌套属性访问嵌套属性user.address.city == 'Beijing'
集合访问访问List/Map元素users[0].name == 'admin'
方法调用调用对象方法username.length() > 5
静态方法调用静态方法@java.lang.Math@max(id1, id2)
逻辑运算符逻辑组合age > 18 and age < 60
三元运算符条件选择isVip ? price*0.8 : price

4.2 循环与批量操作

foreach标签是处理集合参数的强大工具,适用于IN条件、批量插入等场景。

IN条件查询
xml
1<select id="findUsersByIds" resultType="User">
2 SELECT * FROM user
3 WHERE id IN
4 <foreach collection="list" item="id" open="(" separator="," close=")">
5 #{id}
6 </foreach>
7</select>

foreach标签的属性解析:

  • collection: 要迭代的集合名称(list、array、map等)
  • item: 当前迭代的元素
  • index: 当前迭代的索引(List)或键(Map)
  • open: 拼接开始的字符串
  • close: 拼接结束的字符串
  • separator: 元素之间的分隔符

4.3 动态表名与列名

在某些场景下,需要动态指定表名或列名,MyBatis也提供了相应的解决方案。

注意

动态表名和列名使用${}语法,会有SQL注入风险,必须确保参数来源安全可控!

动态表名
xml
1<select id="selectFromTable" resultType="map">
2 SELECT * FROM ${tableName} WHERE id = #{id}
3</select>
java
1// 使用示例
2Map<String, Object> data = mapper.selectFromTable("user_20230101", 1001);

4.4 多数据库支持

MyBatis提供了DatabaseIdProvider机制,可以根据不同数据库类型选择不同的SQL语句,实现跨数据库兼容。

首先在配置文件中定义数据库厂商标识:

mybatis-config.xml
xml
1<databaseIdProvider type="DB_VENDOR">
2 <property name="MySQL" value="mysql"/>
3 <property name="Oracle" value="oracle"/>
4 <property name="SQL Server" value="sqlserver"/>
5 <property name="PostgreSQL" value="postgresql"/>
6</databaseIdProvider>

然后在Mapper XML中使用databaseId属性区分不同数据库的SQL:

多数据库支持示例
xml
1<!-- MySQL版本 -->
2<select id="findUsers" resultType="User" databaseId="mysql">
3 SELECT * FROM user
4 <where>
5 <if test="createDate != null">
6 AND DATE(create_time) = #{createDate}
7 </if>
8 </where>
9 LIMIT #{offset}, #{limit}
10</select>
11
12<!-- Oracle版本 -->
13<select id="findUsers" resultType="User" databaseId="oracle">
14 SELECT * FROM
15 (
16 SELECT ROWNUM rn, u.* FROM user u
17 <where>
18 <if test="createDate != null">
19 AND TRUNC(create_time) = #{createDate}
20 </if>
21 </where>
22 WHERE ROWNUM &lt;= #{offset} + #{limit}
23 )
24 WHERE rn > #{offset}
25</select>
26
27<!-- SQL Server版本 -->
28<select id="findUsers" resultType="User" databaseId="sqlserver">
29 SELECT * FROM user
30 <where>
31 <if test="createDate != null">
32 AND CONVERT(date, create_time) = #{createDate}
33 </if>
34 </where>
35 ORDER BY id
36 OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
37</select>

针对不同数据库的主要差异处理:

功能MySQLOracleSQL ServerPostgreSQL
分页查询LIMIT offset, limitROWNUMROW_NUMBER()OFFSET-FETCHLIMIT-OFFSET
日期处理DATE(field)TRUNC(field)CONVERT(date, field)field::date
字符串连接CONCAT()||+CONCAT()||
自增主键AUTO_INCREMENTSEQUENCEIDENTITYSERIAL
批量插入多值语法联合查询表变量多值语法
NULL排序IS NULL DESCNULLS LASTCASE WHENNULLS LAST
数据库兼容最佳实践
  1. 抽象公共SQL:将共同部分提取为SQL片段
  2. 数据库版本检测:根据databaseId选择适当的SQL
  3. 封装特殊处理:将数据库特定功能封装到辅助方法
  4. 分页插件:使用PageHelper等插件统一分页处理
  5. 方言配置:通过配置文件管理不同数据库方言

5. 结果映射与关联查询

结果映射是MyBatis的核心功能之一,它负责将查询结果集转换为Java对象,处理属性名与列名的映射关系,以及管理复杂的关联对象映射。

5.1 基础结果映射

基础结果映射处理简单的列名到属性名的映射,MyBatis提供了自动映射和手动配置两种方式。

当Java属性名与数据库列名匹配或符合驼峰命名转换规则时,MyBatis可以自动映射:

自动映射示例
xml
1<!-- 启用驼峰命名转换 (mybatis-config.xml) -->
2<settings>
3 <setting name="mapUnderscoreToCamelCase" value="true"/>
4</settings>
5
6<!-- 使用自动映射 -->
7<select id="getUser" resultType="com.example.model.User">
8 SELECT
9 id,
10 username,
11 email,
12 phone,
13 create_time, <!-- 会自动映射到createTime属性 -->
14 update_time <!-- 会自动映射到updateTime属性 -->
15 FROM user
16 WHERE id = #{id}
17</select>
对应实体类
java
1public class User {
2 private Long id;
3 private String username;
4 private String email;
5 private String phone;
6 private Date createTime; // 对应create_time列
7 private Date updateTime; // 对应update_time列
8
9 // getter和setter
10}

resultMap属性详解

属性描述用法示例
id主键映射,用于区分相同ID的对象<id property="id" column="user_id"/>
result普通属性映射<result property="username" column="user_name"/>
constructor使用构造函数注入属性<constructor><idArg/><arg/></constructor>
association一对一关联映射<association property="profile" javaType="UserProfile"/>
collection一对多关联映射<collection property="orders" ofType="Order"/>
discriminator鉴别器映射,根据列值选择映射方式<discriminator javaType="int" column="type"/>

类型转换

MyBatis会自动处理Java类型与数据库类型之间的转换,也可以通过typeHandler指定自定义类型处理:

类型处理器示例
xml
1<resultMap id="UserMap" type="User">
2 <id property="id" column="id"/>
3 <result property="createdAt" column="created_at"
4 typeHandler="com.example.handler.LocalDateTimeHandler"/>
5 <result property="status" column="status"
6 typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
7 <result property="settings" column="settings"
8 typeHandler="com.example.handler.JsonTypeHandler"/>
9</resultMap>

5.2 一对一关联查询

MyBatis提供了强大的一对一关联查询能力,支持嵌套结果和嵌套查询两种方式。

嵌套结果映射通过一次SQL查询获取主对象和关联对象的所有数据:

嵌套结果映射示例
xml
1<resultMap id="UserWithProfileMap" type="User">
2 <id property="id" column="user_id"/>
3 <result property="username" column="username"/>
4 <result property="email" column="email"/>
5
6 <!-- 嵌套结果映射 - 一对一关联 -->
7 <association property="profile" javaType="UserProfile">
8 <id property="id" column="profile_id"/>
9 <result property="address" column="address"/>
10 <result property="phone" column="phone"/>
11 <result property="avatar" column="avatar"/>
12 </association>
13</resultMap>
14
15<select id="getUserWithProfile" resultMap="UserWithProfileMap">
16 SELECT
17 u.id as user_id,
18 u.username,
19 u.email,
20 p.id as profile_id,
21 p.address,
22 p.phone,
23 p.avatar
24 FROM user u
25 LEFT JOIN user_profile p ON u.id = p.user_id
26 WHERE u.id = #{id}
27</select>

5.3 一对多关联查询

一对多关联处理一个对象关联多个子对象的情况,例如用户拥有多个订单。

嵌套结果映射方式加载一对多关联:

一对多嵌套结果映射
xml
1<resultMap id="UserWithOrdersMap" type="User">
2 <id property="id" column="user_id"/>
3 <result property="username" column="username"/>
4 <result property="email" column="email"/>
5
6 <!-- 一对多关联 -->
7 <collection property="orders" ofType="Order">
8 <id property="id" column="order_id"/>
9 <result property="orderNo" column="order_no"/>
10 <result property="amount" column="amount"/>
11 <result property="createTime" column="order_create_time"/>
12 <result property="status" column="order_status"/>
13 </collection>
14</resultMap>
15
16<select id="getUserWithOrders" resultMap="UserWithOrdersMap">
17 SELECT
18 u.id as user_id,
19 u.username,
20 u.email,
21 o.id as order_id,
22 o.order_no,
23 o.amount,
24 o.create_time as order_create_time,
25 o.status as order_status
26 FROM user u
27 LEFT JOIN orders o ON u.id = o.user_id
28 WHERE u.id = #{id}
29 ORDER BY o.create_time DESC
30</select>
性能注意事项
  1. N+1问题:嵌套查询可能导致大量额外查询,降低性能
  2. 延迟加载:适当使用fetchType="lazy"可以按需加载关联对象
  3. 结果集过大:复杂嵌套结果映射可能返回大量冗余数据
  4. 内存消耗:多级嵌套映射会消耗更多内存资源

5.4 鉴别器映射

鉴别器映射允许根据某列的值来决定如何映射结果集,类似于Java中的switch语句。

根据类型字段选择不同的映射方式:

鉴别器基础用法
xml
1<resultMap id="VehicleResultMap" type="Vehicle">
2 <id property="id" column="id"/>
3 <result property="name" column="name"/>
4 <result property="price" column="price"/>
5
6 <!-- 根据vehicle_type列值确定映射方式 -->
7 <discriminator javaType="int" column="vehicle_type">
8 <case value="1" resultType="Car">
9 <result property="engineType" column="engine_type"/>
10 <result property="doors" column="doors"/>
11 </case>
12 <case value="2" resultType="Motorcycle">
13 <result property="engineCapacity" column="engine_capacity"/>
14 <result property="hasSideCar" column="has_side_car"/>
15 </case>
16 <case value="3" resultType="Bicycle">
17 <result property="frameType" column="frame_type"/>
18 <result property="gears" column="gears"/>
19 </case>
20 </discriminator>
21</resultMap>
22
23<select id="getVehicleById" resultMap="VehicleResultMap">
24 SELECT
25 id, name, price, vehicle_type,
26 engine_type, doors, engine_capacity,
27 has_side_car, frame_type, gears
28 FROM vehicle
29 WHERE id = #{id}
30</select>

鉴别器映射的主要适用场景:

  1. 继承关系映射:处理不同子类的差异化字段
  2. 多态对象处理:根据类型字段返回不同类型的对象
  3. 条件字段映射:根据状态决定如何处理某些字段
  4. 复杂业务逻辑:根据业务规则选择不同的映射策略
结果映射最佳实践
  1. 优先使用自动映射:当列名符合命名规则时,启用自动映射
  2. 组合使用策略:根据复杂度选择合适的映射方式
  3. 延迟加载:使用fetchType="lazy"避免不必要的数据加载
  4. 结果缓存:为复杂查询启用缓存,提高性能
  5. 预加载关联:对于经常一起使用的关联数据,使用嵌套结果映射

6. 缓存机制与性能优化

MyBatis提供了完善的缓存机制,通过合理使用缓存,可以显著提升查询性能,减少数据库访问次数。MyBatis的缓存分为一级缓存和二级缓存两个层次。

6.1 一级缓存机制

一级缓存是SqlSession级别的缓存,默认启用,生命周期仅限于单个SqlSession。

一级缓存的工作原理:

  1. 作用范围:SqlSession级别,同一个SqlSession中的相同查询会使用缓存
  2. 缓存键:基于Statement ID + SQL + 参数值 + RowBounds计算
  3. 自动管理:默认开启,无需额外配置
  4. 生命周期:SqlSession关闭后缓存失效
一级缓存示例
java
1// 获取SqlSession
2try (SqlSession session = sqlSessionFactory.openSession()) {
3 UserMapper mapper = session.getMapper(UserMapper.class);
4
5 // 第一次查询,访问数据库
6 User user1 = mapper.getUserById(1001L);
7
8 // 第二次查询,使用一级缓存
9 User user2 = mapper.getUserById(1001L);
10
11 // user1和user2是同一个对象引用
12 System.out.println(user1 == user2); // true
13}

6.2 二级缓存配置

二级缓存是Mapper级别的缓存,可以跨越多个SqlSession共享,需要手动启用。

启用二级缓存的步骤:

  1. 全局配置文件中启用缓存
mybatis-config.xml
xml
1<settings>
2 <setting name="cacheEnabled" value="true"/> <!-- 默认就是true -->
3</settings>
  1. 在Mapper XML中配置缓存
UserMapper.xml
xml
1<mapper namespace="com.example.mapper.UserMapper">
2 <!-- 启用该命名空间的二级缓存 -->
3 <cache
4 eviction="LRU" <!-- 缓存淘汰策略:LRU/FIFO/SOFT/WEAK -->
5 flushInterval="60000" <!-- 刷新间隔(毫秒),不设置则不自动刷新 -->
6 size="512" <!-- 缓存对象数量上限 -->
7 readOnly="false"/> <!-- false允许修改对象,但需要可序列化 -->
8
9 <!-- 映射语句... -->
10</mapper>
  1. 确保实体类实现Serializable接口
可序列化实体类
java
1public class User implements Serializable {
2 private static final long serialVersionUID = 1L;
3
4 private Long id;
5 private String username;
6 // 其他属性...
7}

6.3 自定义缓存实现

MyBatis支持自定义缓存实现,可以集成第三方缓存,如Redis、Ehcache等。

自定义缓存需要实现MyBatis的Cache接口:

自定义缓存实现
java
1package com.example.cache;
2
3import org.apache.ibatis.cache.Cache;
4
5public class CustomCache implements Cache {
6 private final String id;
7 private final Map<Object, Object> cache = new ConcurrentHashMap<>();
8
9 public CustomCache(String id) {
10 this.id = id;
11 }
12
13 @Override
14 public String getId() {
15 return id;
16 }
17
18 @Override
19 public void putObject(Object key, Object value) {
20 cache.put(key, value);
21 }
22
23 @Override
24 public Object getObject(Object key) {
25 return cache.get(key);
26 }
27
28 @Override
29 public Object removeObject(Object key) {
30 return cache.remove(key);
31 }
32
33 @Override
34 public void clear() {
35 cache.clear();
36 }
37
38 @Override
39 public int getSize() {
40 return cache.size();
41 }
42}

在Mapper中使用自定义缓存:

使用自定义缓存
xml
1<mapper namespace="com.example.mapper.UserMapper">
2 <cache type="com.example.cache.CustomCache">
3 <property name="cacheType" value="LRU"/>
4 <property name="maxSize" value="1000"/>
5 </cache>
6
7 <!-- 映射语句... -->
8</mapper>

6.4 性能优化策略

除了缓存,MyBatis还有多种性能优化策略,可以显著提高系统性能。

常见的MyBatis性能优化策略:

  1. SQL优化

    • 仅查询必要的列,避免SELECT *
    • 合理使用索引
    • 优化JOIN操作和子查询
    • 分页查询大数据集
  2. 参数处理

    • 使用批量操作减少数据库交互
    • 合理设置PreparedStatement的参数
    • 对于IN条件,限制参数数量
  3. 结果映射

    • 使用ResultMap避免重复定义列映射
    • 合理配置延迟加载
    • 避免复杂的嵌套查询
  4. 配置优化

    • 使用连接池管理数据库连接
    • 优化日志配置,生产环境关闭详细日志
    • 适当设置缓存大小和刷新间隔
SQL优化示例
xml
1<!-- 避免SELECT * -->
2<select id="getUsers" resultType="User">
3 SELECT id, username, email FROM user
4 WHERE status = #{status}
5 LIMIT #{offset}, #{limit}
6</select>
7
8<!-- 使用批量操作 -->
9<insert id="batchInsert" parameterType="list">
10 INSERT INTO user (username, email, status)
11 VALUES
12 <foreach collection="list" item="user" separator=",">
13 (#{user.username}, #{user.email}, #{user.status})
14 </foreach>
15</insert>
性能优化注意事项
  1. 合理使用缓存:缓存不是万能的,需要考虑数据一致性
  2. 避免过度优化:先找出真正的性能瓶颈,再有针对性地优化
  3. 监控与测试:引入性能监控,验证优化效果
  4. 内存消耗:缓存会增加内存使用,需要平衡配置
  5. 定期维护:清理不必要的缓存,执行必要的数据库优化
MyBatis性能优化清单
  1. 使用二级缓存:为频繁查询且很少更新的数据配置缓存
  2. 延迟加载:只在需要时加载复杂关联对象
  3. 批量操作:减少数据库往返次数
  4. 分页查询:避免一次性加载大量数据
  5. SQL优化:编写高效SQL,善用索引
  6. 结果集处理:只查询必要的字段
  7. 连接池配置:优化数据库连接池参数
  8. 避免N+1问题:使用关联查询替代多次单独查询

7. 插件开发与定制扩展

MyBatis提供了强大的插件机制,可以在SQL执行的关键节点进行拦截并添加自定义行为。插件机制是MyBatis灵活性和可扩展性的重要体现。

7.1 插件机制原理

MyBatis插件基于动态代理和责任链模式设计,通过拦截核心组件的方法调用实现功能扩展。

MyBatis提供了四个拦截点,对应核心组件的不同处理阶段:

  1. Executor:负责执行SQL的核心组件,包括创建Statement、参数设置、执行SQL、处理结果等

    • update: 执行插入/更新/删除操作
    • query: 执行查询操作
    • flushStatements: 刷新语句
    • commit: 提交事务
    • rollback: 回滚事务
    • getTransaction: 获取事务
    • close: 关闭executor
    • isClosed: 检查executor是否关闭
  2. StatementHandler:负责处理JDBC Statement的组件

    • prepare: 准备Statement
    • parameterize: 设置参数
    • batch: 批处理
    • update: 执行更新
    • query: 执行查询
  3. ParameterHandler:负责处理SQL参数的组件

    • getParameterObject: 获取参数对象
    • setParameters: 设置参数
  4. ResultSetHandler:负责处理查询结果集的组件

    • handleResultSets: 处理结果集
    • handleOutputParameters: 处理输出参数
拦截点注解示例
java
1@Intercepts({
2 // 拦截Executor的update方法
3 @Signature(
4 type = Executor.class,
5 method = "update",
6 args = {MappedStatement.class, Object.class}
7 ),
8 // 拦截Executor的query方法
9 @Signature(
10 type = Executor.class,
11 method = "query",
12 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
13 ),
14 // 拦截StatementHandler的prepare方法
15 @Signature(
16 type = StatementHandler.class,
17 method = "prepare",
18 args = {Connection.class, Integer.class}
19 )
20})
21public class ExamplePlugin implements Interceptor {
22 // 实现拦截逻辑...
23}

7.2 自定义插件开发

开发MyBatis插件需要实现Interceptor接口,通过拦截点进行功能扩展。以下是常见的插件开发场景。

实现一个MyBatis插件需要完成以下步骤:

  1. 实现org.apache.ibatis.plugin.Interceptor接口
  2. 使用@Intercepts@Signature注解定义拦截点
  3. intercept()方法中实现拦截逻辑
  4. plugin()方法中包装目标对象
  5. 实现setProperties()方法处理插件配置属性
插件基本结构
java
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "update",
5 args = {MappedStatement.class, Object.class}
6 )
7})
8public class ExamplePlugin implements Interceptor {
9
10 private Properties properties = new Properties();
11
12 @Override
13 public Object intercept(Invocation invocation) throws Throwable {
14 // 在方法执行前添加逻辑
15 Object target = invocation.getTarget(); // 目标对象
16 Method method = invocation.getMethod(); // 被拦截的方法
17 Object[] args = invocation.getArgs(); // 方法参数
18
19 // 执行原方法
20 Object result = invocation.proceed();
21
22 // 在方法执行后添加逻辑
23
24 return result;
25 }
26
27 @Override
28 public Object plugin(Object target) {
29 // 包装目标对象为代理对象
30 return Plugin.wrap(target, this);
31 }
32
33 @Override
34 public void setProperties(Properties properties) {
35 // 设置插件属性
36 this.properties = properties;
37 }
38}

7.3 分页插件实现

分页是应用中常见的需求,通过插件机制可以实现统一的分页功能,避免手动编写分页代码。

实现一个基础的分页插件:

自定义分页插件
java
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "query",
5 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
6 )
7})
8public class PaginationPlugin implements Interceptor {
9
10 private String databaseType;
11
12 @Override
13 public Object intercept(Invocation invocation) throws Throwable {
14 Object[] args = invocation.getArgs();
15 MappedStatement ms = (MappedStatement) args[0];
16 Object parameterObject = args[1];
17 RowBounds rowBounds = (RowBounds) args[2];
18
19 // 判断是否需要分页
20 if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
21 // 不需要分页,执行原方法
22 return invocation.proceed();
23 }
24
25 // 获取原始SQL
26 BoundSql boundSql = ms.getBoundSql(parameterObject);
27 String sql = boundSql.getSql().trim();
28
29 // 构建分页SQL
30 String pageSql = buildPageSql(sql, rowBounds);
31
32 // 创建新的BoundSql
33 BoundSql newBoundSql = copyBoundSql(ms, boundSql, pageSql);
34
35 // 创建新的MappedStatement
36 MappedStatement newMs = copyMappedStatement(ms, newBoundSql);
37
38 // 清除RowBounds,避免MyBatis内存分页
39 args[0] = newMs;
40 args[2] = RowBounds.DEFAULT;
41
42 // 执行分页查询
43 return invocation.proceed();
44 }
45
46 /**
47 * 根据数据库类型构建分页SQL
48 */
49 private String buildPageSql(String sql, RowBounds rowBounds) {
50 int offset = rowBounds.getOffset();
51 int limit = rowBounds.getLimit();
52
53 StringBuilder pageSql = new StringBuilder(sql.length() + 100);
54
55 if ("mysql".equalsIgnoreCase(databaseType)) {
56 pageSql.append(sql)
57 .append(" LIMIT ")
58 .append(offset)
59 .append(", ")
60 .append(limit);
61 } else if ("oracle".equalsIgnoreCase(databaseType)) {
62 pageSql.append("SELECT * FROM ( ")
63 .append("SELECT TMP.*, ROWNUM ROW_ID FROM ( ")
64 .append(sql)
65 .append(" ) TMP WHERE ROWNUM <= ")
66 .append(offset + limit)
67 .append(" ) WHERE ROW_ID > ")
68 .append(offset);
69 } else if ("sqlserver".equalsIgnoreCase(databaseType)) {
70 // SQL Server 2012+
71 pageSql.append(sql)
72 .append(" OFFSET ")
73 .append(offset)
74 .append(" ROWS FETCH NEXT ")
75 .append(limit)
76 .append(" ROWS ONLY");
77 } else {
78 // 默认使用MySQL语法
79 pageSql.append(sql)
80 .append(" LIMIT ")
81 .append(offset)
82 .append(", ")
83 .append(limit);
84 }
85
86 return pageSql.toString();
87 }
88
89 /**
90 * 创建新的BoundSql
91 */
92 private BoundSql copyBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
93 // 复制BoundSql的实现...
94 // 实际代码需要复制boundSql的所有属性和参数映射
95
96 return new BoundSql(
97 ms.getConfiguration(),
98 sql,
99 boundSql.getParameterMappings(),
100 boundSql.getParameterObject()
101 );
102 }
103
104 /**
105 * 创建新的MappedStatement
106 */
107 private MappedStatement copyMappedStatement(MappedStatement ms, BoundSql boundSql) {
108 // 复制MappedStatement的实现...
109 // 实际代码需要使用MappedStatement.Builder来构建新的MappedStatement
110
111 return new MappedStatement.Builder(
112 ms.getConfiguration(),
113 ms.getId(),
114 new StaticSqlSource(ms.getConfiguration(), boundSql.getSql(), boundSql.getParameterMappings()),
115 ms.getSqlCommandType()
116 ).cache(ms.getCache())
117 .fetchSize(ms.getFetchSize())
118 .flushCacheRequired(ms.isFlushCacheRequired())
119 .useCache(ms.isUseCache())
120 .resultMaps(ms.getResultMaps())
121 .resultSetType(ms.getResultSetType())
122 .statementType(ms.getStatementType())
123 .timeout(ms.getTimeout())
124 .parameterMap(ms.getParameterMap())
125 .build();
126 }
127
128 @Override
129 public Object plugin(Object target) {
130 return Plugin.wrap(target, this);
131 }
132
133 @Override
134 public void setProperties(Properties properties) {
135 this.databaseType = properties.getProperty("databaseType", "mysql");
136 }
137}

配置分页插件:

插件配置
xml
1<plugin interceptor="com.example.plugin.PaginationPlugin">
2 <property name="databaseType" value="mysql"/>
3</plugin>

使用分页插件:

分页查询
java
1// 创建分页参数(offset, limit)
2RowBounds rowBounds = new RowBounds(20, 10); // 从第21条开始,获取10条
3
4// 执行分页查询
5List<User> users = sqlSession.selectList("getUserList", parameter, rowBounds);

7.4 性能监控插件

通过插件机制可以实现SQL性能监控和统计功能,帮助识别和优化慢查询。

实现一个监控SQL执行时间的插件:

SQL性能监控插件
java
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "update",
5 args = {MappedStatement.class, Object.class}
6 ),
7 @Signature(
8 type = Executor.class,
9 method = "query",
10 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
11 )
12})
13public class SqlPerformancePlugin implements Interceptor {
14
15 private static final Logger logger = LoggerFactory.getLogger(SqlPerformancePlugin.class);
16 private long slowSqlThreshold = 1000; // 慢查询阈值,单位毫秒
17
18 // 记录SQL执行统计信息
19 private final ConcurrentHashMap<String, SqlStats> sqlStatsMap = new ConcurrentHashMap<>();
20
21 @Override
22 public Object intercept(Invocation invocation) throws Throwable {
23 MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
24 Object parameter = invocation.getArgs()[1];
25
26 String sqlId = ms.getId();
27 BoundSql boundSql = ms.getBoundSql(parameter);
28 String sql = boundSql.getSql();
29
30 long startTime = System.currentTimeMillis();
31 Object result = null;
32 boolean success = false;
33
34 try {
35 // 执行SQL
36 result = invocation.proceed();
37 success = true;
38 return result;
39 } catch (Throwable e) {
40 throw e;
41 } finally {
42 long endTime = System.currentTimeMillis();
43 long costTime = endTime - startTime;
44
45 // 更新统计信息
46 updateStats(sqlId, costTime, success);
47
48 // 记录慢查询
49 if (costTime > slowSqlThreshold) {
50 logger.warn("Slow SQL detected: {} ms, SQL ID: {}, SQL: {}", costTime, sqlId, getSqlWithParams(boundSql));
51 }
52 }
53 }
54
55 private void updateStats(String sqlId, long costTime, boolean success) {
56 SqlStats stats = sqlStatsMap.computeIfAbsent(sqlId, k -> new SqlStats());
57
58 stats.incrementCount();
59 stats.addExecutionTime(costTime);
60
61 if (costTime > stats.getMaxTime()) {
62 stats.setMaxTime(costTime);
63 }
64
65 if (costTime < stats.getMinTime() || stats.getMinTime() == 0) {
66 stats.setMinTime(costTime);
67 }
68
69 if (!success) {
70 stats.incrementErrorCount();
71 }
72 }
73
74 private String getSqlWithParams(BoundSql boundSql) {
75 // 获取SQL语句和参数的实现
76 // 实际代码需要解析参数映射和参数对象
77
78 return boundSql.getSql();
79 }
80
81 /**
82 * 获取SQL统计信息
83 */
84 public Map<String, SqlStats> getSqlStats() {
85 return new HashMap<>(sqlStatsMap);
86 }
87
88 /**
89 * 重置统计信息
90 */
91 public void resetStats() {
92 sqlStatsMap.clear();
93 }
94
95 @Override
96 public Object plugin(Object target) {
97 return Plugin.wrap(target, this);
98 }
99
100 @Override
101 public void setProperties(Properties properties) {
102 String threshold = properties.getProperty("slowSqlThreshold");
103 if (threshold != null) {
104 this.slowSqlThreshold = Long.parseLong(threshold);
105 }
106 }
107
108 /**
109 * SQL统计信息类
110 */
111 public static class SqlStats {
112 private final AtomicLong count = new AtomicLong(0);
113 private final AtomicLong totalTime = new AtomicLong(0);
114 private final AtomicLong errorCount = new AtomicLong(0);
115 private volatile long minTime = 0;
116 private volatile long maxTime = 0;
117
118 public void incrementCount() {
119 count.incrementAndGet();
120 }
121
122 public void addExecutionTime(long time) {
123 totalTime.addAndGet(time);
124 }
125
126 public void incrementErrorCount() {
127 errorCount.incrementAndGet();
128 }
129
130 public long getCount() {
131 return count.get();
132 }
133
134 public long getTotalTime() {
135 return totalTime.get();
136 }
137
138 public long getErrorCount() {
139 return errorCount.get();
140 }
141
142 public double getAverageTime() {
143 long countVal = count.get();
144 return countVal == 0 ? 0 : (double) totalTime.get() / countVal;
145 }
146
147 public long getMinTime() {
148 return minTime;
149 }
150
151 public void setMinTime(long minTime) {
152 this.minTime = minTime;
153 }
154
155 public long getMaxTime() {
156 return maxTime;
157 }
158
159 public void setMaxTime(long maxTime) {
160 this.maxTime = maxTime;
161 }
162 }
163}

配置性能监控插件:

插件配置
xml
1<plugin interceptor="com.example.plugin.SqlPerformancePlugin">
2 <property name="slowSqlThreshold" value="1000"/>
3</plugin>

使用监控插件收集的统计信息:

获取性能统计数据
java
1@RestController
2@RequestMapping("/admin/monitoring")
3public class MonitoringController {
4
5 @Autowired
6 private SqlPerformancePlugin sqlPerformancePlugin;
7
8 @GetMapping("/sql-stats")
9 public Map<String, SqlStats> getSqlStats() {
10 return sqlPerformancePlugin.getSqlStats();
11 }
12
13 @PostMapping("/reset-stats")
14 public ResponseEntity<Void> resetStats() {
15 sqlPerformancePlugin.resetStats();
16 return ResponseEntity.ok().build();
17 }
18}
插件开发最佳实践
  1. 专注单一功能:每个插件只做一件事,便于维护和组合
  2. 性能考虑:插件会影响SQL执行性能,应尽量减少开销
  3. 异常处理:确保在异常情况下正确传递异常,不干扰正常流程
  4. 线程安全:多线程环境下插件需要注意线程安全
  5. 插件顺序:多个插件同时使用时,注意配置顺序(先配置的后执行)
  6. 可配置性:提供足够的配置选项,适应不同场景
  7. 优先使用现成插件:对于常见功能,优先使用成熟的社区插件

8. MyBatis实战应用场景

MyBatis在实际项目中有着广泛的应用,从简单的CRUD操作到复杂的多表查询、存储过程调用、批量处理等场景都能很好地支持。本节将展示MyBatis在各种实战场景中的应用。

8.1 多表复杂查询

在实际业务中,经常需要进行多表关联查询,MyBatis提供了多种方式处理复杂查询。

使用JOIN进行多表关联查询:

多表JOIN查询
xml
1<select id="getOrderDetails" resultMap="OrderDetailMap">
2 SELECT
3 o.id AS order_id,
4 o.order_no,
5 o.create_time,
6 o.status,
7 o.total_amount,
8 u.id AS user_id,
9 u.username,
10 u.email,
11 p.id AS product_id,
12 p.name AS product_name,
13 p.price,
14 oi.quantity,
15 oi.subtotal
16 FROM orders o
17 LEFT JOIN user u ON o.user_id = u.id
18 LEFT JOIN order_item oi ON o.id = oi.order_id
19 LEFT JOIN product p ON oi.product_id = p.id
20 WHERE o.id = #{orderId}
21</select>
22
23<resultMap id="OrderDetailMap" type="OrderDetail">
24 <id property="orderId" column="order_id"/>
25 <result property="orderNo" column="order_no"/>
26 <result property="createTime" column="create_time"/>
27 <result property="status" column="status"/>
28 <result property="totalAmount" column="total_amount"/>
29
30 <association property="user" javaType="User">
31 <id property="id" column="user_id"/>
32 <result property="username" column="username"/>
33 <result property="email" column="email"/>
34 </association>
35
36 <collection property="items" ofType="OrderItemDetail">
37 <id property="productId" column="product_id"/>
38 <result property="productName" column="product_name"/>
39 <result property="price" column="price"/>
40 <result property="quantity" column="quantity"/>
41 <result property="subtotal" column="subtotal"/>
42 </collection>
43</resultMap>

8.2 存储过程调用

MyBatis支持调用数据库存储过程,为特定场景提供高效的处理能力。

调用不带参数的简单存储过程:

简单存储过程调用
xml
1<select id="callGetSystemDate" statementType="CALLABLE" resultType="Date">
2 {call get_system_date()}
3</select>

调用带输入参数的存储过程:

带输入参数的存储过程
xml
1<select id="getUserOrders" parameterType="int" statementType="CALLABLE" resultMap="OrderMap">
2 {call get_user_orders(#{userId,jdbcType=INTEGER})}
3</select>

8.3 批量数据处理

在处理大量数据时,批量操作可以显著提高性能,减少数据库交互次数。

使用foreach实现批量插入:

批量插入
xml
1<insert id="batchInsertUsers" parameterType="list">
2 INSERT INTO user (username, email, password, status, create_time)
3 VALUES
4 <foreach collection="list" item="user" separator=",">
5 (
6 #{user.username},
7 #{user.email},
8 #{user.password},
9 #{user.status},
10 #{user.createTime}
11 )
12 </foreach>
13</insert>

8.4 动态权限SQL

在企业应用中,数据权限控制是一个常见需求,MyBatis的动态SQL特性可以很好地实现基于用户角色和权限的动态查询。

实现基于用户部门的数据权限控制:

部门数据权限SQL
xml
1<select id="getUserList" resultType="User">
2 SELECT u.* FROM sys_user u
3 LEFT JOIN sys_dept d ON u.dept_id = d.id
4 <where>
5 <!-- 根据用户权限类型动态生成SQL -->
6 <choose>
7 <!-- 全部数据权限 -->
8 <when test="user.dataScope == 'ALL'">
9 </when>
10
11 <!-- 本部门及以下数据权限 -->
12 <when test="user.dataScope == 'DEPT_AND_CHILD'">
13 AND (d.id = #{user.deptId} OR d.parent_ids LIKE CONCAT('%,', #{user.deptId}, ',%'))
14 </when>
15
16 <!-- 本部门数据权限 -->
17 <when test="user.dataScope == 'DEPT'">
18 AND d.id = #{user.deptId}
19 </when>
20
21 <!-- 仅个人数据权限 -->
22 <when test="user.dataScope == 'SELF'">
23 AND u.create_by = #{user.userId}
24 </when>
25
26 <!-- 自定义数据权限 -->
27 <when test="user.dataScope == 'CUSTOM' and user.deptIds != null and user.deptIds.size() > 0">
28 AND d.id IN
29 <foreach collection="user.deptIds" item="deptId" open="(" separator="," close=")">
30 #{deptId}
31 </foreach>
32 </when>
33
34 <!-- 默认无权限 -->
35 <otherwise>
36 AND 1=2
37 </otherwise>
38 </choose>
39
40 <!-- 其他查询条件 -->
41 <if test="username != null and username != ''">
42 AND u.username LIKE CONCAT('%', #{username}, '%')
43 </if>
44 <if test="status != null">
45 AND u.status = #{status}
46 </if>
47 </where>
48 ORDER BY u.create_time DESC
49</select>

9. 总结与最佳实践

经过前面章节的学习,我们已经全面了解了MyBatis的各个方面。在实际项目中,为了更好地使用MyBatis,需要遵循一些最佳实践和规范,以确保代码质量和系统性能。

9.1 MyBatis使用规范

制定合理的MyBatis使用规范可以提高代码质量、可维护性和性能。

良好的命名规范有助于提高代码可读性和可维护性:

  1. 命名空间(namespace):使用完整的包名+类名,保持与Mapper接口一致

    xml
    1<mapper namespace="com.example.mapper.UserMapper">
  2. SQL语句ID:使用有意义的名称,动词+名词形式,与Mapper接口方法名一致

    xml
    1<select id="getUserById">...</select>
    2<insert id="insertUser">...</insert>
    3<update id="updateUserStatus">...</update>
    4<delete id="deleteUserBatch">...</delete>
  3. 参数名称:使用有意义的参数名,与业务实体属性名保持一致

    xml
    1#{userId}, #{userName}, #{startTime}, #{endTime}
  4. 结果映射ID:使用Entity名+Result/Map后缀

    xml
    1<resultMap id="UserResultMap" type="User">...</resultMap>
    2<resultMap id="OrderDetailMap" type="OrderDetail">...</resultMap>
  5. SQL片段ID:使用功能描述+Fragment/Column/Where后缀

    xml
    1<sql id="BaseColumnList">...</sql>
    2<sql id="CommonWhereFragment">...</sql>

9.2 常见问题解决方案

在使用MyBatis过程中,经常会遇到一些常见问题,这里提供一些解决方案。

N+1查询问题是MyBatis中最常见的性能问题之一:

问题描述:当查询一个列表数据,然后遍历列表查询关联数据时,会产生N+1次查询。

java
1// 产生N+1查询的代码
2List<User> users = userMapper.getAllUsers(); // 1次查询
3for (User user : users) {
4 List<Order> orders = orderMapper.getOrdersByUserId(user.getId()); // N次查询
5 user.setOrders(orders);
6}

解决方案

  1. 使用嵌套结果映射(推荐)

    xml
    1<select id="getUsersWithOrders" resultMap="UserWithOrdersMap">
    2 SELECT u.*, o.* FROM user u LEFT JOIN orders o ON u.id = o.user_id
    3</select>
  2. 使用延迟加载(适用于关联数据使用率低的场景)

    xml
    1<!-- 配置启用延迟加载 -->
    2<setting name="lazyLoadingEnabled" value="true"/>
    3<setting name="aggressiveLazyLoading" value="false"/>
    4
    5<resultMap id="userMap" type="User">
    6 <id property="id" column="id"/>
    7 <result property="name" column="name"/>
    8 <collection property="orders" select="getOrdersByUserId" column="id" fetchType="lazy"/>
    9</resultMap>
  3. 使用批量查询

    java
    1// 先获取用户列表
    2List<User> users = userMapper.getAllUsers();
    3
    4// 提取所有用户ID
    5List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
    6
    7// 批量查询订单
    8Map<Long, List<Order>> orderMap = orderMapper.getOrdersByUserIds(userIds).stream()
    9 .collect(Collectors.groupingBy(Order::getUserId));
    10
    11// 设置订单数据
    12users.forEach(user -> user.setOrders(orderMap.getOrDefault(user.getId(), Collections.emptyList())));

9.3 MyBatis技术栈扩展

MyBatis生态系统非常丰富,有多种扩展和工具可以简化开发,提高效率。

MyBatis-Plus是基于MyBatis的增强工具,简化了开发,提供了更多强大的功能。

主要特性

  1. 通用CRUD:无需编写SQL即可实现基本CRUD操作

    java
    1@Mapper
    2public interface UserMapper extends BaseMapper<User> {
    3 // 已内置通用CRUD方法
    4 // insert, deleteById, updateById, selectById, selectList等
    5}
    6
    7// 使用示例
    8userMapper.insert(user);
    9userMapper.selectById(1);
    10userMapper.deleteById(1);
  2. 条件构造器:强大的查询条件构造

    java
    1// 查询条件构造
    2LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    3queryWrapper
    4 .eq(User::getStatus, 1)
    5 .like(User::getUsername, "张")
    6 .between(User::getCreateTime, startDate, endDate)
    7 .orderByDesc(User::getCreateTime);
    8
    9List<User> users = userMapper.selectList(queryWrapper);
  3. 分页插件:简化分页查询

    java
    1// 配置
    2@Bean
    3public MybatisPlusInterceptor mybatisPlusInterceptor() {
    4 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    5 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    6 return interceptor;
    7}
    8
    9// 使用
    10Page<User> page = new Page<>(1, 10);
    11Page<User> resultPage = userMapper.selectPage(page, queryWrapper);
    12
    13// 获取分页信息
    14long total = resultPage.getTotal();
    15long pages = resultPage.getPages();
    16List<User> records = resultPage.getRecords();
  4. 自动填充:自动处理创建时间、更新时间等字段

    java
    1@TableField(fill = FieldFill.INSERT)
    2private LocalDateTime createTime;
    3
    4@TableField(fill = FieldFill.INSERT_UPDATE)
    5private LocalDateTime updateTime;
    6
    7// 元数据处理器
    8@Component
    9public class MyMetaObjectHandler implements MetaObjectHandler {
    10 @Override
    11 public void insertFill(MetaObject metaObject) {
    12 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    13 this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    14 }
    15
    16 @Override
    17 public void updateFill(MetaObject metaObject) {
    18 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    19 }
    20}
  5. 代码生成器:快速生成Entity、Mapper、Service、Controller

    java
    1// 代码生成示例
    2AutoGenerator generator = new AutoGenerator();
    3
    4// 数据源配置
    5generator.setDataSource(dataSourceConfig);
    6
    7// 全局配置
    8generator.setGlobalConfig(globalConfig);
    9
    10// 包配置
    11generator.setPackageInfo(packageConfig);
    12
    13// 策略配置
    14generator.setStrategy(strategyConfig);
    15
    16// 执行
    17generator.execute();
  6. 乐观锁插件:支持乐观锁机制

    java
    1@Version
    2private Integer version;
    3
    4// 配置乐观锁插件
    5@Bean
    6public MybatisPlusInterceptor mybatisPlusInterceptor() {
    7 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    8 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    9 return interceptor;
    10}
MyBatis最佳实践总结
  1. 命名规范:保持SQL ID与Mapper接口方法名一致,使用有意义的命名
  2. 模块化:按业务模块组织代码,提取公共SQL片段
  3. SQL优化:只查询必要的列,合理使用索引,避免过多JOIN
  4. 安全防护:使用参数绑定防止SQL注入,敏感数据加密处理
  5. 性能优化:合理使用缓存,避免N+1查询,批量处理大量数据
  6. 集成扩展:根据需求选择合适的MyBatis扩展工具
  7. 面向接口:遵循面向接口编程原则,降低代码耦合度
  8. 单一职责:每个Mapper接口专注于单一实体的操作
  9. 测试覆盖:编写单元测试,确保SQL语句正确
  10. 版本管理:使用数据库版本管理工具(如Flyway、Liquibase)管理数据库变更

10. 面试题精选

MyBatis是Java面试中常见的持久层框架话题,以下是一些常见面试题及其详细解答。

10.1 基础原理面试题

Q: MyBatis的核心工作原理是什么?

A: MyBatis的核心工作原理可以概括为:

  1. 配置解析阶段

    • 读取XML配置文件或注解配置
    • 创建Configuration对象,包含所有配置信息
    • 解析映射文件,创建MappedStatement对象
  2. 初始化阶段

    • 创建SqlSessionFactory实例
    • 注册所有Mapper接口
  3. 执行阶段

    • 通过SqlSessionFactory创建SqlSession
    • 通过动态代理创建Mapper接口实现类
    • 执行SQL,映射结果

这个过程的核心是通过动态代理将接口方法调用转换为SQL执行,再将结果集映射为Java对象。

Q: SqlSession和SqlSessionFactory的区别是什么?

A:

  • SqlSessionFactory

    • 负责创建SqlSession实例
    • 线程安全,可供多个线程共享
    • 全局唯一,应用级生命周期
    • 类似于数据库连接池,是一个重量级对象
  • SqlSession

    • 提供执行SQL的API
    • 非线程安全,不能在多线程中共享
    • 会话级生命周期,使用后需关闭
    • 类似于数据库连接,是一个轻量级对象

Q: MyBatis的工作流程是怎样的?

A: MyBatis完整工作流程如下:

  1. 读取配置文件,创建SqlSessionFactoryBuilder对象
  2. SqlSessionFactoryBuilder解析配置,创建SqlSessionFactory对象
  3. 从SqlSessionFactory获取SqlSession
  4. 通过SqlSession获取Mapper接口的代理实现
  5. 通过Mapper接口调用方法
  6. SqlSession将请求转发给Executor
  7. Executor通过StatementHandler创建Statement对象
  8. ParameterHandler设置参数
  9. Statement执行SQL
  10. ResultSetHandler处理结果集,转换为Java对象
  11. 返回结果,关闭SqlSession

10.2 配置与映射面试题

Q: #和$的区别是什么?

A: 这两种符号用于SQL参数传递,但有本质区别:

  • #

    • 参数占位符,使用PreparedStatement
    • 会自动添加引号(如字符串)
    • 防止SQL注入
    • 支持类型转换
    • 例:WHERE id = #{id}WHERE id = ?
  • $

    • 文本替换,直接拼接SQL
    • 不会添加引号
    • 存在SQL注入风险
    • 不支持类型转换
    • 例:ORDER BY ${column}ORDER BY column_name

何时使用$

  • 动态表名:FROM ${tableName}
  • 动态列名:ORDER BY ${columnName}
  • 动态SQL片段:${sqlFragment}

安全建议:尽量使用#,必须使用$时要进行严格参数校验。

Q: MyBatis的配置文件中有哪些重要元素?

A: MyBatis配置文件的重要元素(按顺序):

  1. properties:定义可重用的属性
  2. settings:修改MyBatis行为的重要设置
  3. typeAliases:为Java类型设置别名
  4. typeHandlers:类型处理器,处理Java与JDBC类型转换
  5. objectFactory:创建结果对象的工厂
  6. plugins:插件拦截器
  7. environments:环境配置
    • environment:特定环境配置
    • transactionManager:事务管理器配置
    • dataSource:数据源配置
  8. databaseIdProvider:数据库厂商标识
  9. mappers:映射器配置

Q: MyBatis的常用设置有哪些?

A: 常用设置:

  1. cacheEnabled:是否启用缓存(默认true)
  2. lazyLoadingEnabled:是否开启延迟加载(默认false)
  3. aggressiveLazyLoading:是否积极加载对象所有属性(默认false)
  4. multipleResultSetsEnabled:是否允许单一语句返回多结果集(默认true)
  5. useColumnLabel:使用列标签代替列名(默认true)
  6. useGeneratedKeys:使用JDBC的getGeneratedKeys(默认false)
  7. autoMappingBehavior:自动映射结果集(NONE/PARTIAL/FULL)
  8. defaultExecutorType:默认执行器类型(SIMPLE/REUSE/BATCH)
  9. mapUnderscoreToCamelCase:下划线转驼峰命名(默认false)
  10. localCacheScope:本地缓存作用域(SESSION/STATEMENT)
  11. jdbcTypeForNull:NULL值的JDBC类型(默认OTHER)
  12. logImpl:指定日志实现(SLF4J/LOG4J/LOG4J2等)

10.3 缓存与性能面试题

Q: MyBatis的缓存机制是怎样的?

A: MyBatis有两级缓存机制:

  1. 一级缓存

    • SqlSession级别的缓存
    • 默认开启,无法关闭
    • 作用范围:同一SqlSession内
    • 生命周期:SqlSession关闭时结束
    • 清除条件:
      • 执行UPDATE、INSERT、DELETE操作
      • 调用clearCache()方法
      • 调用close()方法
      • 事务提交或回滚操作
  2. 二级缓存

    • Mapper级别的缓存(namespace)
    • 默认关闭,需手动配置
    • 作用范围:同一namespace内的所有SqlSession共享
    • 生命周期:应用运行期间
    • 启用方式:
      xml
      1<!-- 全局配置 -->
      2<setting name="cacheEnabled" value="true"/>
      3
      4<!-- Mapper配置 -->
      5<cache
      6 eviction="LRU"
      7 flushInterval="60000"
      8 size="512"
      9 readOnly="false"/>

Q: 什么情况下不应使用二级缓存?

A: 以下情况不适合使用二级缓存:

  1. 写操作频繁的场景
  2. 多表关联查询(可能导致脏数据)
  3. 分布式系统(缓存不一致)
  4. 需要实时数据的场景
  5. 对数据一致性要求高的业务
  6. 表之间存在复杂关联关系

在这些情况下,建议使用Redis等外部缓存代替MyBatis内置的二级缓存。

Q: MyBatis的一级缓存为什么会出现脏读?如何避免?

A: 一级缓存可能导致脏读的原因:

  1. 同一SqlSession内,两次查询之间数据被其他会话修改
  2. 缓存中的对象被程序修改,但未同步到数据库

避免一级缓存脏读的方法:

  1. 调整缓存级别为STATEMENT:
    xml
    1<setting name="localCacheScope" value="STATEMENT"/>
  2. 关键查询前清空缓存:
    java
    1sqlSession.clearCache();
  3. 避免共享SqlSession
  4. 敏感查询使用flushCache="true":
    xml
    1<select id="getUser" flushCache="true">
    2 SELECT * FROM user WHERE id = #{id}
    3</select>

10.4 插件与扩展面试题

Q: MyBatis的插件机制是如何工作的?

A: MyBatis插件机制基于拦截器模式和责任链模式:

  1. 原理:使用JDK动态代理,在四大核心对象的方法调用前后进行拦截

  2. 拦截点

    • Executor:执行器,负责SQL执行
    • ParameterHandler:参数处理器
    • ResultSetHandler:结果集处理器
    • StatementHandler:语句处理器
  3. 实现步骤

    • 实现Interceptor接口
    • 使用@Intercepts和@Signature注解定义拦截点
    • 实现intercept方法处理拦截逻辑
    • 在配置文件中注册插件
  4. 工作流程

    • 在MyBatis初始化时,解析插件配置
    • 创建拦截器链InterceptorChain
    • 当创建四大核心对象时,调用InterceptorChain.pluginAll()包装对象
    • 执行目标方法时,按插件注册顺序逆序执行拦截器

Q: 如何开发一个自定义MyBatis插件?

A: 开发自定义插件的步骤:

  1. 定义插件类
java
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "update",
5 args = {MappedStatement.class, Object.class}
6 ),
7 @Signature(
8 type = Executor.class,
9 method = "query",
10 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
11 )
12})
13public class ExamplePlugin implements Interceptor {
14
15 private Properties properties;
16
17 @Override
18 public Object intercept(Invocation invocation) throws Throwable {
19 // 执行前处理
20 Object target = invocation.getTarget(); // 被代理对象
21 Method method = invocation.getMethod(); // 被拦截方法
22 Object[] args = invocation.getArgs(); // 方法参数
23
24 // 执行原方法
25 Object result = invocation.proceed();
26
27 // 执行后处理
28
29 return result;
30 }
31
32 @Override
33 public Object plugin(Object target) {
34 // 包装目标对象
35 return Plugin.wrap(target, this);
36 }
37
38 @Override
39 public void setProperties(Properties properties) {
40 // 设置插件属性
41 this.properties = properties;
42 }
43}
  1. 注册插件
xml
1<plugins>
2 <plugin interceptor="com.example.plugin.ExamplePlugin">
3 <property name="someProperty" value="100"/>
4 </plugin>
5</plugins>

Q: 为什么MyBatis的插件只能拦截这四种接口?

A: MyBatis只允许拦截这四个接口是基于以下考虑:

  1. 架构设计:这四个接口是MyBatis执行SQL的核心组件,覆盖了SQL执行的完整生命周期
  2. 安全性:限制拦截点减少对核心功能的干扰
  3. 可控性:避免过度的扩展导致框架行为不可预测
  4. 性能考虑:减少代理层级,降低性能开销

这四个接口足以满足大多数扩展需求:

  • Executor:整体SQL执行过程(缓存、事务等)
  • StatementHandler:SQL语句处理(预处理、执行)
  • ParameterHandler:参数设置(类型转换、参数映射)
  • ResultSetHandler:结果集处理(结果映射、对象创建)

如需更深层次的定制,可以通过继承或修改MyBatis源码实现。

面试技巧
  1. 理解原理:深入理解MyBatis的核心原理和执行流程
  2. 对比框架:了解MyBatis与其他ORM框架的区别和适用场景
  3. 性能优化:掌握常见的性能问题和优化方法
  4. 实践案例:准备实际项目中的使用案例和问题解决方案
  5. 源码分析:对关键组件的源码有基本了解
  6. 扩展机制:熟悉插件机制和常用扩展

评论