MyBatis常见面试题
MyBatis常见面试题
一、基础概念类
1. 什么是 MyBatis?它和 JDBC 有什么区别?
什么是 MyBatis?
MyBatis 是一款优秀的半自动化的持久层框架,它封装了几乎所有的 JDBC 底层操作,简化了开发。它的核心思想是将程序中的 SQL 语句剥离出来,配置在 XML 文件或注解中,从而实现 SQL 与 Java 代码的解耦。它通过一套简单的配置,将 Java 对象(POJO)与数据库中的记录进行映射(ORM)。
它和 JDBC 有什么区别?
特性 | JDBC (Java Database Connectivity) | MyBatis |
---|---|---|
本质 | Java 访问数据库的标准 API,是基础。 | 基于 JDBC 的封装和扩展,是一个框架。 |
SQL 处理 | 需要程序员手动编写、拼接复杂的 SQL 字符串在 Java 代码中,容易出错且难以维护。 | SQL 与 Java 代码分离,写在 XML 或注解中,清晰易管理。支持动态 SQL,灵活应对复杂查询。 |
结果集处理 | 需要手动编写代码从 ResultSet 中遍历并一个个字段地取出数据,再塞到 Java 对象中,非常繁琐。 | 自动进行 ORM 映射,通过 <resultMap> 或约定,直接将查询结果映射成 Java 对象,无需手动处理。 |
性能 | 原生性能最高,但需要开发者自己优化(如连接池、预编译等)。 | 性能接近 JDBC,因为它只是封装了 JDBC。它内置了连接池、预编译语句等优化,开箱即用。 |
开发效率 | 低。需要大量样板代码(注册驱动、获取连接、创建语句、处理异常、关闭资源等)。 | 高。大大减少了冗余代码,开发者只需关注 SQL 和业务逻辑。 |
可移植性 | 差,SQL 硬编码在 Java 文件中,更换数据库可能需要修改代码。 | 较好,SQL 在配置文件中,更换数据库时主要修改数据库连接配置和少量特定 SQL 语法。 |
总结:MyBatis 是 JDBC 的“增强版”,它解决了 JDBC 的硬编码、高冗余和繁琐结果集映射等问题,极大地提高了开发效率和代码可维护性。
2. MyBatis 的核心组件有哪些?
SqlSessionFactoryBuilder
(构造器):- 作用: 用于构建
SqlSessionFactory
对象。 - 生命周期: 最佳作用域是方法作用域。一旦创建了
SqlSessionFactory
,它就可以被丢弃。
- 作用: 用于构建
SqlSessionFactory
(会话工厂):- 作用: 用于创建
SqlSession
实例。它是线程安全的,一旦被创建,在整个应用运行期间都应该存在。 - 生命周期: 应用作用域(Application Scope)。通常通过单例模式来管理,一个数据库对应一个
SqlSessionFactory
。
- 作用: 用于创建
SqlSession
(会话):- 作用: 是 MyBatis 的核心接口,包含了执行 SQL、管理事务、获取 Mapper 等方法。它代表了一次与数据库的会话。
- 生命周期: 请求或方法作用域。它不是线程安全的!每次收到一个请求,就打开一个
SqlSession
,请求处理完毕,必须及时关闭(通常放在finally
块或使用 try-with-resources 语法)。
Executor
(执行器):- 作用: MyBatis 的底层核心,真正负责执行 SQL 语句。
SqlSession
只是门面,所有数据库操作最终都是通过Executor
完成的。它负责缓存、动态SQL参数处理等。 - 类型:
SIMPLE
(默认),REUSE
,BATCH
。
- 作用: MyBatis 的底层核心,真正负责执行 SQL 语句。
MappedStatement
(映射语句):- 作用: 它封装了 MyBatis 执行语句的所有信息,例如 SQL 语句、输入参数映射、输出结果映射、缓存配置等。每一个
<select|insert|update|delete>
标签都会被解析成一个MappedStatement
对象。
- 作用: 它封装了 MyBatis 执行语句的所有信息,例如 SQL 语句、输入参数映射、输出结果映射、缓存配置等。每一个
MapperProxy
(Mapper 代理):- 作用: Mapper 接口并没有实现类,MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象。当我们调用
UserMapper.selectById()
方法时,实际上调用的是MapperProxy
的invoke
方法,它会找到对应的MappedStatement
并执行。
- 作用: Mapper 接口并没有实现类,MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象。当我们调用
3. MyBatis 的优缺点?适合什么场景?
优点:
- SQL 灵活可控: SQL 由开发者编写,可以针对性地进行优化,满足高性能、复杂查询的需求。
- 学习成本低: 相比于 Hibernate,MyBatis 更容易上手,只要会写 SQL 就能快速使用。
- 与 JDBC 平滑过渡: 性能接近原生 JDBC,且能直接使用数据库的特有功能。
- 良好的解耦: SQL 与 Java 代码分离,代码更清晰,易于维护。
缺点:
- SQL 工作量大: 需要手动编写所有 SQL 语句和结果映射,当表字段多时,比较繁琐。
- 数据库移植性差: SQL 是依赖特定数据库的,如果更换数据库,可能需要重写或调整大量 SQL。
- 半自动化: 不像全自动 ORM 框架那样完全屏蔽数据库,开发者仍需对数据库有一定的了解。
适合场景:
- 需求多变、性能要求高的项目: 如互联网项目,业务逻辑复杂,经常需要优化 SQL。
- 遗留系统或对 SQL 有特殊要求的项目: 需要充分利用数据库特性或处理复杂存储过程。
- 团队 SQL 能力较强: 团队更倾向于直接控制和优化 SQL。
4. MyBatis 与 Hibernate 的主要区别?
维度 | MyBatis | Hibernate |
---|---|---|
ORM 程度 | 半自动化,需要开发者编写 SQL。 | 全自动化,完全封装 JDBC,开发者无需关心 SQL。 |
SQL 控制 | 灵活可控,SQL 优化方便。 | 失去控制权,由框架生成 SQL,复杂查询优化困难。 |
性能 | 在复杂、大数据量查询中,由于 SQL 可优化,性能通常更好。 | 在小批量、简单 CRUD 场景下效率高。复杂查询可能产生 N+1 问题,性能较差。 |
学习成本 | 低,易于上手。 | 高,需要掌握 HQL、Criteria API、缓存机制等。 |
数据库移植性 | 差,SQL 与数据库绑定。 | 好,使用 HQL 和方言,更换数据库成本低。 |
开发效率 | 在简单 CRUD 上不如 Hibernate,在复杂查询上效率高。 | 在标准 CRUD 场景下开发效率极高。 |
核心思想 | SQL 映射,围绕 SQL 开展工作。 | 对象关系映射,围绕对象开展工作。 |
形象比喻:MyBatis 像手动挡汽车,驾驶者可以精准控制速度和性能;Hibernate 像自动挡汽车,驾驶简单,但极限操控不如手动挡。
5. MyBatis 是如何实现 SQL 映射的?
MyBatis 的 SQL 映射是其最核心的功能,主要通过以下几步实现:
- 解析阶段(启动时):
- MyBatis 在应用启动时,会解析全局配置文件(
mybatis-config.xml
)和所有的 Mapper XML 文件(或注解)。 - 它会将每一个 SQL 标签(如
<select id="selectUser">
)解析成一个MappedStatement
对象,并将其存储在一个巨大的Configuration
对象中。MappedStatement
的 Key 通常是namespace.id
(例如com.example.mapper.UserMapper.selectUser
)。
- MyBatis 在应用启动时,会解析全局配置文件(
- 代理阶段(获取 Mapper 时):
- 当我们通过
sqlSession.getMapper(UserMapper.class)
获取 Mapper 接口的实例时,MyBatis 会使用 JDK 动态代理为该接口生成一个代理对象(MapperProxy
)。
- 当我们通过
- 映射与执行阶段(调用方法时):
- 当我们调用代理对象的方法时(如
userMapper.selectUser(1)
),代理对象MapperProxy
的invoke
方法会被触发。 - 关键步骤:
a. 定位 MappedStatement: 根据接口的全限定名 + 方法名 组合成 Key,从Configuration
中找到对应的MappedStatement
。
b. 参数转换: 将传入的 Java 参数转换为 SQL 执行时所需的参数。
c. SQL 执行: 将MappedStatement
、参数等信息传递给底层的Executor
执行器。
d. 结果映射:Executor
通过 JDBC 执行 SQL 后,获取ResultSet
。然后根据MappedStatement
中定义的结果映射规则(<resultMap>
或自动映射),将ResultSet
的一行行数据转换为 Java 对象。
- 当我们调用代理对象的方法时(如
总结:MyBatis 通过 “接口全限定名+方法名”作为ID,在启动时建立起一个 SQL 命令的仓库(Configuration
),运行时通过动态代理将接口方法调用映射到仓库中具体的 SQL 命令上,最后由执行器完成数据库操作和结果集到 Java 对象的转换。
二、SQL 映射与动态 SQL
6. #{} 和 ${} 的区别?哪个能防止 SQL 注入?
这是一个非常高频的面试题,考察点在于SQL注入安全和MyBatis的参数处理机制。
特性 | #{} (占位符) | ${} (拼接符) |
---|---|---|
处理方式 | 预编译(PreparedStatement) | 字符串直接替换(Statement) |
安全性 | 能有效防止SQL注入 | 不能防止SQL注入,存在安全风险 |
底层实现 | 会被解析成 ? ,然后使用 PreparedStatement.setXxx() 方法来安全地设置参数。 | 会将 ${} 内的内容原样拼接在SQL语句中,然后编译执行。 |
数据库优化 | 数据库可以对预编译的SQL进行缓存,有利于性能优化。 | 每次都是一个新的SQL语句,无法利用预编译缓存。 |
适用场景 | 传入参数的值(Where条件中的值,Insert的值等)。99%的场景都应使用#{} 。 | 动态传入表名、列名等非值参数。例如: ORDER BY ${columnName} 。 |
示例 | SELECT * FROM user WHERE name = #{name} 解析为:SELECT * FROM user WHERE name = ? ,参数 ‘Alice’ 会被安全地设置进去。 | SELECT * FROM ${tableName} WHERE name = ${name} 如果 tableName 为 user ,name 为 ‘Alice’ ,则解析为: SELECT * FROM user WHERE name = Alice (语法错误) 如果 name 被恶意传入 ‘Alice’ OR ‘1’=‘1’ ,则解析为: SELECT * FROM user WHERE name = Alice OR ‘1’=‘1’ (查询出所有数据,SQL注入成功)。 |
结论:
#{}
能防止SQL注入,应优先使用。${}
有SQL注入风险,除非是动态表名/列名等必须使用字符串替换的场景,否则严禁使用。
7. MyBatis 中如何处理字段名与属性名不一致?
当数据库字段名(如 user_name
)和 Java 实体类属性名(如 userName
)不一致时,查询结果无法自动映射。有三种解决方案:
1.起别名(最原始)
在 SQL 语句中为字段起一个与属性名相同的别名。
SELECT user_id as id, user_name as userName FROM user;
2.使用 resultMap
(最强大、最常用)
定义一个 <resultMap>
来精确地指定数据库字段和Java属性的映射关系。这是最推荐的方式。
<resultMap id="UserResultMap" type="com.example.User"><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- 其他字段映射 -->
</resultMap><select id="selectUser" resultMap="UserResultMap">SELECT * FROM user
</select>
3.开启驼峰命名自动映射(最方便)
在 MyBatis 的全局配置文件中(如 application.yml
或 mybatis-config.xml
)设置 mapUnderscoreToCamelCase
为 true
。MyBatis 会自动将下划线风格的字段名(user_name
)转换为驼峰风格的属性名(userName
)。
# application.yml
mybatis:configuration:map-underscore-to-camel-case: true
8. resultType 和 resultMap 的区别?
特性 | resultType | resultMap |
---|---|---|
映射方式 | 自动映射 | 手动映射 |
灵活性 | 低。要求查询返回的列名(或别名)必须与Java对象的属性名严格一致(或开启驼峰映射)。 | 高。可以自定义复杂的映射关系,解决任何字段名和属性名不一致的问题。 |
功能 | 只能简单映射。 | 功能强大,可以处理: 1. 字段名/属性名不一致。 2. 复杂类型关联(一对一、一对多)。 3. 继承关系映射。 |
使用场景 | 查询结果非常简单,且字段名与属性名完全对应。 | 所有需要自定义映射规则的场景,尤其是有关联查询时必须使用。 |
总结:resultType
是自动挡,简单省事但受限;resultMap
是手动挡,操控精准功能强。
9. MyBatis 动态 SQL 有哪些标签?执行原理是?
动态SQL标签:
<if>
: 条件判断。<choose>、<when>、<otherwise>
: 类似 Java 中的switch-case-default
,实现多选一。<trim>、<where>、<set>
: 智能地处理SQL语句的前缀、后缀和多余的连接词(如AND, OR, 逗号)。<where>
会智能地去掉开头多余的AND
/OR
。<set>
会智能地去掉结尾多余的逗号。
<foreach>
: 遍历集合,常用于IN
条件或批量操作。
<!-- 示例:批量查询 -->
<select id="selectUsersByIds" resultType="User">SELECT * FROM user WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
执行原理:
MyBatis 的动态 SQL 功能是基于 OGNL (Object-Graph Navigation Language) 表达式和内部强大的 SqlNode
、SqlSource
等组件实现的。
- 解析阶段: 在应用启动时,MyBatis 会解析 Mapper XML 文件。它会将动态 SQL 标签解析成一个个
SqlNode
对象,形成一个混合的静态/动态 SQL 树状结构。 - 执行阶段: 当执行 SQL 时,MyBatis 会传入参数对象。
- 应用OGNL: MyBatis 会使用 OGNL 表达式来评估动态标签中的判断条件(如
<if test="name != null">
)。 - 拼接SQL: 根据评估结果(true/false),
SqlNode
会动态地拼接出最终的、完整的 SQL 字符串。<where>
,<set>
等标签会在此过程中智能地处理语法问题。 - 参数设置: 最终,这个拼接好的 SQL 会被交给
PreparedStatement
,并使用#{}
占位符的方式安全地设置参数。
10. 如何实现一对多、多对一、多对多关联查询?
主要通过 <association>
和 <collection>
标签在 <resultMap>
中实现。
- 多对一 / 一对一 (
<association>
):
例如:多个订单属于一个用户(多对一)。
<resultMap id="OrderWithUserResultMap" type="Order"><!-- 映射Order本身的基本字段 --><id property="id" column="order_id"/><result property="orderNumber" column="order_number"/><!-- association 映射关联的单个User对象 --><association property="user" javaType="User"><id property="id" column="user_id"/><result property="userName" column="user_name"/></association>
</resultMap><select id="selectOrderWithUser" resultMap="OrderWithUserResultMap">SELECT o.order_id, o.order_number, u.user_id, u.user_nameFROM orders o LEFT JOIN user u ON o.user_id = u.user_idWHERE o.order_id = #{id}
</select>
-
一对多 (
<collection>
):例如:一个用户有多个订单(一对多)。
<resultMap id="UserWithOrdersResultMap" type="User"><!-- 映射User本身的基本字段 --><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- collection 映射关联的Order集合 --><collection property="orders" ofType="Order"><id property="id" column="order_id"/><result property="orderNumber" column="order_number"/></collection>
</resultMap><select id="selectUserWithOrders" resultMap="UserWithOrdersResultMap">SELECT u.user_id, u.user_name, o.order_id, o.order_numberFROM user u LEFT JOIN orders o ON u.user_id = o.user_idWHERE u.user_id = #{id}
</select>
-
多对多:
多对多关系需要借助中间表。例如:用户和角色。一个用户有多个角色,一个角色属于多个用户。查询用户及其角色列表,本质上仍然是一对多查询,只是关联关系通过中间表实现。
<resultMap id="UserWithRolesResultMap" type="User"><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- 角色集合 --><collection property="roles" ofType="Role"><id property="roleId" column="role_id"/><result property="roleName" column="role_name"/></collection>
</resultMap><select id="selectUserWithRoles" resultMap="UserWithRolesResultMap">SELECT u.user_id, u.user_name, r.role_id, r.role_nameFROM user uLEFT JOIN user_role ur ON u.user_id = ur.user_idLEFT JOIN role r ON ur.role_id = r.role_idWHERE u.user_id = #{id}
</select>
11. MyBatis 支持延迟加载吗?原理是?
支持。延迟加载(懒加载)是 MyBatis 提供的一种优化手段。
- 什么是延迟加载?
在关联查询(如查询用户及其订单)时,默认情况下(急加载),MyBatis 会通过一条复杂的联表SQL将主对象(用户)和关联对象(订单)的数据一次性全部查询出来。
而延迟加载的意思是:先查询主对象(用户)。只有当程序真正去访问主对象中的关联对象(如调用user.getOrders()
方法)时,MyBatis 才会发起第二条SQL去查询关联的订单数据。 - 如何开启?
在全局配置中开启:
mybatis:configuration:lazy-loading-enabled: true # 开启延迟加载aggressive-lazy-loading: false # 禁用“侵略性”延迟加载(重要)
-
原理是什么?
MyBatis 的实现非常巧妙,它利用了 动态代理。- 当你执行查询主对象的SQL时(如
selectUserById
),MyBatis 返回的User
对象中,orders
属性并不是真正的List<Order>
,而是一个由 MyBatis 生成的、实现了List<Order>
接口的代理对象。 - 这个代理对象内部保存了能够单独查询订单数据的SQL语句和参数。
- 当你第一次调用
user.getOrders()
方法时,这个代理对象会被触发,它去执行之前保存的SQL,查询出真正的订单数据,然后返回给你。
- 当你执行查询主对象的SQL时(如
-
优点: 提高了性能,特别是在不需要使用关联数据的情况下,避免了执行复杂SQL和传输多余数据。
-
缺点: 可能会遇到著名的 “N+1 查询问题”。如果先查询一个用户列表(1条SQL),然后遍历每个用户并访问其订单(N条SQL),就会产生大量数据库查询,反而降低性能。因此,延迟加载需要根据业务场景谨慎使用。
三、缓存机制
12. MyBatis 的一级缓存和二级缓存区别?
这道题要求对MyBatis的两级缓存有全局的理解。以下是核心区别的对比:
特性 | 一级缓存 (Local Cache) | 二级缓存 (Second Level Cache) |
---|---|---|
作用范围 | SqlSession 级别 | Mapper (Namespace) 级别 |
生命周期 | 与 SqlSession 相同。SqlSession 关闭或提交,缓存就清空。 | 与整个应用(SqlSessionFactory )相同。多个 SqlSession 可共享。 |
是否默认开启 | 是,且无法关闭。 | 否,需要手动配置开启。 |
存储位置 | 内存(SqlSession 对象内部)。 | 内存(默认,可配置到Redis等外部存储)。 |
如何开启 | 自动开启。 | 在Mapper.xml中添加 <cache/> 标签或在接口上加 @CacheNamespace 。 |
共享性 | 不能共享。每个 SqlSession 有自己独立的一级缓存。 | 可以共享。同一个 Mapper 下的所有 SqlSession 操作都会影响同一个二级缓存。 |
执行顺序 | 二级缓存 -> 一级缓存 -> 数据库。 | 查询时先看二级缓存,再看一级缓存。 |
一个形象的比喻:
- 一级缓存 像是你的个人工作台。你刚查过的资料放在手边,自己随时可以快速拿到,但别人看不到,你下班了(关闭Session)资料就被清走了。
- 二级缓存 像是团队的共享资料库。任何人(任何SqlSession)查过的资料都会放一份进去,大家都可以来取,资料会长期保存(直到根据策略被清理)。
13. 一级缓存什么时候会失效?
一级缓存失效(清空)的时机非常重要,是保证数据一致性的关键。以下操作会导致一级缓存失效:
1.执行 commit()
操作:
提交事务时,MyBatis会清空一级缓存,以避免后续查询读到脏数据。这是最常见的原因。
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.selectById(1); // 查询数据库,放入缓存
user1.setName("New Name");
mapper.updateUser(user1); // 执行更新,但缓存还未清空
// sqlSession.commit(); // 一旦执行commit,一级缓存被清空
User user2 = mapper.selectById(1); // 如果未commit,从缓存取(脏读);如果commit了,重新查询数据库。
2.执行 rollback()
操作:
回滚事务时,同样会清空一级缓存。
3.执行任意 INSERT
、UPDATE
、DELETE
操作(即使不是修改的同一数据):
只要执行了增删改操作,无论是否提交,一级缓存都会立即被清空。这是一种保守但安全的策略。
4.手动清空缓存:
调用 SqlSession
的 clearCache()
方法会手动清空其一级缓存。
5.关闭 SqlSession
:
SqlSession.close()
会清空其一级缓存。
关键点:一级缓存的生命周期非常短,尤其是在有写操作的场景下,它很容易失效。因此,不要过分依赖一级缓存能带来巨大的性能提升。
14. 如何开启二级缓存?二级缓存的清理策略?
如何开启二级缓存?
1.全局配置(确保开启): 在 mybatis-config.xml
中,<settings>
标签下的 cacheEnabled
设置为 true
(默认就是 true
,通常无需修改)。
<settings><setting name="cacheEnabled" value="true"/>
</settings>
2.在具体的Mapper中声明:
- XML方式: 在对应的
Mapper.xml
文件中添加<cache/>
标签。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper"><!-- 开启本Mapper的二级缓存 --><cache/><!-- 其他SQL定义 -->
</mapper>
- 注解方式: 在Mapper接口上使用
@CacheNamespace
注解。
@CacheNamespace
public interface UserMapper {// ...
}
3.实体类序列化: 使用二级缓存的POJO类必须实现 Serializable
接口。
二级缓存的清理策略(可配置性)
<cache/>
标签可以配置多个属性来控制缓存的行为(清理策略是核心):
<cacheeviction="LRU"flushInterval="60000"size="512"readOnly="true"/>
eviction
(清除策略): 当缓存数量达到上限时,如何移除对象。LRU
(最近最少使用,默认):移除最长时间不被使用的对象。FIFO
(先进先出):按对象进入缓存的顺序来移除它们。SOFT
(软引用):基于垃圾收集器状态和软引用规则移除对象。WEAK
(弱引用):更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval
(刷新间隔): 缓存会自动刷新的时间间隔,单位毫秒。默认不清空,设置后到时间会清空缓存,避免数据长期不更新。size
(引用数目): 缓存最多可以存储的对象数量。readOnly
(只读): 如果为true
,缓存返回的是相同的对象实例,性能好但不安全。如果为false
,缓存会返回对象的拷贝(通过序列化),安全但性能稍差。
四、分页与性能优化
15. MyBatis 如何实现分页?(物理分页 vs 逻辑分页)
MyBatis 本身不提供内置的分页功能,但可以通过多种方式实现,主要分为逻辑分页和物理分页。
1. 逻辑分页 (Logical Pagination)
- 实现方式: 使用 MyBatis 内置的
RowBounds
对象。 - 原理: 在查询出所有数据之后,在内存中进行分页。
- 执行 SQL 时,依然是
SELECT * FROM table
,查询出所有符合条件的数据。 - MyBatis 的
ResultSetHandler
在处理结果集时,会根据RowBounds
指定的offset
(偏移量)和limit
(限制条数)在内存中手动“跳过”前面的记录,只返回指定范围的数据。
- 执行 SQL 时,依然是
- 代码示例:
int offset = 10; // 起始行,相当于 (pageNum - 1) * pageSize
int limit = 5; // 每页条数
RowBounds rowBounds = new RowBounds(offset, limit);List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAllUsers", null, rowBounds);
- 优点: 对于少量数据简单方便。
- 缺点: 性能极差! 如果数据量巨大(如100万条),会先将这100万条数据从数据库全部加载到应用服务器内存中,再进行分页,极易导致内存溢出(OOM)。严禁在生产环境中用于大数据量分页。
2. 物理分页 (Physical Pagination)
- 原理: 在数据库层面进行分页,SQL 语句中直接使用数据库特有的分页关键字(如 LIMIT, ROWNUM),只查询当前页需要的数据。
- 实现方式:
- 手动编写分页SQL(推荐): 最直接、性能最好的方式。
-- MySQL
SELECT * FROM user ORDER BY id LIMIT #{offset}, #{pageSize}-- Oracle(使用ROWNUM,较复杂)
SELECT * FROM (SELECT a.*, ROWNUM rn FROM (SELECT * FROM user ORDER BY id) a WHERE ROWNUM <= #{end}
) WHERE rn > #{start}
- 使用分页插件(最常用)**: 例如 PageHelper,它可以自动拦截SQL并为其加上分页语句,开发者无需关心不同数据库的语法差异。
- 优点: 性能高,只传输和处理一页的数据,减轻了数据库和网络的负担。
- 缺点: 需要编写复杂SQL或引入第三方插件。
结论:生产环境中必须使用物理分页。
16. 分页插件(如 PageHelper)的原理?
以最流行的 PageHelper
为例,其核心原理是 MyBatis 插件(拦截器)机制。
- 插件声明:
PageHelper
实现了 MyBatis 的Interceptor
接口,并声明它要拦截的是Executor
的查询方法。 - 拦截时机: 当你在调用DAO方法之前,使用
PageHelper.startPage(pageNum, pageSize)
方法后,它会将分页参数(页码、大小)存储在一个ThreadLocal
变量中。 - 执行拦截:
- 当MyBatis执行SQL时,
PageHelper
插件会拦截Executor
的query
方法。 - 插件从
ThreadLocal
中获取到分页参数。 - 插件利用数据库方言(
Dialect
)将原始SQL重写为带有分页功能的物理分页SQL。- 原始SQL:
SELECT * FROM user
- 重写后SQL(MySQL):
SELECT * FROM user LIMIT 0, 10
- 原始SQL:
- 当MyBatis执行SQL时,
- 查询总数(用于计算总页数):
- 插件还会自动执行一条
COUNT(*)
查询来获取数据总数,例如:SELECT COUNT(*) FROM user
。 - 它将总数和分页数据一起封装到一个
PageInfo
对象中返回给用户。
- 插件还会自动执行一条
- 清理 ThreadLocal: 在分页查询结束后,插件会自动清除
ThreadLocal
中的分页参数,防止影响后续的非分页查询。
总结:PageHelper 通过动态代理和SQL重写技术,将开发者的逻辑分页请求,透明地、自动化地转换成了高效的物理分页。
17. 如何优化 MyBatis 的批量插入?
直接使用 foreach
标签在循环中多次执行单条INSERT语句效率很低,因为每次都要与数据库建立一次网络连接和事务开销。优化方案如下:
1. 使用 foreach
标签拼接批量SQL(推荐用于数据量不大时)
将多条INSERT语句合并成一条,利用SQL的批量插入语法。
<insert id="batchInsert">INSERT INTO user (name, email) VALUES<foreach collection="list" item="user" separator=",">(#{user.name}, #{user.email})</foreach>
</insert>
- 生成SQL:
INSERT INTO user (name, email) VALUES (?, ?), (?, ?), (?, ?)
- 优点: 一次网络传输,效率高。
- 缺点:
- 当数据量非常大时(如数万条),拼接的SQL字符串会非常长,可能超出数据库允许的最大包限制。
- 对数据库的压力是瞬间的。
2. 使用 BatchExecutor(真正意义上的批量操作)
通过将 SqlSession
的执行器类型设置为 ExecutorType.BATCH
。
// 获取批量操作的SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {UserMapper mapper = sqlSession.getMapper(UserMapper.class);for (int i = 0; i < 10000; i++) {User user = new User(...);mapper.insertUser(user); // 这里只是将SQL预编译并加入批处理队列,并未执行// 每积累一定数量(如500条),手动刷入一次数据库,避免内存溢出if (i % 500 == 0) {sqlSession.flushStatements();}}// 最后提交事务,执行剩余的批处理操作sqlSession.commit();
}
- 原理:
BatchExecutor
会将多条INSERT语句进行预编译,然后通过addBatch()
方法将其加入批处理,最后通过executeBatch()
一次性发送到数据库执行。 - 优点: 性能极高,适合超大数据量插入。能有效利用JDBC的批处理能力,减少网络交互次数。
- 缺点: 代码稍复杂,需要手动管理
flush
和commit
。
结论: 中小批量数据用 foreach
简单高效;超大批量数据(数万以上)用 BatchExecutor
更稳定可靠。
18. 如何获取自动生成的主键值?(useGeneratedKeys)
当我们插入一条记录后,经常需要获取数据库自动生成的主键(如MySQL的AUTO_INCREMENT)。MyBatis提供了两种方式:
1. 使用 useGeneratedKeys
和 keyProperty
(推荐)
这是最简洁的方式。
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>
注解方式:
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("INSERT INTO user (name, email) VALUES (#{name}, #{email})")
int insertUser(User user);
- 工作原理: MyBatis 会使用 JDBC 的
Statement.getGeneratedKeys()
方法来获取数据库生成的主键。 - 效果: 插入成功后,生成的主键值会自动回填到传入的
User
对象的id
属性中。
User user = new User();
user.setName("Alice");
user.setEmail("alice@example.com");
mapper.insertUser(user); // 执行插入
System.out.println(user.getId()); // 此处id已被赋值,即为数据库生成的主键
2. 使用 <selectKey>
标签(适用于不支持自增主键的数据库,如Oracle)
<insert id="insertUser"><selectKey keyProperty="id" resultType="long" order="BEFORE">SELECT SEQ_USER.NEXTVAL FROM DUAL <!-- Oracle序列 --></selectKey>INSERT INTO user (id, name, email) VALUES (#{id}, #{name}, #{email})
</insert>
order="BEFORE"
表示在执行INSERT语句之前先执行<selectKey>
中的查询来获取主键。
19. MyBatis 是否支持预编译?如何开启?
是的,MyBatis 完全支持预编译,并且这是它的默认行为,也是其防止SQL注入的核心机制。
- 什么是预编译?
预编译是指数据库驱动程序先将SQL语句模板(带?
占位符)发送给数据库进行编译和优化。之后执行时,只需要将参数传递给这个已编译好的模板,而无需再次编译整个SQL语句。 - MyBatis 如何实现?
MyBatis 在处理#{}
占位符时,底层就是使用 JDBC 的PreparedStatement
对象来实现预编译的。- 你的Mapper中的SQL:
SELECT * FROM user WHERE id = #{id}
- MyBatis 交给JDBC的SQL:
SELECT * FROM user WHERE id = ?
- JDBC 会先预编译这个带
?
的SQL,然后调用pstmt.setInt(1, 5)
来安全地设置参数。
- 你的Mapper中的SQL:
- 如何“开启”?
你不需要做任何事来“开启”预编译,因为只要你使用#{}
,MyBatis 默认就使用PreparedStatement
。 所谓的“开启”实际上是确保你没有错误地使用${}
来拼接SQL,因为${}
会绕过预编译机制。 - 验证预编译:
你可以在日志中看到类似以下的输出,这证明SQL是预编译的:
==> Preparing: SELECT * FROM user WHERE id = ? (SQL已被预编译)
==> Parameters: 1(Integer) (参数被安全地设置进去)
总结:MyBatis通过 #{}
语法和底层的 PreparedStatement
自动实现了预编译,这是其安全性和性能的重要保障。开发者需要做的就是坚持使用 #{}
。