MyBatis 常见问题

SqlSession

SqlSession 是 MyBatis 框架中的核心接口之一,它用于与数据库交互并执行 SQL 语句。MyBatis 是一个流行的持久层框架,它简化了 JDBC 操作,使得开发人员可以使用 SQL 语句直接操作数据库,而不需要繁琐的 JDBC 代码。


SqlSession 主要作用

  1. 执行 SQL 语句

    • selectOne():执行查询,返回单个结果
    • selectList():执行查询,返回结果列表
    • insert():执行插入操作
    • update():执行更新操作
    • delete():执行删除操作
  2. 管理事务

    • commit():提交事务
    • rollback():回滚事务
    • close():关闭 SqlSession,释放资源
  3. 获取 Mapper

    • getMapper(Class<T> type):获取映射接口的实现类,调用对应的 SQL 语句

SqlSession 的使用

在 MyBatis 中,我们通常通过 SqlSessionFactory 获取 SqlSession

1. 传统方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 读取 MyBatis 配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

// 2. 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 3. 获取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
// 4. 获取 Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 5. 执行查询
User user = userMapper.selectUserById(1);
System.out.println(user);

// 6. 提交事务(若有修改操作)
sqlSession.commit();
} finally {
// 7. 关闭 SqlSession
sqlSession.close();
}

2. Spring 整合方式

在 Spring 整合 MyBatis 时,我们通常不需要手动管理 SqlSession,而是使用 @Autowired 注入 Mapper

1
2
3
4
5
6
7
8
9
@Service
public class UserService {
@Autowired
private UserMapper userMapper;

public User getUserById(int id) {
return userMapper.selectUserById(id);
}
}

这样,Spring 会自动管理 SqlSession 的生命周期。


SqlSession 的生命周期管理

SqlSession 的生命周期非常重要,通常有以下几种管理方式:

  1. 手动管理(如上例)

    • 适用于独立 MyBatis 使用的情况,每次操作都获取新的 SqlSession,用完后关闭。
  2. Spring 事务管理

    • 适用于 Spring 整合 MyBatis 的情况,Spring 通过 SqlSessionTemplate 自动管理 SqlSession,开发者不需要手动获取和关闭。

SqlSession 是否是线程安全的?

SqlSession 不是线程安全的!

  • SqlSession 设计为短生命周期对象,每个线程应该使用自己的 SqlSession 实例。
  • 错误做法:
    1
    2
    3
    public class MyDao {
    private static SqlSession sqlSession; // ❌ 共享 SqlSession,会导致线程安全问题
    }
  • 正确做法:
    1
    2
    3
    4
    5
    6
    7
    8
    public 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 语句中使用 LIMITOFFSET 进行分页。

示例

1
2
3
4
<select id="getUsersByPage" resultType="User">
SELECT * FROM users
LIMIT #{offset}, #{pageSize}
</select>

对应的 Mapper 接口

1
2
3
public interface UserMapper {
List<User> getUsersByPage(@Param("offset") int offset, @Param("pageSize") int pageSize);
}

调用方式

1
2
3
4
5
int pageNum = 1; // 第1页
int pageSize = 10; // 每页10条数据
int offset = (pageNum - 1) * pageSize;

List<User> users = userMapper.getUsersByPage(offset, pageSize);
  • 该方式适用于MySQL、PostgreSQL 等数据库,SQL Server 需要使用 OFFSETFETCH NEXT

2. 使用拦截器进行分页(物理分页)

MyBatis 支持拦截器,可以拦截 SQL 语句,在 SQL 执行前自动添加分页逻辑。

步骤

  1. 编写 MyBatis 插件,拦截 SQL 并自动添加 LIMIT
  2. 注册插件
  3. 使用拦截器进行分页

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();

// 手动添加 LIMIT 分页
sql = sql + " LIMIT 10 OFFSET 0";

Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, sql);

return invocation.proceed();
}
}
  • 这种方式适用于所有数据库,但需要手动编写拦截器。

3. 使用 PageHelper 进行分页(物理分页)

