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 id="batchUpdate"><foreach collection="list" item="u" separator=";">UPDATE user SET age = #{u.age} WHERE id = #{u.id}</foreach>
</update>
Java 调用测试
List<User> 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 全局开关
<settings><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/>
</settings>
Mapper.xml
fetchType="lazy" 可局部覆盖
<resultMap id="userMap" type="User"><id column="id" property="id"/><collection property="orders" ofType="Order"column="id" select="selectOrdersByUserId"fetchType="lazy"/>
</resultMap>
Java示例
try (SqlSession session = factory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);User u = mapper.selectById(1); // ① 只发一条 SQL:SELECT * FROM user WHERE id=1System.out.println(u.getName()); // ② 仍不发 orders SQLList<Order> orders = u.getOrders(); // ③ 第一次访问触发第二条 SQLSystem.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、条件类
在配置这个之前,需要添加相关的依赖
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.2.0</version></dependency>
文件(必须要叫generatorConfig.xml)
generatorConfig.xml(这个很重要)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration><!--targetRuntime: 执行生成的逆向工程的版本MyBatis3Simple: 生成基本的CRUD(低配)MyBatis3: 生成带条件的CRUD(高配)--><context id="DB2Tables" targetRuntime="MyBatis3"><!-- 数据库的连接信息 --><jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"connectionURL="jdbc:mysql://localhost:3306/mybatis?currentSchema=mybatis&useSSL=false&serverTimezone=UTC"userId="root"password="123456"></jdbcConnection><!-- javaBean的生成策略--><javaModelGenerator targetPackage="com.noy.pojo" targetProject="src\main\java"><!--是否生成子包。如果为true com.noy.pojo生成的保姆那个带有层级
目录false com.noy.pojo就是一个包名--><property name="enableSubPackages" value="true" /><!--通过数据表字段生成pojo。如果字段名称带空格,会去掉空格--><property name="trimStrings" value="true" /></javaModelGenerator><!-- SQL映射文件的生成策略 --><sqlMapGenerator targetPackage="com.noy" targetProject="src\main\resources"><property name="enableSubPackages" value="true" /></sqlMapGenerator><!-- Mapper接口的生成策略 --><javaClientGenerator type="XMLMAPPER"targetPackage="com.noy" targetProject="src\main\java"><property name="enableSubPackages" value="true" /></javaClientGenerator><!-- 逆向分析的表 --><!-- tableName设置为*号,可以对应所有表,此时不写
domainObjectName --><!-- domainObjectName属性指定生成出来的实体类的类名 --><table tableName="`user`"domainObjectName="User"catalog="mybatis">
<!-- schema="mybatis">--><generatedKey column="id" sqlStatement="JDBC" identity="true"/></table></context>
</generatorConfiguration>
填入数据库以及开发与resources的文件生成策略(即格式)之后,可以点击maven的
开始生成代码
生成无误之后可以在测试包添加测试
@Testpublic void test01() throws Exception {//selectByExample为根据条件筛选,若其中填null,则全盘扫描List<User> 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 <= #{end}
) WHERE rn >= #{start}
逻辑分页
先获取全部数据,再在内存中截取,数据量大会导致内存溢出、传输全部数据浪费网络带宽
// MyBatis RowBounds示例(已过时)
List<Product> 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<Product> selectByPage(@Param("offset") int offset, @Param("pageSize") int pageSize);// 获取总数Long selectTotalCount();// PageHelper 查询(无需特殊参数)List<Product> selectAll();
}
Mapper XML
<!-- 手写分页 -->
<select id="selectByPage" resultType="Product">SELECT id, name, priceFROM productsORDER BY id DESCLIMIT #{offset}, #{pageSize}
</select><!-- 总数统计 -->
<select id="selectTotalCount" resultType="Long">SELECT COUNT(1) FROM products
</select><!-- PageHelper 查询 -->
<select id="selectAll" resultType="Product">SELECT id, name, priceFROM productsORDER BY id DESC
</select>
分页结果封装类
public class PageResult<T> {private int pageNum; // 当前页码private int pageSize; // 每页条数private long total; // 总记录数private int pages; // 总页数private List<T> list; // 当前页数据public PageResult(int pageNum, int pageSize, long total, List<T> list) {this.pageNum = pageNum;this.pageSize = pageSize;this.total = total;this.list = list;this.pages = (int) Math.ceil((double) total / pageSize);}// Getter 省略
}
服务层实现 仅展示手写分页
// 手写分页public PageResult<Product> getProductsManual(int pageNum, int pageSize) {int offset = (pageNum - 1) * pageSize;List<Product> list = productMapper.selectByPage(offset, pageSize);long total = productMapper.selectTotalCount();return new PageResult<>(pageNum, pageSize, total, list);}
Test类 仅展示手写分页
/*** 测试手写分页*/@Testvoid testManualPagination() {// 请求第2页,每页10条PageResult<Product> 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()));}