Skip to main content

Java MyBatis 面试题集

总题数: 17道 | 重点领域: ORM、缓存、动态SQL | 难度分布: 中级

本文档整理了 Java MyBatis 的完整17道面试题目,涵盖ORM映射、缓存机制、动态SQL等各个方面。


面试题目列表

1. 说说 MyBatis 的缓存机制?

MyBatis 提供了两级缓存机制来提高查询性能:

一级缓存(本地缓存)

  • 默认开启,无法关闭
  • SqlSession级别的缓存,存储在SqlSession对象中
  • 缓存范围:同一个SqlSession中执行的相同SQL查询会被缓存
  • 生命周期:随SqlSession的创建和销毁而同时存在

一级缓存失效情况

  • 调用SqlSession的clearCache()方法
  • 调用SqlSession的commit()、rollback()方法
  • 执行INSERT、UPDATE、DELETE操作,会清空与该表相关的一级缓存
  • 不同的SqlSession之间缓存数据互不影响

二级缓存(全局缓存)

  • 默认未开启,需要手动配置
  • Mapper级别的缓存,被同一namespace下的所有SqlSession共享
  • 跨SqlSession可用,缓存范围更广

二级缓存启用步骤

  1. 在MyBatis配置文件中开启二级缓存:

    xml
    1<settings>
    2 <setting name="cacheEnabled" value="true"/>
    3</settings>
  2. 在Mapper映射文件中配置缓存:

    xml
    1<cache
    2 eviction="LRU"
    3 flushInterval="60000"
    4 size="1024"
    5 readOnly="true"/>
  3. 实体类需要实现Serializable接口

二级缓存失效情况

  • 执行INSERT、UPDATE、DELETE操作,会清空与该表相关的二级缓存
  • 缓存超过了flushInterval设置的时间
  • 缓存容量达到上限时,会按策略(如LRU)淘汰旧数据

缓存淘汰策略

  • LRU(最近最少使用):默认策略,移除最长时间未被使用的缓存
  • FIFO(先进先出):按对象进入缓存的顺序来移除
  • SOFT(软引用):基于垃圾回收器的软引用规则
  • WEAK(弱引用):基于垃圾回收器的弱引用规则

自定义缓存

  • MyBatis支持自定义缓存实现,通过实现Cache接口
  • 可以集成第三方缓存如EhCache、Redis等

缓存使用建议

  • 对查询频率高、变化少的数据使用缓存
  • 在多表关联查询中应谨慎使用缓存
  • 考虑使用专业缓存框架如Redis替代MyBatis的二级缓存

2. 能详细说说 MyBatis 的执行流程吗?

MyBatis的执行流程主要包括以下步骤:

1. 初始化阶段

  • 加载MyBatis全局配置文件(mybatis-config.xml)
  • 解析配置文件,创建Configuration对象
  • 加载Mapper映射文件(*.xml)或注解
  • 解析SQL语句,创建MappedStatement对象
  • 创建SqlSessionFactory对象

2. 会话创建阶段

  • 通过SqlSessionFactory创建SqlSession实例
  • SqlSession是MyBatis操作的核心接口,代表一次数据库会话

3. 数据读写阶段

  • 用户通过SqlSession调用Mapper接口方法
  • MyBatis将方法调用解析为具体的MappedStatement
  • 根据具体操作类型选择对应的Executor执行器
  • 创建StatementHandler处理SQL语句预编译
  • 创建ParameterHandler处理参数映射
  • 执行SQL语句
  • 通过ResultSetHandler处理结果集映射
  • 返回映射后的结果对象

4. 会话关闭阶段

  • 提交或回滚事务
  • 关闭SqlSession,释放资源

核心组件

  1. Configuration:保存MyBatis所有配置信息
  2. SqlSessionFactory:创建SqlSession的工厂
  3. SqlSession:数据库会话,提供数据库操作接口
  4. Executor:SQL执行器,负责SQL语句的执行
  5. StatementHandler:封装JDBC Statement操作
  6. ParameterHandler:处理SQL参数
  7. ResultSetHandler:处理结果集映射
  8. TypeHandler:处理Java类型与JDBC类型之间的映射
  9. MappedStatement:对应映射文件中的SQL节点

流程图

1加载配置文件 → 解析配置文件 → 创建Configuration对象 → 创建SqlSessionFactory
2
3创建SqlSession → 获取Mapper代理对象 → 执行SQL
4
5Executor执行器 → StatementHandler处理语句 → ParameterHandler设置参数
6
7执行SQL → ResultSetHandler处理结果集 → 返回结果对象 → 关闭SqlSession