PageHelper 是 MyBatis 提供的一个分页插件,内部封装了分页逻辑,最推荐使用。

引入 PageHelper 依赖

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;

public void getUsersByPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.getAllUsers();

// 使用 PageInfo 封装分页结果
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("当前页:" + pageInfo.getPageNum());
System.out.println("每页数量:" + pageInfo.getPageSize());
}
  • PageHelper.startPage(pageNum, pageSize); 会自动拦截 SQL 并添加 LIMIT 语句。
  • PageInfo 可以封装分页信息,包括总记录数、当前页、总页数等。

4. 使用 RowBounds 进行分页(逻辑分页,不推荐)

RowBounds 是 MyBatis 提供的逻辑分页方式,它会查询所有数据后,在内存中截取分页数据。

示例

1
List<User> getUsers(RowBounds rowBounds);

调用

1
2
RowBounds rowBounds = new RowBounds(0, 10); // 偏移量0,取10条
List<User> users = userMapper.getUsers(rowBounds);
  • 缺点:会加载所有数据到内存,不推荐大数据量场景使用

3. 对比总结

方式 类型 优势 劣势 适用场景
自己写 SQL 分页 物理分页 控制力强,兼容性好 需要手写 SQL 推荐用于所有数据库
MyBatis 拦截器 物理分页 透明化处理 需要自定义拦截器 适用于项目需要统一分页逻辑
PageHelper 物理分页 易用,功能强大 需要引入第三方库 推荐,最常用方式
RowBounds 逻辑分页 使用简单 查询所有数据,性能差 不推荐大数据量场景

4. 最佳实践

小型项目

  • 直接写 SQL 进行分页
  • 适用于少量查询,不依赖第三方库

中大型项目

  • 推荐 PageHelper
  • 无需改动 SQL 代码,直接插入 PageHelper.startPage()

Spring Boot + MyBatis

  • 使用 PageHelper 并结合 PageInfo 封装分页数据
  • 结合 MyBatis-PlusIPage 进行分页

5. 总结

  1. 物理分页优于逻辑分页,推荐使用 LIMITPageHelper 进行分页。
  2. 逻辑分页 (RowBounds) 适用于小数据集,不推荐大数据场景。
  3. 拦截器适用于全局自动分页需求,但需额外配置插件。
  4. PageHelper 是最常见的分页插件,使用简单,推荐使用。

MyBatis 缓存机制详解(含示例代码)

在 MyBatis 中,为了减少数据库的访问,提高查询效率,引入了缓存机制。MyBatis 提供了两级缓存:

  • 一级缓存(默认开启,作用域是 SqlSession
  • 二级缓存(默认关闭,作用域是 Mapper

1. 一级缓存

一级缓存

一级缓存原理

  • 作用范围SqlSession 级别的缓存,即同一个 SqlSession 查询相同数据时,MyBatis 会从缓存中取数据,而不会重复查询数据库。
  • 缓存存储方式HashMap
  • 默认开启,但在以下情况下缓存会失效:
    1. 使用不同的 SqlSession(不同的 SqlSession 互相隔离)
    2. 查询条件不同
    3. 执行了 INSERTUPDATEDELETE 操作(数据变更,缓存失效)
    4. 手动清空缓存 sqlSession.clearCache()

一级缓存示例

1️⃣ 配置 Mapper

1
2
3
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>

2️⃣ 测试一级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void testFirstLevelCache() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);

// 第一次查询
User user1 = userMapper1.getUserById(1);
System.out.println("第一次查询: " + user1);

// 再次查询相同的数据
User user2 = userMapper1.getUserById(1);
System.out.println("第二次查询: " + user2);

sqlSession1.close(); // 关闭 SqlSession(清空一级缓存)
}

✅ 运行结果

1
2
第一次查询: User{id=1, name='Tom'}
第二次查询: User{id=1, name='Tom'}(缓存命中,未查询数据库)
  • 第二次查询没有去数据库,而是从缓存获取数据,提高查询效率。

一级缓存失效场景

1️⃣ 使用不同的 SqlSession

