Mybatis的高级特性 - 指南
MyBatis 核心进阶
缓存机制
MyBatis 缓存是位于应用层的数据暂存区,通过减少数据库访问次数提升性能
一级缓存
本地缓存--默认当前启用的线程绑定缓存,生命周期与 SqlSession 一致
运行流程
SqlSession查询 → 缓存是否存在 → (存在)返回缓存结果、(不存在)查询数据库 → 结果存入缓存中
导致失效的操作
- 执行任意 UPDATE 操作
- 手动调用
clearCache() - 会话关闭
二级缓存
应用级缓存--用于在多个 SqlSession 之间共享数据,减少数据库访问次数,提升查询性能
二级缓存默认为关闭,需要手动开启
开启操作
全局配置
在 MyBatis 配置文件中启用缓存( <setting name="cacheEnabled" value="true"/> )
Mapper配置
在映射 XML 中添加 <cache> ,可自定义参数(如 eviction 、 size 、 flushInterval 等)
查询控制
若某条 SQL 不想用缓存,通过 useCache="true/false" 控制特定查询是否使用缓存
工作原理
查询顺序为二级缓存 → 一级缓存 → 数据库,命中二级缓存则直接返回结果。默认实现为 PerpetualCache ,支持自定义实现(如集成 Redis、EhCache 等)以满足分布式
缓存应用三定律
数据变动频率高 → 禁用缓存
查询耗时大于1ms → 考虑一级缓存
QPS > 100且数据稳定 → 启用二级缓存
用注解代替XML
每写一条SQL,都要写对应实体类Dao的XML文件做配置,是不是很复杂?现在一般情况较少使用XML这种传统的配置方式了,我们可以通过注解来做配置
配置方法
创建 Mapper 接口
直接在接口方法上使用 SQL 注解,不需要 XML 文件
public interface UserMapper {
// 简单查询
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(int id);
// 插入(返回自增ID)
@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
// 更新
@Update("UPDATE users SET name=#{name} WHERE id=#{id}")
int updateName(@Param("id") int id, @Param("name") String name);
// 删除
@Delete("DELETE FROM users WHERE id = #{id}")
int delete(int id);
}
若遇到数据表名与接口名对不上的情况,解决方式与xml相似
public interface UserDao {
@Results(id = "userMap",value = {
@Result(id = true ,column = "id" ,property = "userId"),
@Result(column = "username" ,property = "userName"),
@Result(column = "birthday" ,property = "userBirthday"),
@Result(column = "sex" ,property = "userSex"),
@Result(column = "address" ,property = "userAddress")
})
@Select("select * from user where id = #{id}")
public User findUserById(Integer id);
在测试类尝试运行,与XML文件无异
总结及补充
核心思路:在 Mapper 接口方法上直接使用 @Select、@Insert 等注解替代 XML。
动态 SQL:通过 @SelectProvider 配合 Java 类实现。
关联映射:用 @Results + @Result + @One/@Many 配置。
适用性:优先用于简单 CRUD,复杂场景仍建议结合 XML。
在适用场景上
简单 CRUD:直接使用 @Select/@Insert 等
中等复杂 SQL:用 @SelectProvider + SQL 工具类
复杂动态 SQL:仍推荐 XML(注解中拼接 SQL 可读性差)
与XML优缺点对比
| 优点 | 缺点 |
|---|---|
| 代码更集中(SQL 与 Java 在一起) | 复杂 SQL 可读性差 |
| 编译时检查 SQL 语法 | 动态 SQL 编写繁琐 |
| 避免 XML 文件切换 | 关联映射配置冗长 |
| 适合简单操作 | 多表联查建议用 XML |
插件机制
在四大对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法前后插入自己的逻辑,例如:
• 打印 SQL 耗时
• 动态改写 SQL(加 limit、加租户字段)
• 参数加解密
通过实现 Interceptor 接口,在类上加 @Intercepts 与 @Signature 指定拦截点。
用途:分页、审计日志、性能监控
批处理
一次网络往返发送多条 SQL,提高写入/更新吞吐量。
批处理黄金公式 最佳批次大小 = 数据库最大包大小 / 单行数据大小MySQL默认max_allowed_packet=4MB
配置
JDBC URL 加参数:
jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true&rewriteBatchedStatements=true
Mapper.xml:多条语句用分号隔开
UPDATE user SET age = #{u.age} WHERE id = #{u.id}
Java 调用测试
List list = Arrays.asList(new User(1,22), new User(2,23));
sqlSession.update("batchUpdate", list);
相当于生成了
UPDATE user SET age = 22 WHERE id = 1;
UPDATE user SET age = 23 WHERE id = 2;
延迟加载
只在真正用到关联对象时才发出第二条 SQL,避免一次查太多数据。
配置
mybatis-config.xml 全局开关
Mapper.xml
fetchType="lazy" 可局部覆盖
Java示例
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User u = mapper.selectById(1); // ① 只发一条 SQL:SELECT * FROM user WHERE id=1
System.out.println(u.getName()); // ② 仍不发 orders SQL
List orders = u.getOrders(); // ③ 第一次访问触发第二条 SQL
System.out.println(orders.size());
}
第①步只查 user 表。
第③步第一次用到 orders 时,
MyBatis 自动补发 SELECT * FROM orders WHERE user_id = 1 ,完成延迟加载。
Generator代码生成器
MyBatis Generator 可以通过数据库表结构 构建 Java 领域模型 + DAO 层代码
graph TB
DB[数据库] -->|JDBC| MBG[MyBatis Generator]
MBG -->|模板引擎| POJO[实体类]
MBG --> MapperInt[Mapper接口]
MBG --> MapperXML[Mapper XML]
MBG --> Example[查询条件类]
工作流程
数据库数据源 → MyBatis Generator → 模板引擎、pojo实体类、Mapper接口、XML、条件类
在配置这个之前,需要添加相关的依赖
com.github.pagehelper
pagehelper
5.2.0
文件(必须要叫generatorConfig.xml)
generatorConfig.xml(这个很重要)
-->
填入数据库以及开发与resources的文件生成策略(即格式)之后,可以点击maven的