执行过程详解

  1. 客户端调用Mapper接口方法
  2. 根据方法的全限定名找到对应的MappedStatement
  3. 通过SqlSession将请求转发给Executor
  4. Executor根据具体的SQL类型,调用query、update等方法
  5. 创建JDBC Statement对象,设置参数
  6. 执行SQL语句,获取结果集
  7. 将结果集映射为Java对象
  8. 返回处理结果

3. MyBatis 与 Hibernate 有哪些不同?

MyBatis和Hibernate都是优秀的ORM框架,但它们在设计理念和使用方式上有显著差异:

1. 映射方式

  • Hibernate:完全面向对象的映射,开发者无需编写SQL
  • MyBatis:半自动映射,需要自己编写SQL语句

2. SQL控制

  • Hibernate:自动生成SQL,开发者很少直接接触SQL
  • MyBatis:手写SQL,对SQL的控制度高,可优化SQL性能

3. 学习曲线

  • Hibernate:学习曲线较陡,需要理解复杂的映射概念和缓存机制
  • MyBatis:学习曲线平缓,熟悉SQL的开发者可以快速上手

4. 数据库适配

  • Hibernate:通过方言支持多种数据库,可无缝切换数据库
  • MyBatis:需要针对不同数据库编写不同的SQL

5. 缓存机制

  • Hibernate:完善的一级、二级缓存机制
  • MyBatis:简单的一级、二级缓存,但可扩展性强

6. 开发效率

  • Hibernate:对于简单CRUD操作,开发效率高
  • MyBatis:需要编写SQL,但对复杂查询控制更灵活

7. 性能调优

  • Hibernate:性能调优复杂,需要深入理解内部机制
  • MyBatis:通过优化SQL可直接提升性能,调优相对简单

8. 适用场景

  • Hibernate:适合ORM映射为主的场景,如领域模型复杂的系统
  • MyBatis:适合SQL优化要求高的场景,如复杂报表查询、多表关联

对比表格

特性MyBatisHibernate
映射方式SQL映射对象映射
SQL控制手动编写SQL自动生成SQL
学习难度
开发效率手写SQL,效率较低不写SQL,效率高
灵活性高,可优化SQL低,难以优化SQL
数据库移植较差,需改SQL较好,通过方言支持
缓存机制简单完善
适用场景性能要求高、SQL复杂领域模型复杂

选择建议

  • 如果项目需要精细控制SQL性能,选择MyBatis
  • 如果项目重在对象模型和业务逻辑,选择Hibernate
  • 在一些项目中,两者可以结合使用:简单操作用Hibernate,复杂查询用MyBatis

4. MyBatis 中 井花括号 和 美元花括号 的区别是什么?

MyBatis中的#{}${}都是用于参数替换的符号,但它们有本质上的区别:

井花括号(预编译参数)

  • 会被预编译为?占位符,然后通过PreparedStatement设置参数值
  • 可以防止SQL注入攻击
  • 自动进行类型转换和值转义
  • 对所有数据类型都有效

美元花括号(文本替换)

  • 直接替换为参数值,不会被预编译
  • 不能防止SQL注入攻击
  • 不会自动进行类型转换和转义
  • 主要用于动态表名、列名等不能使用预编译的场景

代码示例

xml
1<!-- 使用#{} -->
2<select id="getUserById" resultType="User">
3 SELECT * FROM user WHERE id = #{id}
4</select>
5<!-- 编译后的SQL: SELECT * FROM user WHERE id = ? -->
6
7<!-- 使用${} -->
8<select id="getUsersByTableName" resultType="User">
9 SELECT * FROM ${tableName}
10</select>
11<!-- 编译后的SQL: SELECT * FROM actual_table_name -->

适用场景

  • 井花括号:用于SQL语句中的值部分,例如WHERE条件值、INSERT的值等
  • 美元花括号:用于不能使用预编译的部分,如表名、列名、ORDER BY子句等

安全性对比: 假设参数值为1; DROP TABLE users;

  • 使用井花括号:WHERE id = ?,参数被安全处理为'1; DROP TABLE users;'
  • 使用美元花括号:WHERE id = 1; DROP TABLE users;,可能导致删表操作被执行

性能对比

  • 井花括号:使用预编译,可以重用预编译的SQL,性能更好
  • 美元花括号:每次都是新SQL,无法使用预编译缓存

最佳实践

  1. 默认情况下优先使用井花括号
  2. 只在必须的场景下使用美元花括号(如动态表名)
  3. 使用美元花括号时,必须严格控制参数来源,避免SQL注入
  4. 对于用户输入的数据,永远不要使用美元花括号直接拼接

5. MyBatis 写个 Xml 映射文件,再写个 DAO 接口就能执行,这个原理是什么?

MyBatis通过动态代理机制实现了从DAO接口到XML映射文件的关联执行。主要原理包括:

