MyBatis 常见问题
MyBatis 常见问题
penjcSqlSession
SqlSession
是 MyBatis 框架中的核心接口之一,它用于与数据库交互并执行 SQL 语句。MyBatis 是一个流行的持久层框架,它简化了 JDBC 操作,使得开发人员可以使用 SQL 语句直接操作数据库,而不需要繁琐的 JDBC 代码。
SqlSession
主要作用
执行 SQL 语句
selectOne()
:执行查询,返回单个结果selectList()
:执行查询,返回结果列表insert()
:执行插入操作update()
:执行更新操作delete()
:执行删除操作
管理事务
commit()
:提交事务rollback()
:回滚事务close()
:关闭SqlSession
,释放资源
获取 Mapper
getMapper(Class<T> type)
:获取映射接口的实现类,调用对应的 SQL 语句
SqlSession
的使用
在 MyBatis 中,我们通常通过 SqlSessionFactory
获取 SqlSession
。
1. 传统方式
1 | // 1. 读取 MyBatis 配置文件 |
2. Spring 整合方式
在 Spring 整合 MyBatis 时,我们通常不需要手动管理 SqlSession
,而是使用 @Autowired
注入 Mapper
:
1 |
|
这样,Spring 会自动管理 SqlSession
的生命周期。
SqlSession
的生命周期管理
SqlSession
的生命周期非常重要,通常有以下几种管理方式:
手动管理(如上例)
- 适用于独立 MyBatis 使用的情况,每次操作都获取新的
SqlSession
,用完后关闭。
- 适用于独立 MyBatis 使用的情况,每次操作都获取新的
Spring 事务管理
- 适用于 Spring 整合 MyBatis 的情况,Spring 通过
SqlSessionTemplate
自动管理SqlSession
,开发者不需要手动获取和关闭。
- 适用于 Spring 整合 MyBatis 的情况,Spring 通过
SqlSession
是否是线程安全的?
SqlSession
不是线程安全的!
SqlSession
设计为短生命周期对象,每个线程应该使用自己的SqlSession
实例。- 错误做法:
1
2
3public class MyDao {
private static SqlSession sqlSession; // ❌ 共享 SqlSession,会导致线程安全问题
} - 正确做法:
1
2
3
4
5
6
7
8public class MyDao {
public void someMethod() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) { // ✅ 每次获取新的 SqlSession
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.selectUserById(1);
}
}
}
MyBatis 分页方式
在 MyBatis 框架中,分页是一个常见的需求。MyBatis 提供了逻辑分页和物理分页两种方式来实现分页。下面我们详细讲解每种分页方式,并提供示例代码。
1. 分页方式分类
逻辑分页
- 方式:查询所有数据后,在内存中进行分页处理(不推荐,性能较差)。
- 适用场景:数据量较小的情况,否则会造成内存溢出。
物理分页
- 方式:在数据库层进行分页查询,直接返回所需的数据,减少数据库到应用层的传输数据量(推荐)。
- 适用场景:数据量较大时,通常采用 SQL 语句进行分页查询,提高性能。
2. 具体实现方式
1. 自己写 SQL 进行分页(物理分页)
最基础的方法是直接在 SQL 语句中使用 LIMIT
或 OFFSET
进行分页。
示例
1 | <select id="getUsersByPage" resultType="User"> |
对应的 Mapper 接口
1 | public interface UserMapper { |
调用方式
1 | int pageNum = 1; // 第1页 |
- 该方式适用于MySQL、PostgreSQL 等数据库,SQL Server 需要使用
OFFSET
和FETCH NEXT
。
2. 使用拦截器进行分页(物理分页)
MyBatis 支持拦截器,可以拦截 SQL 语句,在 SQL 执行前自动添加分页逻辑。
步骤
- 编写 MyBatis 插件,拦截 SQL 并自动添加
LIMIT
- 注册插件
- 使用拦截器进行分页
示例
1 |
|
- 这种方式适用于所有数据库,但需要手动编写拦截器。
3. 使用 PageHelper 进行分页(物理分页)
PageHelper
是 MyBatis 提供的一个分页插件,内部封装了分页逻辑,最推荐使用。
引入 PageHelper 依赖
1 | <dependency> |
示例
1 | import com.github.pagehelper.PageHelper; |
PageHelper.startPage(pageNum, pageSize);
会自动拦截 SQL 并添加LIMIT
语句。PageInfo
可以封装分页信息,包括总记录数、当前页、总页数等。
4. 使用 RowBounds 进行分页(逻辑分页,不推荐)
RowBounds
是 MyBatis 提供的逻辑分页方式,它会查询所有数据后,在内存中截取分页数据。
示例
1 | List<User> getUsers(RowBounds rowBounds); |
调用
1 | RowBounds rowBounds = new RowBounds(0, 10); // 偏移量0,取10条 |
- 缺点:会加载所有数据到内存,不推荐大数据量场景使用。
3. 对比总结
方式 | 类型 | 优势 | 劣势 | 适用场景 |
---|---|---|---|---|
自己写 SQL 分页 | 物理分页 | 控制力强,兼容性好 | 需要手写 SQL | 推荐用于所有数据库 |
MyBatis 拦截器 | 物理分页 | 透明化处理 | 需要自定义拦截器 | 适用于项目需要统一分页逻辑 |
PageHelper | 物理分页 | 易用,功能强大 | 需要引入第三方库 | 推荐,最常用方式 |
RowBounds | 逻辑分页 | 使用简单 | 查询所有数据,性能差 | 不推荐大数据量场景 |
4. 最佳实践
小型项目
- 直接写 SQL 进行分页
- 适用于少量查询,不依赖第三方库
中大型项目
- 推荐
PageHelper
- 无需改动 SQL 代码,直接插入
PageHelper.startPage()
Spring Boot + MyBatis
- 使用
PageHelper
并结合PageInfo
封装分页数据 - 结合
MyBatis-Plus
的IPage
进行分页
5. 总结
- 物理分页优于逻辑分页,推荐使用
LIMIT
、PageHelper
进行分页。 - 逻辑分页 (
RowBounds
) 适用于小数据集,不推荐大数据场景。 - 拦截器适用于全局自动分页需求,但需额外配置插件。
PageHelper
是最常见的分页插件,使用简单,推荐使用。
MyBatis 缓存机制详解(含示例代码)
在 MyBatis 中,为了减少数据库的访问,提高查询效率,引入了缓存机制。MyBatis 提供了两级缓存:
- 一级缓存(默认开启,作用域是
SqlSession
) - 二级缓存(默认关闭,作用域是
Mapper
)
1. 一级缓存
一级缓存原理
- 作用范围:
SqlSession
级别的缓存,即同一个SqlSession
查询相同数据时,MyBatis 会从缓存中取数据,而不会重复查询数据库。 - 缓存存储方式:
HashMap
- 默认开启,但在以下情况下缓存会失效:
- 使用不同的
SqlSession
(不同的SqlSession
互相隔离) - 查询条件不同
- 执行了
INSERT
、UPDATE
、DELETE
操作(数据变更,缓存失效) - 手动清空缓存
sqlSession.clearCache()
- 使用不同的
一级缓存示例
1️⃣ 配置 Mapper
1 | <select id="getUserById" parameterType="int" resultType="User"> |
2️⃣ 测试一级缓存
1 | public void testFirstLevelCache() { |
✅ 运行结果:
1 | 第一次查询: User{id=1, name='Tom'} |
- 第二次查询没有去数据库,而是从缓存获取数据,提高查询效率。
一级缓存失效场景
1️⃣ 使用不同的 SqlSession
1 | SqlSession sqlSession1 = sqlSessionFactory.openSession(); |
- 不同
SqlSession
之间缓存不共享,因此sqlSession2
还是会查询数据库。
2️⃣ 查询条件不同
1 | userMapper.getUserById(1); // 缓存数据 |
- 参数不同,相当于执行了不同的 SQL 语句,缓存不会命中。
3️⃣ 执行 INSERT
、UPDATE
、DELETE
操作
1 | userMapper.getUserById(1); // 查询并缓存 |
- 增删改操作会让缓存失效,保证数据一致性。
4️⃣ 手动清除缓存
1 | sqlSession.clearCache(); // 清空缓存 |
2. 二级缓存
二级缓存原理
- 作用范围:
Mapper
级别的缓存,多个SqlSession
共享缓存。 - 默认关闭,需要手动开启。
- 二级缓存生效的前提:
SqlSession
提交或关闭 后,查询的数据才会存入二级缓存。- 必须在
Mapper.xml
中配置<cache/>
。 - 查询对象 必须实现
Serializable
接口。 - 不同
Mapper
之间的缓存是隔离的,但相同namespace
的Mapper
可以共享。
二级缓存示例
1️⃣ 开启二级缓存
方式 1:在 application.yml
配置
1 | mybatis: |
方式 2:在 Mapper.xml
中配置
1 | <mapper namespace="com.example.mapper.UserMapper"> |
2️⃣ 测试二级缓存
1 | public void testSecondLevelCache() { |
✅ 运行结果:
1 | 第一次查询:User{id=1, name='Tom'}(查询数据库) |
- 二级缓存命中后,第二次查询不会再访问数据库,而是直接从缓存中取数据。
二级缓存失效场景
二级缓存的失效场景和一级缓存类似:
- 执行了
INSERT
、UPDATE
、DELETE
操作 - 缓存数据过期
- 手动清空缓存
- 不同的
Mapper
之间不共享缓存- 只有
Mapper.xml
的namespace
相同,缓存才会共享。
- 只有
3. 一级缓存 vs 二级缓存
对比项 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession 级别 |
Mapper 级别 |
默认是否开启 | ✅ 开启 | ❌ 关闭 |
缓存共享 | ❌ 不同 SqlSession 之间不共享 |
✅ 多个 SqlSession 共享 |
生效条件 | SqlSession 存在期间 |
SqlSession 提交或关闭后 |
存储结构 | HashMap |
HashMap 或 EhCache |
失效场景 | 增删改操作、查询条件不同、SqlSession 关闭 |
增删改操作、数据过期、不同 Mapper |
4. 总结
一级缓存(默认开启)
- 作用域:
SqlSession
级别 - 优点:减少数据库查询,提高性能
- 缺点:
SqlSession
关闭后缓存失效,不同SqlSession
不共享缓存
- 作用域:
二级缓存(默认关闭,需要手动开启)
- 作用域:
Mapper
级别,可跨SqlSession
共享缓存 - 优点:多个
SqlSession
共享缓存,提高查询效率 - 缺点:默认关闭,需要额外配置,数据一致性需注意
- 作用域:
✅ 推荐实践:
- 数据变更频繁的业务(如订单系统)➡ 关闭二级缓存
- 查询较多、变更较少的业务(如字典数据)➡ 开启二级缓存