开始生成代码
生成无误之后可以在测试包添加测试
@Test
public void test01() throws Exception {
//selectByExample为根据条件筛选,若其中填null,则全盘扫描
List userList = userMapper.selectByExample(null);
for (User user : userList) {
System.out.println(user);
}
}
测试代码生成器生成的功能,这样可以让我们在传统的crud中节省更多的精力
分页查询
将数据库中的大量数据分割成多个小块(页)进行展示,避免一次性加载成千上万条数据,减少单次查询的数据传输量,降低数据库查询负载
提升用户体验 减轻服务器压力 优化性能
分页流程
接收参数:获取前端传入的 pageNum 和 pageSize
计算偏移:offset = (pageNum-1) * pageSize
执行查询:
物理分页:通过数据库的 LIMIT/ROWNUM 实现/插件分页:使用 PageHelper.startPage()
返回结果:包装为包含分页信息的对象(总条数/总页数等)
最好不要在内存中处理大数据分页,让数据库完成分页截取
核心参数
| 参数名 | 作用 | 示例 | 计算公式 |
|---|---|---|---|
| pageNum | 当前页码 | 第3页 | 前端传入 |
| pageSize | 每页显示条数 | 每页10条 | 常取10/20/50 |
| total | 总数据量 | 125条 | SELECT COUNT(*) |
| offset | 数据起始位置 | 第20条开始 | (pageNum-1)*pageSize |
基础分页实现方式
物理分页(推荐)
在数据库层面截取数据片段,性能高、内存消耗小
-- MySQL
SELECT * FROM products
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize} -- 从offset位置取pageSize条
-- Oracle
SELECT * FROM (
SELECT t.*, ROWNUM rn FROM (
SELECT * FROM products ORDER BY create_time DESC
) t WHERE ROWNUM = #{start}
逻辑分页
先获取全部数据,再在内存中截取,数据量大会导致内存溢出、传输全部数据浪费网络带宽
// MyBatis RowBounds示例(已过时)
List list = sqlSession.selectList(
"selectProducts",
null,
new RowBounds(offset, pageSize) // 内存分页
);
示例
实体类
public class Product {
private Long id;
private String name;
private Double price;
// 构造方法/getter/setter 省略
}
Mapper 接口
@Mapper
public interface ProductMapper {
// 手写分页查询
List selectByPage(@Param("offset") int offset,
@Param("pageSize") int pageSize);
// 获取总数
Long selectTotalCount();
// PageHelper 查询(无需特殊参数)
List selectAll();
}
Mapper XML
SELECT id, name, price
FROM products
ORDER BY id DESC
LIMIT #{offset}, #{pageSize}
SELECT COUNT(1) FROM products
SELECT id, name, price
FROM products
ORDER BY id DESC
分页结果封装类
public class PageResult {
private int pageNum; // 当前页码
private int pageSize; // 每页条数
private long total; // 总记录数
private int pages; // 总页数
private List list; // 当前页数据
public PageResult(int pageNum, int pageSize, long total, List list) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.list = list;
this.pages = (int) Math.ceil((double) total / pageSize);
}
// Getter 省略
}
服务层实现 仅展示手写分页
// 手写分页
public PageResult getProductsManual(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
List list = productMapper.selectByPage(offset, pageSize);
long total = productMapper.selectTotalCount();
return new PageResult<>(pageNum, pageSize, total, list);
}
Test类 仅展示手写分页
/**
* 测试手写分页
*/
@Test
void testManualPagination() {
// 请求第2页,每页10条
PageResult result = productService.getProductsManual(2, 10);
System.out.println("===== 手写分页结果 =====");
System.out.println("当前页: " + result.getPageNum());
System.out.println("每页条数: " + result.getPageSize());
System.out.println("总记录数: " + result.getTotal());
System.out.println("总页数: " + result.getPages());
System.out.println("当前页数据: ");
result.getList().forEach(p ->
System.out.printf("| %2d | %-20s | %.2f |%n",
p.getId(), p.getName(), p.getPrice())
);
}
浙公网安备 33010602011771号