1. 接口与映射文件绑定

  • MyBatis通过namespace将Mapper接口与XML映射文件关联起来
  • 接口的全限定名必须与XML的namespace值完全一致
  • 接口中的方法名必须与XML中的SQL ID一致
  • 接口方法的参数和返回类型必须与SQL语句匹配

2. 动态代理机制

  • MyBatis使用JDK动态代理为接口创建代理对象
  • 当调用接口方法时,代理对象会拦截调用
  • 根据接口的全限定名和方法名查找对应的SQL语句
  • 执行SQL并返回结果

3. 核心代码流程

java
1// 1. 创建SqlSessionFactory
2SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
3
4// 2. 获取SqlSession
5SqlSession session = factory.openSession();
6
7// 3. 获取Mapper接口的代理实现
8UserMapper mapper = session.getMapper(UserMapper.class);
9
10// 4. 调用接口方法(实际由动态代理执行)
11User user = mapper.getUserById(1);

4. 代理创建过程

  • 在SqlSession.getMapper()中,调用Configuration.getMapper()
  • Configuration委托MapperRegistry.getMapper()处理
  • MapperRegistry通过MapperProxyFactory创建代理实例
  • 最终使用JDK的Proxy.newProxyInstance()创建代理对象
  • 代理对象使用MapperProxy作为InvocationHandler处理调用

5. 方法调用过程

  1. 调用接口方法时,进入MapperProxy的invoke()方法
  2. 解析方法签名,找到对应的MapperMethod
  3. MapperMethod根据方法类型(CRUD)调用SqlSession的相应方法
  4. SqlSession通过Executor执行SQL并处理结果
  5. 将结果转换为方法的返回类型并返回

6. 配置示例

java
1// 接口定义
2public interface UserMapper {
3 User getUserById(Integer id);
4}
xml
1<!-- XML映射文件 -->
2<mapper namespace="com.example.UserMapper">
3 <select id="getUserById" resultType="User">
4 SELECT * FROM user WHERE id = #{id}
5 </select>
6</mapper>

7. 关键约定

  • namespace必须是接口的全限定名
  • id必须是接口方法名
  • 参数和返回类型必须匹配
  • XML文件一般与接口同名(非强制,但推荐)

8. 注解替代XML: MyBatis也支持使用注解代替XML配置:

java
1public interface UserMapper {
2 @Select("SELECT * FROM user WHERE id = #{id}")
3 User getUserById(Integer id);
4}

当使用注解时,无需XML映射文件,但复杂SQL推荐仍使用XML配置。

6. MyBatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

动态SQL的作用

  • 根据不同条件生成不同的SQL语句
  • 避免拼接SQL字符串,提高代码可维护性
  • 减少重复SQL编写,提高开发效率
  • 实现更灵活的数据库查询

执行原理

  1. MyBatis在解析XML时,将动态SQL节点解析为DynamicSqlSource对象
  2. DynamicSqlSource包含一系列SqlNode对象(如IfSqlNode、ForEachSqlNode等)
  3. 执行时,将参数对象传入DynamicSqlSource
  4. 各SqlNode根据参数值判断是否应该被包含在最终SQL中
  5. 最终生成完整的SQL语句并执行

主要的动态SQL标签

1. if: 条件判断,满足条件时才包含其内容

xml
1<select id="findUsers" resultType="User">
2 SELECT * FROM users WHERE 1=1
3 <if test="name != null">
4 AND name LIKE #{name}
5 </if>
6 <if test="age != null">
7 AND age = #{age}
8 </if>
9</select>

2. choose (when, otherwise): 类似Java中的switch语句,实现多条件分支

xml
1<select id="findUsers" resultType="User">
2 SELECT * FROM users
3 <where>
4 <choose>
5 <when test="name != null">
6 name LIKE #{name}
7 </when>
8 <when test="email != null">
9 email = #{email}
10 </when>
11 <otherwise>
12 status = 'ACTIVE'
13 </otherwise>
14 </choose>
15 </where>
16</select>

3. where: 智能处理WHERE子句,自动添加WHERE关键字,移除多余的AND/OR

xml
1<select id="findUsers" resultType="User">
2 SELECT * FROM users
3 <where>
4 <if test="name != null">
5 name LIKE #{name}
6 </if>
7 <if test="email != null">
8 AND email = #{email}
9 </if>
10 </where>
11</select>

4. set: 用于UPDATE语句,智能处理SET部分,自动添加SET关键字,处理多余的逗号

xml
1<update id="updateUser">
2 UPDATE users
3 <set>
4 <if test="name != null">name = #{name},</if>
5 <if test="email != null">email = #{email},</if>
6 <if test="age != null">age = #{age},</if>
7 </set>
8 WHERE id = #{id}
9</update>

5. foreach: 循环遍历集合,生成IN子句或批量插入语句

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

6. trim: 更灵活的前缀/后缀控制,可自定义添加和去除的内容

