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可用,缓存范围更广
二级缓存启用步骤:
-
在MyBatis配置文件中开启二级缓存:
xml1<settings>2 <setting name="cacheEnabled" value="true"/>3</settings> -
在Mapper映射文件中配置缓存:
xml1<cache2 eviction="LRU"3 flushInterval="60000"4 size="1024"5 readOnly="true"/> -
实体类需要实现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,释放资源
核心组件:
- Configuration:保存MyBatis所有配置信息
- SqlSessionFactory:创建SqlSession的工厂
- SqlSession:数据库会话,提供数据库操作接口
- Executor:SQL执行器,负责SQL语句的执行
- StatementHandler:封装JDBC Statement操作
- ParameterHandler:处理SQL参数
- ResultSetHandler:处理结果集映射
- TypeHandler:处理Java类型与JDBC类型之间的映射
- MappedStatement:对应映射文件中的SQL节点
流程图:
1加载配置文件 → 解析配置文件 → 创建Configuration对象 → 创建SqlSessionFactory2 ↓3创建SqlSession → 获取Mapper代理对象 → 执行SQL4 ↓5Executor执行器 → StatementHandler处理语句 → ParameterHandler设置参数6 ↓7执行SQL → ResultSetHandler处理结果集 → 返回结果对象 → 关闭SqlSession执行过程详解:
- 客户端调用Mapper接口方法
- 根据方法的全限定名找到对应的MappedStatement
- 通过SqlSession将请求转发给Executor
- Executor根据具体的SQL类型,调用query、update等方法
- 创建JDBC Statement对象,设置参数
- 执行SQL语句,获取结果集
- 将结果集映射为Java对象
- 返回处理结果
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优化要求高的场景,如复杂报表查询、多表关联
对比表格:
| 特性 | MyBatis | Hibernate |
|---|---|---|
| 映射方式 | SQL映射 | 对象映射 |
| SQL控制 | 手动编写SQL | 自动生成SQL |
| 学习难度 | 低 | 高 |
| 开发效率 | 手写SQL,效率较低 | 不写SQL,效率高 |
| 灵活性 | 高,可优化SQL | 低,难以优化SQL |
| 数据库移植 | 较差,需改SQL | 较好,通过方言支持 |
| 缓存机制 | 简单 | 完善 |
| 适用场景 | 性能要求高、SQL复杂 | 领域模型复杂 |
选择建议:
- 如果项目需要精细控制SQL性能,选择MyBatis
- 如果项目重在对象模型和业务逻辑,选择Hibernate
- 在一些项目中,两者可以结合使用:简单操作用Hibernate,复杂查询用MyBatis
4. MyBatis 中 井花括号 和 美元花括号 的区别是什么?
MyBatis中的#{}和${}都是用于参数替换的符号,但它们有本质上的区别:
井花括号(预编译参数):
- 会被预编译为
?占位符,然后通过PreparedStatement设置参数值 - 可以防止SQL注入攻击
- 自动进行类型转换和值转义
- 对所有数据类型都有效
美元花括号(文本替换):
- 直接替换为参数值,不会被预编译
- 不能防止SQL注入攻击
- 不会自动进行类型转换和转义
- 主要用于动态表名、列名等不能使用预编译的场景
代码示例:
1<!-- 使用#{} -->2<select id="getUserById" resultType="User">3 SELECT * FROM user WHERE id = #{id}4</select>5<!-- 编译后的SQL: SELECT * FROM user WHERE id = ? -->67<!-- 使用${} -->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,无法使用预编译缓存
最佳实践:
- 默认情况下优先使用井花括号
- 只在必须的场景下使用美元花括号(如动态表名)
- 使用美元花括号时,必须严格控制参数来源,避免SQL注入
- 对于用户输入的数据,永远不要使用美元花括号直接拼接
5. MyBatis 写个 Xml 映射文件,再写个 DAO 接口就能执行,这个原理是什么?
MyBatis通过动态代理机制实现了从DAO接口到XML映射文件的关联执行。主要原理包括:
1. 接口与映射文件绑定:
- MyBatis通过namespace将Mapper接口与XML映射文件关联起来
- 接口的全限定名必须与XML的namespace值完全一致
- 接口中的方法名必须与XML中的SQL ID一致
- 接口方法的参数和返回类型必须与SQL语句匹配
2. 动态代理机制:
- MyBatis使用JDK动态代理为接口创建代理对象
- 当调用接口方法时,代理对象会拦截调用
- 根据接口的全限定名和方法名查找对应的SQL语句
- 执行SQL并返回结果
3. 核心代码流程:
1// 1. 创建SqlSessionFactory2SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);34// 2. 获取SqlSession5SqlSession session = factory.openSession();67// 3. 获取Mapper接口的代理实现8UserMapper mapper = session.getMapper(UserMapper.class);910// 4. 调用接口方法(实际由动态代理执行)11User user = mapper.getUserById(1);4. 代理创建过程:
- 在SqlSession.getMapper()中,调用Configuration.getMapper()
- Configuration委托MapperRegistry.getMapper()处理
- MapperRegistry通过MapperProxyFactory创建代理实例
- 最终使用JDK的Proxy.newProxyInstance()创建代理对象
- 代理对象使用MapperProxy作为InvocationHandler处理调用
5. 方法调用过程:
- 调用接口方法时,进入MapperProxy的invoke()方法
- 解析方法签名,找到对应的MapperMethod
- MapperMethod根据方法类型(CRUD)调用SqlSession的相应方法
- SqlSession通过Executor执行SQL并处理结果
- 将结果转换为方法的返回类型并返回
6. 配置示例:
1// 接口定义2public interface UserMapper {3 User getUserById(Integer id);4}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配置:
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编写,提高开发效率
- 实现更灵活的数据库查询
执行原理:
- MyBatis在解析XML时,将动态SQL节点解析为DynamicSqlSource对象
- DynamicSqlSource包含一系列SqlNode对象(如IfSqlNode、ForEachSqlNode等)
- 执行时,将参数对象传入DynamicSqlSource
- 各SqlNode根据参数值判断是否应该被包含在最终SQL中
- 最终生成完整的SQL语句并执行
主要的动态SQL标签:
1. if: 条件判断,满足条件时才包含其内容
1<select id="findUsers" resultType="User">2 SELECT * FROM users WHERE 1=13 <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语句,实现多条件分支
1<select id="findUsers" resultType="User">2 SELECT * FROM users3 <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
1<select id="findUsers" resultType="User">2 SELECT * FROM users3 <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关键字,处理多余的逗号
1<update id="updateUser">2 UPDATE users3 <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子句或批量插入语句
1<select id="findUsersByIds" resultType="User">2 SELECT * FROM users3 WHERE id IN4 <foreach collection="list" item="id" open="(" separator="," close=")">5 #{id}6 </foreach>7</select>6. trim: 更灵活的前缀/后缀控制,可自定义添加和去除的内容
1<select id="findUsers" resultType="User">2 SELECT * FROM users3 <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: 创建一个变量并绑定到上下文
1<select id="findUsersByName" resultType="User">2 <bind name="pattern" value="'%' + name + '%'" />3 SELECT * FROM users4 WHERE name LIKE #{pattern}5</select>动态SQL的注解形式: MyBatis也支持在Java注解中使用动态SQL脚本:
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);最佳实践:
- 复杂SQL使用XML配置,简单SQL可以使用注解
- 动态SQL中注意参数名称一致性
- 使用where、set标签简化条件处理
- 注意动态SQL的可读性和维护性
7. Mybatis 如何实现一对一、一对多的关联查询?
MyBatis提供了强大的关联查询功能,可以通过不同的方式实现一对一和一对多的关联:
**一对一关联(association)**有三种主要实现方式:
1. 嵌套查询(分步查询):
使用单独的SQL查询关联对象,通过select属性指定
1<!-- 主查询 -->2<select id="getOrder" resultMap="orderResultMap">3 SELECT * FROM orders WHERE id = #{id}4</select>56<!-- 结果映射 -->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>1415<!-- 关联查询 -->16<select id="getCustomerById" resultType="Customer">17 SELECT * FROM customers WHERE id = #{id}18</select>2. 嵌套结果(联表查询):
使用JOIN联表查询,通过resultMap进行映射
1<!-- 联表查询 -->2<select id="getOrder" resultMap="orderResultMap">3 SELECT o.*, c.name as customer_name, c.email as customer_email4 FROM orders o5 LEFT JOIN customers c ON o.customer_id = c.id6 WHERE o.id = #{id}7</select>89<!-- 结果映射 -->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)
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 o7 LEFT JOIN customers c ON o.customer_id = c.id8 WHERE o.id = #{id}9</select>**一对多关联(collection)**也有三种主要实现方式:
1. 嵌套查询(分步查询):
1<!-- 主查询 -->2<select id="getCustomer" resultMap="customerResultMap">3 SELECT * FROM customers WHERE id = #{id}4</select>56<!-- 结果映射 -->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>1415<!-- 关联查询 -->16<select id="getOrdersByCustomerId" resultType="Order">17 SELECT * FROM orders WHERE customer_id = #{id}18</select>2. 嵌套结果(联表查询):
1<!-- 联表查询 -->2<select id="getCustomer" resultMap="customerResultMap">3 SELECT c.*, o.id as order_id, o.order_number, o.order_date4 FROM customers c5 LEFT JOIN orders o ON c.id = o.customer_id6 WHERE c.id = #{id}7</select>89<!-- 结果映射 -->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来控制是否延迟加载关联对象
1<!-- 全局配置 -->2<settings>3 <setting name="lazyLoadingEnabled" value="true" />4 <setting name="aggressiveLazyLoading" value="false" />5</settings>67<!-- 结果映射 -->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>多对多关系可以通过两个一对多关系来实现:
1<!-- 查询用户及其角色 -->2<select id="getUserWithRoles" resultMap="userWithRolesMap">3 SELECT u.*, r.id as role_id, r.role_name4 FROM users u5 LEFT JOIN user_roles ur ON u.id = ur.user_id6 LEFT JOIN roles r ON ur.role_id = r.id7 WHERE u.id = #{id}8</select>910<!-- 结果映射 -->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>关联查询最佳实践:
- 小数据量时使用联表查询,大数据量时考虑分步查询
- 合理设置延迟加载策略,避免N+1问题
- 灵活使用缓存,提高查询性能
- 考虑使用ResultMap复用,减少重复配置
- 根据业务场景选择适合的映射方式
8. MyBatis 的插件运行原理是什么?如何编写一个插件?
插件运行原理:
MyBatis允许在SQL执行的关键点进行拦截,通过插件(Plugin)机制实现功能扩展。
可拦截的四大对象:
- Executor:执行器,拦截SQL执行
- StatementHandler:SQL语句处理器,拦截SQL预编译
- ParameterHandler:参数处理器,拦截参数设置
- ResultSetHandler:结果集处理器,拦截结果映射
拦截原理:
- MyBatis使用JDK动态代理为四大对象创建代理
- 当调用这些对象的方法时,会先经过插件的拦截器
- 插件可以在方法执行前后添加自定义逻辑
编写插件步骤:
1. 实现Interceptor接口:
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 @Override11 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 @Override25 public Object plugin(Object target) {26 // 使用Plugin.wrap包装目标对象27 return Plugin.wrap(target, this);28 }29 30 @Override31 public void setProperties(Properties properties) {32 // 获取插件配置参数33 String prop = properties.getProperty("someProperty");34 }35}2. 注册插件:
XML配置方式:
1<plugins>2 <plugin interceptor="com.example.MyPlugin">3 <property name="someProperty" value="someValue"/>4 </plugin>5</plugins>Spring Boot配置方式:
1@Configuration2public class MyBatisConfig {3 @Bean4 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. 分页插件:
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 @Override8 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 // 获取原始SQL15 BoundSql boundSql = ms.getBoundSql(parameter);16 String sql = boundSql.getSql();17 18 // 添加分页19 String pageSql = sql + " LIMIT " + rowBounds.getOffset() 20 + "," + rowBounds.getLimit();21 22 // 执行分页SQL23 // ...24 return invocation.proceed();25 }26}2. SQL性能监控插件:
1@Intercepts({2 @Signature(type = StatementHandler.class, method = "query",3 args = {Statement.class, ResultHandler.class})4})5public class PerformanceInterceptor implements Interceptor {6 @Override7 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日志打印插件:
1@Intercepts({2 @Signature(type = StatementHandler.class, method = "prepare",3 args = {Connection.class, Integer.class})4})5public class SqlLogInterceptor implements Interceptor {6 @Override7 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标签批量插入:
1<insert id="batchInsert">2 INSERT INTO users (name, email, age) VALUES3 <foreach collection="list" item="user" separator=",">4 (#{user.name}, #{user.email}, #{user.age})5 </foreach>6</insert>2. foreach标签批量更新:
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执行器:
1// 获取批量执行的SqlSession2SqlSession 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集成批量操作:
1@Autowired2private SqlSessionTemplate sqlSessionTemplate;34public 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秒
最佳实践:
- 大批量数据(>1000条)建议分批处理,每批500-1000条
- 插入操作优先使用foreach方式
- 更新操作使用BATCH执行器
- 注意事务大小,避免长事务
10. MyBatis 如何处理枚举类型?
MyBatis提供了两种内置的枚举类型处理器:
1. EnumTypeHandler(默认): 将枚举转换为枚举名称字符串存储
1public enum UserStatus {2 ACTIVE, INACTIVE, DELETED3}45// 数据库存储: "ACTIVE", "INACTIVE", "DELETED"2. EnumOrdinalTypeHandler: 将枚举转换为枚举序号(ordinal)存储
1// 数据库存储: 0, 1, 2配置枚举处理器:
全局配置:
1<typeHandlers>2 <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"3 javaType="com.example.UserStatus"/>4</typeHandlers>字段级配置:
1<result property="status" column="status" 2 typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>自定义枚举处理器:
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}1718// 自定义处理器19public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {20 21 @Override22 public void setNonNullParameter(PreparedStatement ps, int i, 23 UserStatus parameter, JdbcType jdbcType) 24 throws SQLException {25 ps.setInt(i, parameter.getCode());26 }27 28 @Override29 public UserStatus getNullableResult(ResultSet rs, String columnName) 30 throws SQLException {31 int code = rs.getInt(columnName);32 return getStatusByCode(code);33 }34 35 @Override36 public UserStatus getNullableResult(ResultSet rs, int columnIndex) 37 throws SQLException {38 int code = rs.getInt(columnIndex);39 return getStatusByCode(code);40 }41 42 @Override43 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. 使用#而不是$:
1<!-- 安全:使用预编译 -->2<select id="getUser" resultType="User">3 SELECT * FROM users WHERE id = #{id}4</select>56<!-- 不安全:直接拼接 -->7<select id="getUser" resultType="User">8 SELECT * FROM users WHERE id = ${id}9</select>2. 参数校验:
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. 使用白名单:
1// 动态表名/列名时使用白名单2private static final Set<String> ALLOWED_COLUMNS = 3 Set.of("id", "name", "email", "age");45public 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. 限制查询结果数量:
1<select id="searchUsers" resultType="User">2 SELECT * FROM users 3 WHERE name LIKE #{name}4 LIMIT 10005</select>12. MyBatis 的延迟加载是什么?如何配置?
延迟加载(Lazy Loading): 关联对象在真正使用时才加载,而不是在查询主对象时立即加载。
配置延迟加载:
全局配置:
1<settings>2 <!-- 开启延迟加载 -->3 <setting name="lazyLoadingEnabled" value="true"/>4 <!-- 关闭积极加载 -->5 <setting name="aggressiveLazyLoading" value="false"/>6</settings>局部配置:
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创建代理对象
- 访问关联属性时触发代理方法
- 代理方法执行关联查询并返回结果
注意事项:
- 延迟加载需要SqlSession保持打开状态
- 序列化时会触发延迟加载
- 使用toString()等方法可能触发加载
13. MyBatis 的分页插件 PageHelper 原理是什么?
PageHelper工作原理:
1. 拦截SQL执行: 通过MyBatis插件机制拦截Executor的query方法
2. 解析分页参数: 从ThreadLocal中获取分页参数(PageNum、PageSize)
3. 改写SQL:
- 执行COUNT查询获取总记录数
- 在原SQL基础上添加LIMIT子句
4. 执行分页查询: 执行改写后的SQL,返回分页结果
使用示例:
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>78// 2. 使用分页9PageHelper.startPage(1, 10);10List<User> users = userMapper.selectAll();11PageInfo<User> pageInfo = new PageInfo<>(users);1213// 3. 获取分页信息14pageInfo.getTotal(); // 总记录数15pageInfo.getPages(); // 总页数16pageInfo.getPageNum(); // 当前页17pageInfo.getPageSize(); // 每页大小18pageInfo.getList(); // 当前页数据14. MyBatis 如何实现乐观锁?
使用版本号实现乐观锁:
1. 数据库表添加version字段:
1CREATE TABLE users (2 id INT PRIMARY KEY,3 name VARCHAR(50),4 version INT DEFAULT 05);2. 实体类添加version属性:
1public class User {2 private Integer id;3 private String name;4 private Integer version;5}3. 更新时检查版本号:
1<update id="updateUser">2 UPDATE users 3 SET name = #{name}, 4 version = version + 15 WHERE id = #{id} AND version = #{version}6</update>4. 业务代码处理:
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的乐观锁插件:
1@Version2private Integer version;34// 自动处理版本号5userMapper.updateById(user);15. MyBatis 的 TypeHandler 是什么?如何自定义?
TypeHandler作用: 处理Java类型与JDBC类型之间的转换
自定义TypeHandler:
1// 1. 实现TypeHandler接口2public class JsonTypeHandler extends BaseTypeHandler<Object> {3 4 @Override5 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 @Override12 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 @Override19 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 @Override26 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}3233// 2. 注册TypeHandler34<typeHandlers>35 <typeHandler handler="com.example.JsonTypeHandler"/>36</typeHandlers>3738// 3. 使用TypeHandler39<result property="extra" column="extra" 40 typeHandler="com.example.JsonTypeHandler"/>16. MyBatis 如何处理大数据量查询?
1. 流式查询:
1@Options(resultSetType = ResultSetType.FORWARD_ONLY, 2 fetchSize = 1000)3@Select("SELECT * FROM users")4void streamQuery(ResultHandler<User> handler);56// 使用7userMapper.streamQuery(context -> {8 User user = (User) context.getResultObject();9 // 处理每条记录10});2. 游标查询:
1@Select("SELECT * FROM users")2Cursor<User> selectByCursor();34// 使用5try (Cursor<User> cursor = userMapper.selectByCursor()) {6 for (User user : cursor) {7 // 处理每条记录8 }9}3. 分页查询:
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 的区别是什么?
对比总结:
| 特性 | MyBatis | JPA/Hibernate |
|---|---|---|
| ORM方式 | 半自动,需手写SQL | 全自动,自动生成SQL |
| SQL控制 | 完全控制 | 有限控制 |
| 学习曲线 | 平缓 | 陡峭 |
| 开发效率 | 中等 | 高 |
| 性能优化 | 容易 | 困难 |
| 数据库移植 | 差 | 好 |
| 复杂查询 | 灵活 | 受限 |
| 适用场景 | SQL优化要求高 | 领域模型复杂 |
选择建议:
- MyBatis:适合SQL优化要求高、复杂查询多的项目
- JPA:适合领域模型复杂、需要快速开发的项目
- 可以在同一项目中混合使用
学习指南
核心要点:
- MyBatis缓存机制和执行流程
- 动态SQL的使用和原理
- 关联查询的实现方式
- 插件机制和自定义扩展
- 性能优化和最佳实践
学习路径建议:
- 掌握MyBatis基本配置和使用
- 理解缓存机制和执行流程
- 熟练使用动态SQL
- 学习插件开发和扩展
- 掌握性能优化技巧
实战建议:
- 合理使用缓存提升性能
- 优先使用#防止SQL注入
- 复杂SQL使用XML配置
- 大数据量使用流式查询或分页
- 根据场景选择合适的ORM框架
评论区 / Comments