1
2
3
4
5
6
7
8
9
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.getUserById(1);
sqlSession1.close(); // 关闭 SqlSession,一级缓存失效

SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUserById(1);
sqlSession2.close();
  • 不同 SqlSession 之间缓存不共享,因此 sqlSession2 还是会查询数据库。

2️⃣ 查询条件不同

1
2
userMapper.getUserById(1);  // 缓存数据
userMapper.getUserById(2); // 新的 SQL,缓存未命中,查询数据库
  • 参数不同,相当于执行了不同的 SQL 语句,缓存不会命中。

3️⃣ 执行 INSERTUPDATEDELETE 操作

1
2
3
4
5
userMapper.getUserById(1); // 查询并缓存

userMapper.updateUser(new User(1, "Updated Name")); // 修改操作,缓存失效

userMapper.getUserById(1); // 重新查询数据库
  • 增删改操作会让缓存失效,保证数据一致性。

4️⃣ 手动清除缓存

1
sqlSession.clearCache(); // 清空缓存

2. 二级缓存

二级缓存

二级缓存原理

  • 作用范围Mapper 级别的缓存,多个 SqlSession 共享缓存。
  • 默认关闭,需要手动开启。
  • 二级缓存生效的前提
    1. SqlSession 提交或关闭 后,查询的数据才会存入二级缓存。
    2. 必须在 Mapper.xml 中配置 <cache/>
    3. 查询对象 必须实现 Serializable 接口
    4. 不同 Mapper 之间的缓存是隔离的,但相同 namespaceMapper 可以共享。

二级缓存示例

1️⃣ 开启二级缓存

方式 1:在 application.yml 配置
1
2
3
mybatis:
configuration:
cache-enabled: true
方式 2:在 Mapper.xml 中配置
1
2
3
4
5
6
7
8
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>

<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>

2️⃣ 测试二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testSecondLevelCache() {
// 第一次查询
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.getUserById(1);
System.out.println("第一次查询:" + user1);
sqlSession1.close(); // 关闭 SqlSession,将数据写入二级缓存

// 第二次查询(新会话)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUserById(1);
System.out.println("第二次查询:" + user2);
sqlSession2.close();
}

✅ 运行结果

1
2
第一次查询:User{id=1, name='Tom'}(查询数据库)
第二次查询:User{id=1, name='Tom'}(缓存命中,未查询数据库)
  • 二级缓存命中后,第二次查询不会再访问数据库,而是直接从缓存中取数据。

二级缓存失效场景

二级缓存的失效场景和一级缓存类似:

  1. 执行了 INSERTUPDATEDELETE 操作
  2. 缓存数据过期
  3. 手动清空缓存
  4. 不同的 Mapper 之间不共享缓存
    • 只有 Mapper.xmlnamespace 相同,缓存才会共享。

3. 一级缓存 vs 二级缓存

对比项 一级缓存 二级缓存
作用范围 SqlSession 级别 Mapper 级别
默认是否开启 ✅ 开启 ❌ 关闭
缓存共享 ❌ 不同 SqlSession 之间不共享 ✅ 多个 SqlSession 共享
生效条件 SqlSession 存在期间 SqlSession 提交或关闭后
存储结构 HashMap HashMapEhCache
失效场景 增删改操作、查询条件不同、SqlSession 关闭 增删改操作、数据过期、不同 Mapper

4. 总结

  1. 一级缓存(默认开启)

    • 作用域SqlSession 级别
    • 优点:减少数据库查询,提高性能
    • 缺点SqlSession 关闭后缓存失效,不同 SqlSession 不共享缓存
  2. 二级缓存(默认关闭,需要手动开启)

    • 作用域Mapper 级别,可跨 SqlSession 共享缓存
    • 优点:多个 SqlSession 共享缓存,提高查询效率
    • 缺点:默认关闭,需要额外配置,数据一致性需注意

推荐实践

  • 数据变更频繁的业务(如订单系统)➡ 关闭二级缓存
  • 查询较多、变更较少的业务(如字典数据)➡ 开启二级缓存