xml
1<select id="findUsers" resultType="User">
2 SELECT * FROM users
3 <trim prefix="WHERE" prefixOverrides="AND|OR">
4 <if test="name != null">
5 AND name LIKE #{name}
6 </if>
7 <if test="email != null">
8 OR email = #{email}
9 </if>
10 </trim>
11</select>

7. bind: 创建一个变量并绑定到上下文

xml
1<select id="findUsersByName" resultType="User">
2 <bind name="pattern" value="'%' + name + '%'" />
3 SELECT * FROM users
4 WHERE name LIKE #{pattern}
5</select>

动态SQL的注解形式: MyBatis也支持在Java注解中使用动态SQL脚本:

java
1@Select({"<script>",
2 "SELECT * FROM users",
3 "<where>",
4 " <if test='name != null'>name LIKE #{name}</if>",
5 " <if test='email != null'>AND email = #{email}</if>",
6 "</where>",
7 "</script>"})
8List<User> findUsers(@Param("name") String name, @Param("email") String email);

最佳实践

  1. 复杂SQL使用XML配置,简单SQL可以使用注解
  2. 动态SQL中注意参数名称一致性
  3. 使用where、set标签简化条件处理
  4. 注意动态SQL的可读性和维护性

7. Mybatis 如何实现一对一、一对多的关联查询?

MyBatis提供了强大的关联查询功能,可以通过不同的方式实现一对一和一对多的关联:

**一对一关联(association)**有三种主要实现方式:

1. 嵌套查询(分步查询): 使用单独的SQL查询关联对象,通过select属性指定

xml
1<!-- 主查询 -->
2<select id="getOrder" resultMap="orderResultMap">
3 SELECT * FROM orders WHERE id = #{id}
4</select>
5
6<!-- 结果映射 -->
7<resultMap id="orderResultMap" type="Order">
8 <id property="id" column="id" />
9 <result property="orderNumber" column="order_number" />
10 <!-- 一对一关联,分步查询 -->
11 <association property="customer" column="customer_id"
12 select="getCustomerById" />
13</resultMap>
14
15<!-- 关联查询 -->
16<select id="getCustomerById" resultType="Customer">
17 SELECT * FROM customers WHERE id = #{id}
18</select>

2. 嵌套结果(联表查询): 使用JOIN联表查询,通过resultMap进行映射

xml
1<!-- 联表查询 -->
2<select id="getOrder" resultMap="orderResultMap">
3 SELECT o.*, c.name as customer_name, c.email as customer_email
4 FROM orders o
5 LEFT JOIN customers c ON o.customer_id = c.id
6 WHERE o.id = #{id}
7</select>
8
9<!-- 结果映射 -->
10<resultMap id="orderResultMap" type="Order">
11 <id property="id" column="id" />
12 <result property="orderNumber" column="order_number" />
13 <!-- 一对一关联,联表映射 -->
14 <association property="customer" javaType="Customer">
15 <id property="id" column="customer_id" />
16 <result property="name" column="customer_name" />
17 <result property="email" column="customer_email" />
18 </association>
19</resultMap>

3. 自动映射: 当字段名符合驼峰命名规则时,可以使用自动映射(需配置mapUnderscoreToCamelCase=true)

xml
1<select id="getOrder" resultType="Order">
2 SELECT o.*,
3 c.id as "customer.id",
4 c.name as "customer.name",
5 c.email as "customer.email"
6 FROM orders o
7 LEFT JOIN customers c ON o.customer_id = c.id
8 WHERE o.id = #{id}
9</select>

**一对多关联(collection)**也有三种主要实现方式:

1. 嵌套查询(分步查询)

xml
1<!-- 主查询 -->
2<select id="getCustomer" resultMap="customerResultMap">
3 SELECT * FROM customers WHERE id = #{id}
4</select>
5
6<!-- 结果映射 -->
7<resultMap id="customerResultMap" type="Customer">
8 <id property="id" column="id" />
9 <result property="name" column="name" />
10 <!-- 一对多关联,分步查询 -->
11 <collection property="orders" ofType="Order"
12 column="id" select="getOrdersByCustomerId" />
13</resultMap>
14
15<!-- 关联查询 -->
16<select id="getOrdersByCustomerId" resultType="Order">
17 SELECT * FROM orders WHERE customer_id = #{id}
18</select>

2. 嵌套结果(联表查询)

xml
1<!-- 联表查询 -->
2<select id="getCustomer" resultMap="customerResultMap">
3 SELECT c.*, o.id as order_id, o.order_number, o.order_date
4 FROM customers c
5 LEFT JOIN orders o ON c.id = o.customer_id
6 WHERE c.id = #{id}
7</select>
8
9<!-- 结果映射 -->
10<resultMap id="customerResultMap" type="Customer">
11 <id property="id" column="id" />
12 <result property="name" column="name" />
13 <!-- 一对多关联,联表映射 -->
14 <collection property="orders" ofType="Order">
15 <id property="id" column="order_id" />
16 <result property="orderNumber" column="order_number" />
17 <result property="orderDate" column="order_date" />
18 </collection>
19</resultMap>

