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())
);
}


posted @ 2025-08-08 08:38  yjbjingcha  阅读(11)  评论(0)    收藏  举报