3. 延迟加载: 可以通过配置lazyLoadingEnabled来控制是否延迟加载关联对象

xml
1<!-- 全局配置 -->
2<settings>
3 <setting name="lazyLoadingEnabled" value="true" />
4 <setting name="aggressiveLazyLoading" value="false" />
5</settings>
6
7<!-- 结果映射 -->
8<resultMap id="customerResultMap" type="Customer">
9 <id property="id" column="id" />
10 <result property="name" column="name" />
11 <!-- fetchType可以覆盖全局延迟加载配置 -->
12 <collection property="orders" ofType="Order"
13 column="id" select="getOrdersByCustomerId"
14 fetchType="lazy" />
15</resultMap>

多对多关系可以通过两个一对多关系来实现:

xml
1<!-- 查询用户及其角色 -->
2<select id="getUserWithRoles" resultMap="userWithRolesMap">
3 SELECT u.*, r.id as role_id, r.role_name
4 FROM users u
5 LEFT JOIN user_roles ur ON u.id = ur.user_id
6 LEFT JOIN roles r ON ur.role_id = r.id
7 WHERE u.id = #{id}
8</select>
9
10<!-- 结果映射 -->
11<resultMap id="userWithRolesMap" type="User">
12 <id property="id" column="id" />
13 <result property="username" column="username" />
14 <collection property="roles" ofType="Role">
15 <id property="id" column="role_id" />
16 <result property="roleName" column="role_name" />
17 </collection>
18</resultMap>

关联查询最佳实践

  1. 小数据量时使用联表查询,大数据量时考虑分步查询
  2. 合理设置延迟加载策略,避免N+1问题
  3. 灵活使用缓存,提高查询性能
  4. 考虑使用ResultMap复用,减少重复配置
  5. 根据业务场景选择适合的映射方式

8. MyBatis 的插件运行原理是什么?如何编写一个插件?

插件运行原理

MyBatis允许在SQL执行的关键点进行拦截,通过插件(Plugin)机制实现功能扩展。

可拦截的四大对象

  1. Executor:执行器,拦截SQL执行
  2. StatementHandler:SQL语句处理器,拦截SQL预编译
  3. ParameterHandler:参数处理器,拦截参数设置
  4. ResultSetHandler:结果集处理器,拦截结果映射

拦截原理

  • MyBatis使用JDK动态代理为四大对象创建代理
  • 当调用这些对象的方法时,会先经过插件的拦截器
  • 插件可以在方法执行前后添加自定义逻辑

编写插件步骤

1. 实现Interceptor接口

java
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "update",
5 args = {MappedStatement.class, Object.class}
6 )
7})
8public class MyPlugin implements Interceptor {
9
10 @Override
11 public Object intercept(Invocation invocation) throws Throwable {
12 // 执行前逻辑
13 System.out.println("SQL执行前...");
14
15 // 执行目标方法
16 Object result = invocation.proceed();
17
18 // 执行后逻辑
19 System.out.println("SQL执行后...");
20
21 return result;
22 }
23
24 @Override
25 public Object plugin(Object target) {
26 // 使用Plugin.wrap包装目标对象
27 return Plugin.wrap(target, this);
28 }
29
30 @Override
31 public void setProperties(Properties properties) {
32 // 获取插件配置参数
33 String prop = properties.getProperty("someProperty");
34 }
35}

2. 注册插件

XML配置方式

xml
1<plugins>
2 <plugin interceptor="com.example.MyPlugin">
3 <property name="someProperty" value="someValue"/>
4 </plugin>
5</plugins>

Spring Boot配置方式

java
1@Configuration
2public class MyBatisConfig {
3 @Bean
4 public MyPlugin myPlugin() {
5 MyPlugin plugin = new MyPlugin();
6 Properties properties = new Properties();
7 properties.setProperty("someProperty", "someValue");
8 plugin.setProperties(properties);
9 return plugin;
10 }
11}

常见插件应用场景

1. 分页插件

java
1@Intercepts({
2 @Signature(type = Executor.class, method = "query",
3 args = {MappedStatement.class, Object.class,
4 RowBounds.class, ResultHandler.class})
5})
6public class PageInterceptor implements Interceptor {
7 @Override
8 public Object intercept(Invocation invocation) throws Throwable {
9 Object[] args = invocation.getArgs();
10 MappedStatement ms = (MappedStatement) args[0];
11 Object parameter = args[1];
12 RowBounds rowBounds = (RowBounds) args[2];
13
14 // 获取原始SQL
15 BoundSql boundSql = ms.getBoundSql(parameter);
16 String sql = boundSql.getSql();
17
18 // 添加分页
19 String pageSql = sql + " LIMIT " + rowBounds.getOffset()
20 + "," + rowBounds.getLimit();
21
22 // 执行分页SQL
23 // ...
24 return invocation.proceed();
25 }
26}

2. SQL性能监控插件

java
1@Intercepts({
2 @Signature(type = StatementHandler.class, method = "query",
3 args = {Statement.class, ResultHandler.class})
4})
5public class PerformanceInterceptor implements Interceptor {
6 @Override
7 public Object intercept(Invocation invocation) throws Throwable {
8 long start = System.currentTimeMillis();
9
10 Object result = invocation.proceed();
11
12 long end = System.currentTimeMillis();
13 long time = end - start;
14
15 if (time > 1000) {
16 System.out.println("慢SQL,执行时间:" + time + "ms");
17 }
18
19 return result;
20 }
21}

3. SQL日志打印插件

java
1@Intercepts({
2 @Signature(type = StatementHandler.class, method = "prepare",
3 args = {Connection.class, Integer.class})
4})
5public class SqlLogInterceptor implements Interceptor {
6 @Override
7 public Object intercept(Invocation invocation) throws Throwable {
8 StatementHandler handler = (StatementHandler) invocation.getTarget();
9 BoundSql boundSql = handler.getBoundSql();
10 String sql = boundSql.getSql();
11
12 System.out.println("执行SQL: " + sql);
13
14 return invocation.proceed();
15 }
16}

9. MyBatis 如何实现批量操作?

MyBatis提供了多种批量操作的方式:

1. foreach标签批量插入

xml
1<insert id="batchInsert">
2 INSERT INTO users (name, email, age) VALUES
3 <foreach collection="list" item="user" separator=",">
4 (#{user.name}, #{user.email}, #{user.age})
5 </foreach>
6</insert>

2. foreach标签批量更新

xml
1<update id="batchUpdate">
2 <foreach collection="list" item="user" separator=";">
3 UPDATE users
4 SET name = #{user.name}, email = #{user.email}
5 WHERE id = #{user.id}
6 </foreach>
7</update>

3. 使用BATCH执行器

java
1// 获取批量执行的SqlSession
2SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
3try {
4 UserMapper mapper = session.getMapper(UserMapper.class);
5
6 for (User user : userList) {
7 mapper.insert(user);
8 }
9
10 // 提交批量操作
11 session.commit();
12} finally {
13 session.close();
14}

4. Spring集成批量操作

java
1@Autowired
2private SqlSessionTemplate sqlSessionTemplate;
3
4public void batchInsert(List<User> users) {
5 SqlSession session = sqlSessionTemplate.getSqlSessionFactory()
6 .openSession(ExecutorType.BATCH, false);
7 try {
8 UserMapper mapper = session.getMapper(UserMapper.class);
9 for (User user : users) {
10 mapper.insert(user);
11 }
12 session.commit();
13 } finally {
14 session.close();
15 }
16}

性能对比

  • 普通插入:每条SQL单独执行,1000条数据约需10秒
  • foreach批量插入:一条SQL插入所有数据,1000条数据约需0.5秒
  • BATCH执行器:预编译复用,1000条数据约需1秒

最佳实践

  1. 大批量数据(>1000条)建议分批处理,每批500-1000条
  2. 插入操作优先使用foreach方式
  3. 更新操作使用BATCH执行器
  4. 注意事务大小,避免长事务

10. MyBatis 如何处理枚举类型?

MyBatis提供了两种内置的枚举类型处理器:

1. EnumTypeHandler(默认): 将枚举转换为枚举名称字符串存储

java
1public enum UserStatus {
2 ACTIVE, INACTIVE, DELETED
3}
4
5// 数据库存储: "ACTIVE", "INACTIVE", "DELETED"

2. EnumOrdinalTypeHandler: 将枚举转换为枚举序号(ordinal)存储

java
1// 数据库存储: 0, 1, 2

配置枚举处理器

全局配置

xml
1<typeHandlers>
2 <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
3 javaType="com.example.UserStatus"/>
4</typeHandlers>

字段级配置

xml
1<result property="status" column="status"
2 typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

自定义枚举处理器

java
1public enum UserStatus {
2 ACTIVE(1, "激活"),
3 INACTIVE(0, "未激活"),
4 DELETED(-1, "已删除");
5
6 private final int code;
7 private final String desc;
8
9 UserStatus(int code, String desc) {
10 this.code = code;
11 this.desc = desc;
12 }
13
14 public int getCode() { return code; }
15 public String getDesc() { return desc; }
16}
17
18// 自定义处理器
19public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
20
21 @Override
22 public void setNonNullParameter(PreparedStatement ps, int i,
23 UserStatus parameter, JdbcType jdbcType)
24 throws SQLException {
25 ps.setInt(i, parameter.getCode());
26 }
27
28 @Override
29 public UserStatus getNullableResult(ResultSet rs, String columnName)
30 throws SQLException {
31 int code = rs.getInt(columnName);
32 return getStatusByCode(code);
33 }
34
35 @Override
36 public UserStatus getNullableResult(ResultSet rs, int columnIndex)
37 throws SQLException {
38 int code = rs.getInt(columnIndex);
39 return getStatusByCode(code);
40 }
41
42 @Override
43 public UserStatus getNullableResult(CallableStatement cs, int columnIndex)
44 throws SQLException {
45 int code = cs.getInt(columnIndex);
46 return getStatusByCode(code);
47 }
48
49 private UserStatus getStatusByCode(int code) {
50 for (UserStatus status : UserStatus.values()) {
51 if (status.getCode() == code) {
52 return status;
53 }
54 }
55 return null;
56 }
57}

11. MyBatis 如何防止SQL注入?

1. 使用#而不是$

xml
1<!-- 安全:使用预编译 -->
2<select id="getUser" resultType="User">
3 SELECT * FROM users WHERE id = #{id}
4</select>
5
6<!-- 不安全:直接拼接 -->
7<select id="getUser" resultType="User">
8 SELECT * FROM users WHERE id = ${id}
9</select>

2. 参数校验

java
1public User getUser(String id) {
2 // 校验参数格式
3 if (!id.matches("\\d+")) {
4 throw new IllegalArgumentException("Invalid ID");
5 }
6 return userMapper.getUser(id);
7}

3. 使用白名单

java
1// 动态表名/列名时使用白名单
2private static final Set<String> ALLOWED_COLUMNS =
3 Set.of("id", "name", "email", "age");
4
5public List<User> getUsersSortedBy(String column) {
6 if (!ALLOWED_COLUMNS.contains(column)) {
7 throw new IllegalArgumentException("Invalid column");
8 }
9 return userMapper.getUsersSortedBy(column);
10}

4. 限制查询结果数量

xml
1<select id="searchUsers" resultType="User">
2 SELECT * FROM users
3 WHERE name LIKE #{name}
4 LIMIT 1000
5</select>

12. MyBatis 的延迟加载是什么?如何配置?

延迟加载(Lazy Loading): 关联对象在真正使用时才加载,而不是在查询主对象时立即加载。

配置延迟加载

全局配置

xml
1<settings>
2 <!-- 开启延迟加载 -->
3 <setting name="lazyLoadingEnabled" value="true"/>
4 <!-- 关闭积极加载 -->
5 <setting name="aggressiveLazyLoading" value="false"/>
6</settings>

局部配置

xml
1<resultMap id="userMap" type="User">
2 <id property="id" column="id"/>
3 <result property="name" column="name"/>
4 <!-- fetchType可覆盖全局配置 -->
5 <association property="department"
6 select="getDepartment"
7 column="dept_id"
8 fetchType="lazy"/>
9</resultMap>

延迟加载原理

  • MyBatis使用CGLIB或Javassist创建代理对象
  • 访问关联属性时触发代理方法
  • 代理方法执行关联查询并返回结果

注意事项

  1. 延迟加载需要SqlSession保持打开状态
  2. 序列化时会触发延迟加载
  3. 使用toString()等方法可能触发加载

13. MyBatis 的分页插件 PageHelper 原理是什么?

PageHelper工作原理

1. 拦截SQL执行: 通过MyBatis插件机制拦截Executor的query方法

2. 解析分页参数: 从ThreadLocal中获取分页参数(PageNum、PageSize)

3. 改写SQL

  • 执行COUNT查询获取总记录数
  • 在原SQL基础上添加LIMIT子句

4. 执行分页查询: 执行改写后的SQL,返回分页结果

使用示例

java
1// 1. 添加依赖
2<dependency>
3 <groupId>com.github.pagehelper</groupId>
4 <artifactId>pagehelper-spring-boot-starter</artifactId>
5 <version>1.4.6</version>
6</dependency>
7
8// 2. 使用分页
9PageHelper.startPage(1, 10);
10List<User> users = userMapper.selectAll();
11PageInfo<User> pageInfo = new PageInfo<>(users);
12
13// 3. 获取分页信息
14pageInfo.getTotal(); // 总记录数
15pageInfo.getPages(); // 总页数
16pageInfo.getPageNum(); // 当前页
17pageInfo.getPageSize(); // 每页大小
18pageInfo.getList(); // 当前页数据

14. MyBatis 如何实现乐观锁?

使用版本号实现乐观锁

1. 数据库表添加version字段

sql
1CREATE TABLE users (
2 id INT PRIMARY KEY,
3 name VARCHAR(50),
4 version INT DEFAULT 0
5);

2. 实体类添加version属性

java
1public class User {
2 private Integer id;
3 private String name;
4 private Integer version;
5}

3. 更新时检查版本号

xml
1<update id="updateUser">
2 UPDATE users
3 SET name = #{name},
4 version = version + 1
5 WHERE id = #{id} AND version = #{version}
6</update>

4. 业务代码处理

java
1public boolean updateUser(User user) {
2 int rows = userMapper.updateUser(user);
3 if (rows == 0) {
4 // 更新失败,版本号已变化
5 throw new OptimisticLockException("数据已被修改");
6 }
7 return true;
8}

使用MyBatis-Plus的乐观锁插件

java
1@Version
2private Integer version;
3
4// 自动处理版本号
5userMapper.updateById(user);

15. MyBatis 的 TypeHandler 是什么?如何自定义?

TypeHandler作用: 处理Java类型与JDBC类型之间的转换

自定义TypeHandler

java
1// 1. 实现TypeHandler接口
2public class JsonTypeHandler extends BaseTypeHandler<Object> {
3
4 @Override
5 public void setNonNullParameter(PreparedStatement ps, int i,
6 Object parameter, JdbcType jdbcType)
7 throws SQLException {
8 ps.setString(i, JSON.toJSONString(parameter));
9 }
10
11 @Override
12 public Object getNullableResult(ResultSet rs, String columnName)
13 throws SQLException {
14 String json = rs.getString(columnName);
15 return JSON.parseObject(json, Object.class);
16 }
17
18 @Override
19 public Object getNullableResult(ResultSet rs, int columnIndex)
20 throws SQLException {
21 String json = rs.getString(columnIndex);
22 return JSON.parseObject(json, Object.class);
23 }
24
25 @Override
26 public Object getNullableResult(CallableStatement cs, int columnIndex)
27 throws SQLException {
28 String json = cs.getString(columnIndex);
29 return JSON.parseObject(json, Object.class);
30 }
31}
32
33// 2. 注册TypeHandler
34<typeHandlers>
35 <typeHandler handler="com.example.JsonTypeHandler"/>
36</typeHandlers>
37
38// 3. 使用TypeHandler
39<result property="extra" column="extra"
40 typeHandler="com.example.JsonTypeHandler"/>

16. MyBatis 如何处理大数据量查询?

1. 流式查询

java
1@Options(resultSetType = ResultSetType.FORWARD_ONLY,
2 fetchSize = 1000)
3@Select("SELECT * FROM users")
4void streamQuery(ResultHandler<User> handler);
5
6// 使用
7userMapper.streamQuery(context -> {
8 User user = (User) context.getResultObject();
9 // 处理每条记录
10});

2. 游标查询

java
1@Select("SELECT * FROM users")
2Cursor<User> selectByCursor();
3
4// 使用
5try (Cursor<User> cursor = userMapper.selectByCursor()) {
6 for (User user : cursor) {
7 // 处理每条记录
8 }
9}

3. 分页查询

java
1// 分批处理
2int pageSize = 1000;
3int pageNum = 1;
4while (true) {
5 PageHelper.startPage(pageNum, pageSize);
6 List<User> users = userMapper.selectAll();
7 if (users.isEmpty()) break;
8
9 // 处理当前批次
10 processBatch(users);
11 pageNum++;
12}

17. MyBatis 与 JPA 的区别是什么?

对比总结

特性MyBatisJPA/Hibernate
ORM方式半自动,需手写SQL全自动,自动生成SQL
SQL控制完全控制有限控制
学习曲线平缓陡峭
开发效率中等
性能优化容易困难
数据库移植
复杂查询灵活受限
适用场景SQL优化要求高领域模型复杂

选择建议

  • MyBatis:适合SQL优化要求高、复杂查询多的项目
  • JPA:适合领域模型复杂、需要快速开发的项目
  • 可以在同一项目中混合使用

学习指南

核心要点

  • MyBatis缓存机制和执行流程
  • 动态SQL的使用和原理
  • 关联查询的实现方式
  • 插件机制和自定义扩展
  • 性能优化和最佳实践

学习路径建议

  1. 掌握MyBatis基本配置和使用
  2. 理解缓存机制和执行流程
  3. 熟练使用动态SQL
  4. 学习插件开发和扩展
  5. 掌握性能优化技巧

实战建议

  • 合理使用缓存提升性能
  • 优先使用#防止SQL注入
  • 复杂SQL使用XML配置
  • 大数据量使用流式查询或分页
  • 根据场景选择合适的ORM框架
forum

评论区 / Comments