Mybatis 框架学习
ORM 思想
什么是 ORM
ORM(Object-Relational Mapping,对象关系映射) 是一种编程技术,旨在通过 面向对象的方式 操作关系型数据库。其核心思想是将 数据库表 映射为 编程语言中的类,将 表中的行 映射为 对象实例,将 表的列 映射为 对象的属性,从而屏蔽底层数据库的细节,让开发者以操作对象的方式操作数据库。
为什么需要 ORM
传统数据库操作的痛点
- 繁琐的 SQL 硬编码:需手动编写 SQL 语句,维护困难。
- 重复的结果集处理:需将
ResultSet
逐行解析为对象。 - 数据库耦合性高:SQL 语法与具体数据库绑定,移植性差。
- 安全隐患:手动拼接 SQL 易引发 SQL 注入攻击。
ORM 的解决思路
- 抽象数据库操作:通过面向对象 API 替代原生 SQL。
- 自动化映射:自动将对象属性与表字段绑定。
- 统一数据访问层:屏蔽数据库差异,提升代码可移植性。
ORM 的核心思想
表与类的映射
- 表(Table) ↔ 类(Class)
- 行(Row) ↔ 对象实例(Object)
- 列(Column) ↔ 对象属性(Property)
示例:
-
数据库表
user
:CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT );
-
对应的 Java 类:
public class User { private Integer id; private String name; private Integer age; // Getter & Setter }
关系的映射
-
一对一(如
User
↔IDCard
)通过外键或共享主键实现。
-
一对多(如
Department
↔User
)通过外键在“多”的一方建立关联。
-
多对多(如
User
↔Role
)通过中间表(如
user_role
)实现。
CRUD 操作的抽象
- Create:
userDao.save(user)
→INSERT INTO user ...
- Read:
userDao.findById(1)
→SELECT * FROM user WHERE id=1
- Update:
userDao.update(user)
→UPDATE user SET ...
- Delete:
userDao.delete(1)
→DELETE FROM user WHERE id=1
ORM 的优势
优势 | 说明 |
---|---|
提高开发效率 | 减少 SQL 编写和结果集解析代码。 |
代码可维护性高 | 数据操作集中在对象层面,逻辑清晰。 |
数据库无关性 | 更换数据库时只需修改配置,无需重写 SQL。 |
防止 SQL 注入 | 通过参数化查询(如 PreparedStatement )自动处理。 |
支持高级特性 | 如事务管理、缓存、延迟加载等。 |
ORM 的局限性
局限性 | 说明 |
---|---|
性能开销 | 自动生成的 SQL 可能不够优化,复杂查询效率低。 |
学习成本 | 需掌握 ORM 框架的配置和特性(如 Hibernate 的 HQL、Session 管理)。 |
灵活性受限 | 复杂查询或数据库特性(如存储过程)难以通过 ORM 直接实现。 |
对象与表的阻抗失配 | 继承、多态等面向对象特性在关系模型中无直接对应。 |
常见 ORM 框架
框架 | 语言 | 特点 |
---|---|---|
Hibernate | Java | 全自动 ORM,支持 JPA 标准,强调对象化操作。 |
MyBatis | Java | 半自动 ORM,SQL 与代码解耦,灵活性高。 |
SQLAlchemy | Python | 支持 ORM 和原生 SQL,适用于复杂查询。 |
Entity Framework | .NET | 微软官方 ORM,集成 LINQ,支持 Code First 开发模式。 |
Django ORM | Python | 高度集成于 Django 框架,简单易用。 |
ORM 使用场景
场景 | 是否适合 ORM | 说明 |
---|---|---|
常规 CRUD 操作 | ✔️ 适合 | ORM 自动生成高效 SQL,简化开发。 |
复杂报表查询 | ⚠️ 谨慎使用 | 手动编写 SQL 或结合原生查询更灵活。 |
高频事务系统 | ⚠️ 需优化配置 | ORM 缓存和批量操作可提升性能,但需避免 N+1 查询等问题。 |
微服务架构 | ✔️ 适合 | ORM 帮助快速实现数据访问层,适配不同数据库。 |
ORM 最佳实践
-
合理设计数据模型
- 避免过度嵌套对象,优先使用扁平化结构。
- 合理使用懒加载(Lazy Loading)避免不必要的数据加载。
-
优化查询性能
- 使用批量操作(如
saveAll()
)减少数据库交互。 - 避免
SELECT *
,按需查询字段。 - 通过
JOIN FETCH
(Hibernate)或<association>
(MyBatis)解决 N+1 查询问题。
- 使用批量操作(如
-
结合原生 SQL
- 复杂查询直接使用原生 SQL 或存储过程。
// JPA 原生 SQL 示例 @Query(value = "SELECT * FROM user WHERE age > :age", nativeQuery = true) List<User> findUsersByAge(@Param("age") int age);
-
事务管理
- 使用声明式事务(如
@Transactional
)确保操作原子性。 - 控制事务粒度,避免长事务占用连接。
- 使用声明式事务(如
总结
ORM 思想通过 对象与关系的映射,将数据库操作抽象为面向对象的 API,显著提升了开发效率和代码可维护性。尽管存在性能调优和复杂查询的挑战,但在大多数企业应用中,ORM 仍是平衡开发效率与维护成本的最佳选择。选择合适的 ORM 框架(如 Hibernate 的全面性、MyBatis 的灵活性),并结合原生 SQL 补充其不足,是构建高效数据访问层的关键。
Mybatis 简介
MyBatis 是一款半自动化的持久层 ORM 框架(Persistence Framework),它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,通过 XML 或注解实现 Java 对象与关系型数据库的映射(ORM)。其核心思想是将 SQL 与业务代码解耦,开发者可完全控制 SQL 语句,同时实现参数映射和结果集转换的自动化。
核心定位:
- 轻量级 ORM 框架:比 Hibernate 更轻量,仅关注 SQL 映射,避免全 ORM 的复杂性。
- SQL 与 Java 解耦:SQL 语句独立于 Java 代码,便于维护和优化。
- 灵活性与高性能:支持动态 SQL、批量操作和缓存机制,提升开发效率。
核心特性
-
半自动化设计
- 相比全自动化的 Hibernate,需要手动编写 SQL。
- 提供结果集到对象的自动映射
- 支持存储过程、复杂联表查询等高级功能
-
动态SQL能力
通过 XML 标签实现条件分支(
<if>
、<choose>
)、循环(<foreach>
)等逻辑处理动态 SQL = 传统 JDBC 条件拼接 + 自动防注入处理
-
轻量级架构
核心 JAR 包仅约2.7MB,无第三方依赖。
核心架构组成
组件 | 作用描述 |
---|---|
SqlSessionFactory | 全局单例,通过 mybatis-config.xml 构建 |
SqlSession | 线程非安全的数据库会话对象,生命周期与请求绑定 |
Mapper 接口 | 通过动态代理绑定到 XML 映射文件 |
Executor | 执行器,处理缓存、事务等底层操作 |
优点与缺点
优点:
- 灵活控制 SQL,适合复杂查询优化。
- 学习曲线平缓,易于调试。
- 与 Spring 等框架集成良好。
缺点:
- 需要手动编写较多 SQL。
- 多表关联映射配置较复杂。
使用场景
- 需要高度优化 SQL 性能的项目。
- 遗留数据库系统(表结构不规范)。
- 需同时使用存储过程与 ORM 的场景。
持久层框架对比
- JDBC
- SQL 夹杂在 Java 代码中耦合度高,导致硬编码内伤。
- 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见。
- 代码冗长,开发效率低
- Hibernate 和 JPA
- 操作简便,开发效率高。
- 程序中的长难复杂 SQL 需要绕过框架。
- 内部自动生成的 SQL,不容易做特殊优化。
- 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
- 反射操作太多,导致数据库性能下降。
- MyBatis
- 轻量级,性能出色
- SQL 和 Java 编码分开,功能边界清晰。Java 代码专注业务、SQL 语句专注数据。
- 开发效率稍逊于 Hibernate,但是完全能够接收
快速开始
- 准备数据库
CREATE DATABASE `mybatis-test`;
USE `mybatis-test`;
CREATE TABLE `user` (
`id` int AUTO_INCREMENT,
`name` varchar(10),
`age` int,
`sex` varchar(2),
PRIMARY KEY (`id`)
);
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('张三', 18, '男');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('李四', 30, '男');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('王五', 6, '女');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('赵六', 99, '女');
- 创建项目,导入依赖
<dependencies>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<!-- MySQL驱动 mybatis底层依赖jdbc驱动实现,本次不需要导入连接池,mybatis自带! -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
- 准备实体类
@Data
public class User {
private Integer id;
private String name;
private Integer age;
private String sex;
}
-
准备 Mapper 接口和 Mapper XML 文件。
MyBatis 框架下,SQL 语句编写位置发生改变,从原来的 Java 类,改成 XML 或者注解定义。
推荐在 XML 文件中编写 SQL 语句,让用户能更专注于 SQL 代码,不用关注其他的 JDBC 代码。
如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。
一般编写 SQL 语句的文件命名:
XxxMapper.xml
Xxx 一般取表名。Mybatis 中的 Mapper 接口相当于以前的 Dao。但是区别在于,Mapper 仅仅只是建接口即可,我们不需要提供实现类,具体的 SQL 写到对应的 Mapper 文件,该用法的思路如下图所示:
mapper 接口:
public interface UserMapper {
// 根据id查询用户
User getUserById(Integer id);
}
编写 mapper xml 文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace等于mapper接口类的全限定名,这样实现对应 -->
<mapper namespace="com.yigongsui.mapper.UserMapper">
<!-- 查询使用 select标签
id = 方法名
resultType = 返回值类型
标签内编写SQL语句
-->
<select id="getUserById" resultType="com.yigongsui.pojo.User">
<!-- #{id}代表动态传入的参数,id为方法的形参名,并且进行赋值!后面详细讲解 -->
select id, name, age, sex from user where id = #{id};
</select>
</mapper>
注意:
- 方法名和 SQL 的 id 一致。
- 方法返回值和 resultType 一致。
- 方法的参数和 SQL 的参数一致。
- 接口的全类名和映射配置文件的名称空间一致。
-
编写 Mybatis 配置文件
mybatis 框架配置文件: 数据库连接信息,性能配置,
mapper.xml
配置等!习惯上命名为
mybatis-config.xml
,这个文件名仅仅只是建议,并非强制要求。将来整合 Spring 之后,这个配置文件可以省略。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- environments表示配置Mybatis的开发环境,可以配置多个环境,在众多具体环境中,使用default属性指定实际运行时使用的环境。default属性的取值是environment标签的id属性的值。 -->
<environments default="development">
<!-- environment表示配置Mybatis的一个具体的环境 -->
<environment id="development">
<!-- Mybatis的内置的事务管理器 -->
<transactionManager type="JDBC"/>
<!-- 配置数据源 -->
<dataSource type="POOLED">
<!-- 建立数据库连接的具体信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis-test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- Mapper注册:指定Mybatis映射文件的具体位置 -->
<!-- mapper标签:配置一个具体的Mapper映射文件 -->
<!-- resource属性:指定Mapper映射文件的实际存储位置,这里需要使用一个以类路径根目录为基准的相对路径 -->
<!-- 对Maven工程的目录结构来说,resources目录下的内容会直接放入类路径,所以这里我们可以以resources目录为基准 -->
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
- 测试
public class MyBatisTest {
@Test
public void testSelectEmployee() throws IOException {
// 1.创建SqlSessionFactory对象
// ①声明Mybatis全局配置文件的路径
String mybatisConfigFilePath = "mybatis-config.xml";
// ②以输入流的形式加载Mybatis配置文件
InputStream inputStream = Resources.getResourceAsStream(mybatisConfigFilePath);
// ③基于读取Mybatis配置文件的输入流创建SqlSessionFactory对象
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2.使用SqlSessionFactory对象开启一个会话
SqlSession session = sessionFactory.openSession();
// 3.根据EmployeeMapper接口的Class对象获取Mapper接口类型的对象(动态代理技术)
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
// 4. 调用代理类方法既可以触发对应的SQL语句
Employee employee = employeeMapper.selectEmployee(1);
System.out.println("employee = " + employee);
// 4.关闭SqlSession
session.commit(); //提交事务 [DQL不需要,其他需要]
session.close(); //关闭会话
}
}
说明:
SqlSession
:代表 Java 程序和数据库之间的会话。(HttpSession
是 Java 程序和浏览器之间的会话)SqlSessionFactory
:是“生产” SqlSession 的“工厂”。- 工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。
Mybatis 基本使用
Mybatis 配置文件常用属性
Mybatis 配置文件就是上面的 mybatis-config.xml
文件。
结果如下:
configuration
(配置)properties
(属性)settings
(设置)typeAliases
(类型别名)typeHandlers
(类型处理器)objectFactory
(对象工厂)plugins
(插件)environments
(环境配置)environment
(环境变量)transactionManager
(事务管理器)dataSource
(数据源)
databaseIdProvider
(数据库厂商标识)mappers
(映射器)
配置文件的元素配置时需要严格按照上面的顺序来配置,否则报错。
我们上面的配置文件配置了 environments
和 mappers
元素,environments
元素必须要在 mappers
上面。
environments
元素
environments
属性是用于配置数据库环境的核心部分,可以配置多个数据库环境,例如实际开发中会有生产环境和测试环境。
<environments default="生产环境">
<environment id="生产环境">
</environment>
<environment id="测试环境">
</environment>
</environments>
不同的环境分别使用 environment
标签指定,最后在 environments
标签的 default
指定要使用哪个环境。
environment
子元素
<environment id="development">
<!-- Mybatis的内置的事务管理器 -->
<transactionManager type="JDBC"/>
<!-- 配置数据源 -->
<dataSource type="POOLED">
<!-- 建立数据库连接的具体信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis-test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
id
属性:唯一标识一个环境(如 development
、test
、production
)。
transactionManager
标签:定义事务管理器,可选类型:
JDBC
:使用 JDBC 的事务管理(默认),依赖 Connection 的提交和回滚。MANAGED
:由容器(如 Spring)管理事务,MyBatis 不干预事务生命周期。
dataSource
标签:配置数据库连接池,管理数据库连接的获取和释放,可选类型:
UNPOOLED
:每次请求时新建连接,适合简单场景(无连接池)。POOLED
:使用连接池(默认),适合高并发场景。JNDI
:从容器(如 Tomcat)的 JNDI 资源中获取数据源。(不常用)
mappers
元素
用于 注册 SQL 映射文件或 Mapper 接口。它告诉 MyBatis 框架到哪里加载 SQL 映射规则,从而将 Java 接口方法与对应的 SQL 语句绑定。
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
注册 Mapper 的四种方式
- 通过 XML 文件路径注册
使用 <mapper resource>
直接指定 Mapper XML 文件的类路径(推荐方式)。
<mapper resource="mappers/UserMapper.xml"/>
要求:XML 文件需放在类路径(resources
目录)对应的位置。
- 通过 mapper 接口全限定名注册
使用 <mapper class>
直接指定 Mapper 接口的完全限定名(需配合注解或 XML 文件)。
<mapper class="com.yigongsui.mapper.UserMapper"/>
要求:
- 如果使用 XML 文件,XML 必须与接口同名且在同一包下(如
ProductMapper.java
和ProductMapper.xml
)。 - 如果使用注解(如
@Select
),则无需 XML 文件。
- 通过包名批量注册
使用 <package>
扫描指定包下的所有 Mapper 接口或 XML 文件。
<package name="com.yigongsui.mapper"/>
要求:
- 接口与 XML 文件需同名且在同一包下(如
UserMapper.java
和UserMapper.xml
)。
包名结构必须完全相同。
- 适用于大量 Mapper 的场景,避免逐个注册。
- 通过绝对路径文件注册
使用 <mapper url>
指定 XML 文件的绝对磁盘路径(不常用,适用于特殊场景)。
<mapper url="file:///D:/project/mappers/UserMapper.xml"/>
常见问题及注意事项
- 路径问题
- XML 文件路径需与
<mapper resource>
或<package>
配置严格匹配。 - 若使用 Maven 项目,需确保 XML 文件位于
resources
目录的对应包路径下。
- XML 文件路径需与
- 同名冲突
- 同一 SQL 语句在 XML 和注解中重复定义时,XML 优先级更高。
- 包扫描限制
<package>
仅扫描接口,不自动加载 XML 文件,需保证 XML 与接口位置一致。
其余元素用到时再介绍。
Mybatis 日志输出
如果想要输出 sql 语句的日志,需要使用 Mybatis 配置文件的 settings
元素。
settings
元素
用于 调整框架的全局运行时行为。通过它,可以启用或禁用缓存、控制延迟加载、配置日志实现等,从而优化性能和定制化功能。
常用配置项及说明
完整列表参考官方文档
配置项 | 默认值 | 说明 |
---|---|---|
cacheEnabled | true | 是否开启二级缓存(Mapper 级别缓存)。 |
lazyLoadingEnabled | false | 是否启用延迟加载(关联对象的延迟初始化)。 |
aggressiveLazyLoading | false | 是否“激进”延迟加载。若为 true ,访问任意属性会立即加载所有延迟对象;若为 false ,按需加载。 |
mapUnderscoreToCamelCase | false | 是否自动将数据库字段的下划线命名(如 user_name )映射为 Java 属性的驼峰命名(如 userName )。 |
logImpl | 未指定 | 指定 MyBatis 使用的日志框架(如 SLF4J 、LOG4J2 )。 |
jdbcTypeForNull | OTHER | 当参数为 null 时,指定 JDBC 类型(某些数据库要求明确类型,可设为 NULL )。 |
useGeneratedKeys | false | 是否允许 JDBC 支持自动生成主键(需数据库驱动支持)。 |
defaultStatementTimeout | 未设置 | 设置数据库操作默认超时时间(秒)。 |
使用 name
属性指定配置项,value
属性指定属性值。
配置示例
- 开启驼峰命名自动映射
在实际开放中,由于表非常多且复杂,需要名字进行区分,各个字段名都要加上业务类型。
例如 User 用户类,它的字段名通常是 userId
、userName
、userAge
等。
在 java 中,实体类的属性命名规则是驼峰命名法(多个单词组合时,首字母小写,其它单词首字母大写),例如:userId
、userName
、updateTime
等。
在数据库中,字段名是单词_单词的形式命名,例如:user_id
、user_name
。
这样两者名对不上了,如果名字严格符合上面要求,我们就可以配置驼峰命名自动映射去匹配。
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
- 效果:无需手动编写
<resultMap>
,自动将user_name
映射到userName
属性。
- 配置延迟加载
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
- 效果:关联对象(如
Order
关联的User
)仅在访问时加载,避免不必要的查询。
- 指定日志框架
<settings>
<setting name="logImpl" value="SLF4J"/>
</settings>
- 支持的值:
SLF4J
、LOG4J2
、LOG4J
、STDOUT_LOGGING
(控制台输出)等。
我们测试配置日志使用 STDOUT_LOGGING
,SLF4J
、 LOG4J2
等需要导入依赖才能使用。
在快速开始的测试代码配置文件 environments
元素上面配置:
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
查看输出:
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1866229258.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
==> Preparing: select id, name, age, sex from user where id = ?;
==> Parameters: 1(Integer)
<== Columns: id, name, age, sex
<== Row: 1, 张三, 18, 男
<== Total: 1
User(id=1, name=张三, age=18, sex=男)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
Returned connection 1866229258 to pool.
#{} 和 ${}
它们的用途都是为 sql 语句传入参数值,下面是对比:
特性 | #{} (井号占位符) | ${} (美元占位符) |
---|---|---|
处理方式 | 预编译参数(PreparedStatement) | 直接字符串替换(字符串拼接) |
防 SQL 注入 | ✅ 自动防止 | ❌ 存在风险(需手动过滤) |
参数类型转换 | 自动匹配 JDBC 类型 | 按原样插入(可能需手动加引号) |
适用场景 | 动态值的传递(如 WHERE 条件值) | 动态SQL 片段(如表名、列名、ORDER BY) |
典型使用场景示例
- 必须用
#{}
的场景(安全优先)
<!-- 动态条件值 -->
<select id="findUser">
SELECT * FROM user
WHERE username = #{name} AND age > #{minAge}
</select>
- 必须用
${}
的场景(动态结构)
<!-- 动态表名(需确保 tableName 值可信) -->
<select id="selectFromTable">
SELECT * FROM ${tableName}
</select>
<!-- 动态排序字段 -->
<select id="getUsers">
SELECT * FROM user ORDER BY ${orderColumn}
</select>
Mybatis 数据输入
这里数据输入具体是指上层方法(例如 Service 接口方法)调用Mapper接口时,数据传入的形式。
- 简单类型:只包含一个值的数据类型
- 基本数据类型:
int
、byte
、short
、double
、…… - 基本数据类型的包装类型:
Integer
、Character
、Double
、…… - 字符串类型:
String
- 基本数据类型:
- 复杂类型:包含多个值的数据类型
- 实体类类型:
User
、Student
、…… - 集合类型:
List
、Set
、Map
、…… - 数组类型:
int[]
、String[]、…… - 复合类型:
List<User>
、实体类中包含集合……
- 实体类类型:
单个简单类型参数
我们快速开始的代码就是单个简单类型参数(Integer id
)
<select id="getUserById" resultType="com.yigongsui.pojo.User">
select id, name, age, sex from user where id = #{id};
</select>
单个简单类型参数,在 #{}
中可以随意命名,但是没有必要。通常还是使用和接口方法参数同名。
单个复杂类型参数
插入一条数据,参数就是实体类 User
。
接口:
int addUser(User user);
mapper.xml
:
<insert id="addUser">
insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>
测试:
User user = new User();
user.setName("田七");
user.setAge(44);
user.setSex("男");
int result = mapper.addUser(user);
if (result == 1) {
System.out.println("插入成功");
}
session.commit();
单个复杂类型参数,在 #{}
中必须为实体类的属性名。
Mybatis 会根据 #{}
中传入的数据,加工成 getXxx()
方法,通过反射在实体类对象中调用这个方法,从而获取到对应的数据。填充到 #{}
解析后的问号占位符这个位置。
因为默认是开启事务的,所以插入语句需要提交事务。
多个简单类型参数
场景:根据用户的姓名,年龄查找用户
接口:
User getUserByNameAndAge(String name, int age);
mapper.xml
<select id="getUserByNameAndAge" resultType="com.yigongsui.pojo.User">
select * from user where name = #{name} and age = #{6}
</select>
#{}
不能任意命名,也不能单纯得使用参数名,而是必要要在接口方法的参数上使用 @Param
注解指定名称
User getUserByNameAndAge(@Param("name") String name, @Param("age") int age);
两个名称对应。
map 类型参数
场景:插入数据,参数为 map 类型
接口:
int addUserMap(Map<String, Object> map);
mapper.xml
<insert id="addUserMap">
insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>
测试
HashMap<String, Object> map = new HashMap<>();
map.put("name","田七");
map.put("age",77);
map.put("sex","男");
int result = mapper.addUserMap(map);
if (result > 0) {
System.out.println("插入成功");
}
session.commit();
#{}
为 map 的 key 名字
Mybatis 数据输出
输出概述
数据输出总体上有两种形式:
- 增删改操作返回的受影响行数:直接使用
int
或long
类型接收即可。 - 查询操作的查询结果,可能为各种类型。
我们需要指定查询的输出数据类型。
并且插入场景下,实现主键数据回显示。
对于 select 语句,需要使用 resultType
指明返回值类型,其它 dml 语句(insert、update、delete)不需要指定 resultType
,因为都是 int
类型。
resultType 属性
- 值:类的全限定名或者别名。例如
Integer
类型的全限定名是java.lang.Integer
,Mybatis 设置了很多别名int Integer integer Int INT INTEGER
。基本数据类型的别名是名称前加_
,例如int
类型的别名是_int
。 - 作用:指定 SQL 返回结果的映射类型,可以是简单类型、POJO 对象或 Map。
- 使用场景
- 基本类型:返回单个字段值(如
int
,String
)。 - POJO 对象:返回结果自动映射到对象的属性(字段名与属性名一致)。
- Map:以键值对形式返回结果(键为列名,值为数据)。
- 注意事项
- 必须显式指定:
resultType
不可省略(除非使用resultMap
)。 - 自动映射规则:
- 默认按列名与属性名匹配(可开启
mapUnderscoreToCamelCase
支持驼峰转换)。 - 若列名与属性名不一致,需使用
<resultMap>
或别名。
- 默认按列名与属性名匹配(可开启
- 集合处理:
- 返回多行数据时,
resultType
仍指定单条记录的类型(MyBatis 自动包装为List<T>
)。
- 返回多行数据时,
返回单个简单类型
场景:查询数据总数
接口:
int getCount();
mapper.xml
<select id="getCount" resultType="int">
select count(*) from user;
</select>
使用 Integer
类型的别名 int
返回单个复杂类型
根据 id 查询用户,最开始的例子。
<select id="getUserById" resultType="com.yigongsui.pojo.User">
<!-- #{id}代表动态传入的参数,id为方法的形参名,并且进行赋值!后面详细讲解 -->
select id, name, age, sex from user where id = #{id};
</select>
这里 resultType
为实体类的全限定名 com.yigongsui.pojo.User
,为了简化,我们可以使用配置别名,用到配置文件的 typeAliases
元素。
typeAliases
元素
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。
typeAliases
元素在 settings
的下面,environments
的上面
配置别名有两种方式。
typeAlias
将类的全限定名与包名做映射。
<typeAliases>
<typeAlias type="com.yigongsui.pojo.User" alias="user"/>
</typeAliases>
当这样配置时,user
可以用在任何使用 com.yigongsui.pojo.User
的地方。
- 配置包名
<typeAliases>
<package name="com.yigongsui.pojo"/>
</typeAliases>
配置包名后,包名下的每个实体类都会以类名首字母小写的名字作为别名。
例如 com.yigongsui.pojo
包下的 User
实体类,它的别名是 user
。
由于这样每个实体类别名都默认为这个,如果想用其它别名,可以在类上设置注解 @Alias
指定别名。
@Data
@Alias("author")
public class User {
private Integer id;
private String name;
private Integer age;
private String sex;
}
返回 map 类型
接口
Map<String, Object> getUserMapById(Integer id);
mapper.xml
<select id="getUserMapById" resultType="map">
select * from user where id = #{id};
</select>
测试
Map<String, Object> resultMap = mapper.getUserMapById(1);
for (Map.Entry<String, Object> entry : resultMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
返回 list 类型
接口
List<User> getAll();
mapper.xml
<select id="getAll" resultType="user">
select * from user
</select>
测试
List<User> userList = mapper.getAll();
userList.forEach(System.out::println);
resultType
指定的是集合的泛型类型,而不是 list
返回主键值
自增长主键
场景:插入一条数据,获取它的主键值。
在 mapper.xml
的 <insert>
标签指定3个属性 useGeneratedKeys
、keyColumn
、keyProperty
属性 | 作用 |
---|---|
useGeneratedKeys | 是否启用数据库自动生成的主键(如自增 ID、序列等),默认为 false 。 |
keyColumn | 指定数据库表中主键列的列名(用于多主键或列名与属性名不一致的场景)。 |
keyProperty | 指定将生成的主键值赋值给 Java 对象的哪个属性(对象的字段名)。 |
<insert id="addUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>
测试
User user = new User();
user.setAge(18);
user.setName("孙八");
user.setName("男");
System.out.println(user.getId());
int rows = mapper.addUser(user);
if (rows > 0) {
System.out.println("插入成功");
}
System.out.println(user.getId());
session.commit();
可以看到,插入前 id
为 null
,插入后 id
就变成了当前的主键值。
非自增类型主键
而对于不支持自增型主键的数据库(例如 Oracle)或者字符串类型主键,则可以使用 selectKey
子元素:selectKey
元素将会首先运行,id 会被设置,然后插入语句会被调用!
使用 selectKey
帮助插入 UUID
作为字符串类型主键示例:
<insert id="addUser" parameterType="User">
<selectKey keyProperty="uId" resultType="java.lang.String"
order="BEFORE">
SELECT UUID() as id
</selectKey>
INSERT INTO user (id, name, age, sex) VALUE (#{id},#{name},#{age},#{sex});
</insert>
在上例中,我们定义了一个 addUser
的插入语句来将 User
对象插入到 user
表中。我们使用 selectKey
来查询 UUID 并设置到 uId
字段中,uId
为 String
类型。
通过 keyProperty
属性来指定查询到的 UUID 赋值给对象中的 uId
属性,而 resultType
属性指定了 UUID 的类型为 java.lang.String
。
需要注意的是,我们将 selectKey
放在了插入语句的前面,这是因为 MySQL 在 insert
语句中只支持一个 select
子句,而 selectKey
中查询 UUID 的语句就是一个 select
子句,因此我们需要将其放在前面。
最后,在将 User
对象插入到 user
表中时,我们直接使用对象中的 id
属性来插入主键值。
使用这种方式,我们可以方便地插入 UUID 作为字符串类型主键。当然,还有其他插入方式可以使用,如使用 Java 代码生成 UUID 并在类中显式设置值等。需要根据具体应用场景和需求选择合适的插入方式。
实体类属性和数据库字段映射
在实际开发中,经常会遇到实体类名和字段名不匹配的情况。
有3种解决方案:
- sql 语句使用别名:
select user_id as userID,user_name as userName from user;
- 开启驼峰命名规则
在 Mybatis 配置文件 settings
元素配置驼峰命名,上面展示过了。
这两种方法均使用 resultType
属性自动配置,适用于单表情况。
对于多表情况,最好使用第3种方法:
- 使用
resultMap
自定义映射
resultMap
resultMap
是 MyBatis 中用于自定义结果集映射的核心属性,适用于以下场景:
- 字段名与属性名不一致:数据库列名与 Java 对象属性名不匹配时。
- 复杂对象映射:如关联对象(一对一)、集合(一对多)、嵌套查询等。
- 需要显式控制映射规则:例如指定主键列、类型转换器或级联映射。
resultMap
的核心子标签
子标签 | 作用 |
---|---|
<id> | 映射主键字段,优化性能(标识对象的唯一性)。 |
<result> | 映射普通字段,指定列名与属性名的对应关系。 |
<association> | 映射 一对一 关联对象(如 User 关联 Department )。 |
<collection> | 映射 一对多 集合对象(如 User 包含多个 Order )。 |
<constructor> | 通过构造方法注入结果(替代 setter)。 |
resultMap
vs resultType
对比项 | resultMap | resultType |
---|---|---|
灵活性 | 高(支持复杂映射) | 低(仅支持简单自动映射) |
配置方式 | 需显式定义 <resultMap> | 直接指定 Java 类或别名 |
适用场景 | 字段名不一致、关联对象、集合、嵌套查询 | 字段名与属性名一致的简单映射 |
resultMap
示例
<select id="getAll" resultMap="myMap">
select * from user
</select>
<!-- id的值与select标签的resultMap的值对应 -->
<resultMap id="myMap" type="user">
<!-- id配置主键 -->
<id column="user_id" property="userId"/>
<!-- result配置其它字段 -->
<result column="user_name" property="userName"/>
<result column="user_age" property="userAge"/>
<result column="user_sex" property="userSex"/>
</resultMap>
高级特性
继承其他 resultMap
通过 extends
属性复用已有的映射配置:
<resultMap id="baseUserMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>
<resultMap id="userWithDeptMap" extends="baseUserMap" type="User">
<association property="dept" resultMap="deptResultMap"/>
</resultMap>
自动映射
在 resultMap
中开启 autoMapping="true"
,自动映射未显式配置的字段:
<resultMap id="userResultMap" type="User" autoMapping="true">
<id property="id" column="id"/>
<!-- 其他字段自动映射 -->
</resultMap>
类型处理器
通过 typeHandler
指定自定义类型转换器:
<result property="createTime" column="create_time"
typeHandler="org.apache.ibatis.type.DateTypeHandler"/>
Mybatis 多表映射
多表映射指通过 MyBatis 实现关系型数据库中多个关联表的数据映射,主要处理三种关系:
- 一对一(1:1)
- 一对多(1:N)
- 多对多(M:N)
MyBatis 处理多表关联的两种核心方法:
- 联表查询(JOIN):通过一条 SQL 查询多表数据,映射到嵌套对象。
- 嵌套查询(分步查询):先查主表数据,再根据主表结果查询关联表数据(支持延迟加载)。
一对一
一只猫对应一个用户。
环境准备
数据库,用户表,猫表
CREATE TABLE `user` (
`user_id` int AUTO_INCREMENT,
`user_name` varchar(10),
PRIMARY KEY (`user_id`)
);
CREATE TABLE `cat` (
`cat_id` int AUTO_INCREMENT,
`cat_name` varchar(10),
`user_id` int
PRIMARY KEY (`cat_id`)
);
INSERT INTO `user` (`user_name`) VALUES ('张三');
INSERT INTO `user` (`user_name`) VALUES ('李四');
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小花',1);
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小红',2);
用户表实体类 User.java
@Data
public class User {
private Integer userId;
private String userName;
}
猫实体类
@Data
public class Cat {
private Integer catId;
private String catName;
private Integer userId;
// 一对一
private User user;
}
环境准备完成。
场景:根据 id 查询猫及所属用户的信息。
联表查询
联表查询本质是1条 sql 语句,例如:
select *
from cat c
left join user u on c.cat_id = u.user_id
where c.cat_id = 1
下面使用代码实现:
接口:
Cat getCatById(int id);
mapper.xml
:
<resultMap id="catMap" type="com.yigongsui.pojo.Cat">
<id property="catId" column="cat_id" />
<result property="catName" column="cat_name" />
<result property="userId" column="user_id" />
<!-- 一对一配置 -->
<association property="user" javaType="com.yigongsui.pojo.User">
<id property="userId" column="user_id" />
<result property="userName" column="user_name" />
</association>
</resultMap>
<select id="getCatById" resultMap="catMap">
select *
from cat c
left join user u on c.cat_id = u.user_id
where c.cat_id = #{id}
</select>
使用 resultMap
配置多表映射,使用 association
给对象属性赋值,配置一对一映射,javaType
是类的全限定名。
嵌套查询(分步查询)
本质是2条 sql 语句,先根据 id 查询猫,获取用户的 id,再根据用户 id 查询用户信息。
select *
from user u
where u.user_id = (
select c.user_id
from cat c
where c.cat_id = 1
);
使用代码实现:
mapper.xml
:
<resultMap id="catMap" type="com.yigongsui.pojo.Cat">
<id property="catId" column="cat_id" />
<result property="catName" column="cat_name" />
<result property="userId" column="user_id" />
<association property="user" column="user_id" select="getUserById" />
</resultMap>
<!-- 主查询 --
<select id="getCatById" resultMap="catMap">
select * from cat c where c.cat_id = #{catId};
</select>
<!-- 关联查询 -->
<select id="getUserById" resultType="com.yigongsui.pojo.User">
select * from user u where u.user_id = #{userId}
</select>
需要在 mybatis 配置文件开启驼峰命名,否则 user 字段无法映射
测试结果
==> Preparing: select * from cat c where c.cat_id = ?;
==> Parameters: 1(Integer)
<== Columns: cat_id, cat_name, user_id
<== Row: 1, 小花, 1
====> Preparing: select * from user u where u.user_id = ?
====> Parameters: 1(Integer)
<==== Columns: user_id, user_name
<==== Row: 1, 张三
<==== Total: 1
<== Total: 1
Cat(catId=1, catName=小花, userId=1, user=User(userId=1, userName=张三))
一对多和多对一
场景:一个用户拥有多只猫
数据库添加两条数据
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小黄',1);
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小绿',1);
用户实体类新增属性
@Data
public class User {
private Integer userId;
private String userName;
private List<Cat> catList;
}
联表查询
sql 语句
select *
from user u
join cat c on u.user_id = c.user_id
where u.user_id = 1
代码实现:
接口:
User getUserById(int id);
mapper.xml
:
<resultMap id="userMap" type="com.yigongsui.pojo.User">
<id property="userId" column="user_id" />
<result property="userName" column="user_name" />
<collection property="catList" ofType="com.yigongsui.pojo.Cat">
<id property="catId" column="cat_id" />
<result property="catName" column="cat_name" />
</collection>
</resultMap>
<select id="getUserById" resultMap="userMap">
select *
from user u
join cat c on u.user_id = c.user_id
where u.user_id = #{userId}
</select>
使用 collection
配置集合,ofType
为集合的泛型
结果:
==> Preparing: select * from user u join cat c on u.user_id = c.user_id where u.user_id = ?
==> Parameters: 1(Integer)
<== Columns: user_id, user_name, cat_id, cat_name, user_id
<== Row: 1, 张三, 1, 小花, 1
<== Row: 1, 张三, 3, 小黄, 1
<== Row: 1, 张三, 4, 小绿, 1
<== Total: 3
User(userId=1, userName=张三, catList=[Cat(catId=1, catName=小花, userId=null, user=null), Cat(catId=3, catName=小黄, userId=null, user=null), Cat(catId=4, catName=小绿, userId=null, user=null)])
嵌套查询
先根据 id 查询用户信息,再根据用户 id 获取所有猫的信息。
<resultMap id="userMap" type="com.yigongsui.pojo.User">
<id property="userId" column="user_id" />
<result property="userName" column="user_name" />
<collection property="catList" column="user_id" select="getCatByUserId" />
</resultMap>
<!-- 主查询 -->
<select id="getUserById" resultMap="userMap">
select * from user u where u.user_id = #{userId};
</select>
<!-- 嵌套查询 -->
<select id="getCatByUserId" resultType="com.yigongsui.pojo.Cat">
select * from cat c where c.user_id = #{userId}
</select>
结果:
==> Preparing: select * from user u where u.user_id = ?;
==> Parameters: 1(Integer)
<== Columns: user_id, user_name
<== Row: 1, 张三
====> Preparing: select * from cat c where c.user_id = ?
====> Parameters: 1(Integer)
<==== Columns: cat_id, cat_name, user_id
<==== Row: 1, 小花, 1
<==== Row: 3, 小黄, 1
<==== Row: 4, 小绿, 1
<==== Total: 3
<== Total: 1
User(userId=1, userName=张三, catList=[Cat(catId=1, catName=小花, userId=1, user=null), Cat(catId=3, catName=小黄, userId=1, user=null), Cat(catId=4, catName=小绿, userId=1, user=null)])
多对多
多对多一般发生在3个表之间,步骤同一对多。
多表映射优化
在 settings
配置中有一个 autoMappingBehavior
可以处理多表复杂映射。
setting属性 | 属性含义 | 可选值 | 默认值 |
---|---|---|---|
autoMappingBehavior | 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 | NONE , PARTIAL , FULL | PARTIAL |
我们可以将 autoMappingBehavior
设置为 full
,进行多表 resultMap
映射的时候,可以省略符合列和属性命名映射规则(列名 = 属性名,或者开启驼峰映射也可以自定映射)的 result 标签。
总结
关联关系 | 配置项关键词 | 所在配置文件和具体位置 |
---|---|---|
对一 | association 标签/javaType 属性/property 属性 | Mapper 配置文件中的resultMap 标签内 |
对多 | collection 标签/ofType 属性/property 属性 | Mapper 配置文件中的resultMap 标签内 |
Mybatis 动态 sql
MyBatis 动态 SQL 允许根据不同条件动态生成 SQL 语句,常用于处理复杂的查询、更新或插入逻辑。
核心标签
标签 | 作用 |
---|---|
<if> | 条件判断,根据表达式结果决定是否包含 SQL 片段。 |
<choose>/<when>/<otherwise> | 多分支选择(类似 Java 的 switch-case )。 |
<trim> | 智能去除 SQL 片段中的多余前缀/后缀(如 AND 、OR 、逗号)。 |
<where> | 自动处理 WHERE 子句,避免条件为空时生成无效 WHERE 或多余的 AND/OR 。 |
<set> | 自动处理 UPDATE 语句中的 SET 子句,去除多余的逗号。 |
<foreach> | 遍历集合参数,生成 IN 查询或批量操作的 SQL 片段。 |
<bind> | 定义变量,简化复杂表达式或重复逻辑。 |
where 和 if 标签
两个标签通常要搭配使用,根据条件动态查询数据。
场景:根据用户和姓名和年龄查询数据,如果条件不存在就查询所有数据。
实体类:
@Data
public class User {
private Integer userId;
private String userName;
private Integer userAge;
}
接口:
List<User> getUserByNameAndAge(@Param("name") String name, @Param("age") Integer age);
mapper.xml
:
<select id="getUserByNameAndAge" resultType="com.yigongsui.pojo.User">
select * from user
<where>
<if test="name != null and name != ''">
user_name = #{name}
</if>
<if test="age != null">
and user_age = #{age}
</if>
</where>
</select>
需要开启驼峰自动映射。
if
标签的 test
属性用于判断查询是否存在,如果满足条件才会拼接语句。
拼接的语句从第2条开始,要在前面加上 and 或 or 关键字进行拼接(也可以在第1条语句前也加上),即时第1条语句不满足条件,Mybatis 会自动省略多余的 and、or 避免 sql 语句出错。
如果条件均不满足,where 也会省略。
set 标签
set
标签用于更新语句。
接口:
int updateUserById(User user);
mapper.xml
:
<update id="updateUserById">
update user
<set>
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="userAge != null">
user_age = #{userAge}
</if>
</set>
where user_id = #{userId}
</update>
使用 set
标签时,if
下语句需要加上 ,
,set
标签会自动删除不需要的 ,
(最后一条语句可加可不加)。
至少要有一个条件满足,否则会报错,毕竟修改至少要修改一条字段。
trim 标签
作用:自定义去除 SQL 片段的前缀/后缀。
属性:
prefix
:添加前缀。suffix
:添加后缀。prefixOverrides
:去除前缀。suffixOverrides
:去除后缀。
场景:替代 where
标签。
choose/when/otherwise 标签
作用:多条件分支选择,类似于 java 中的 switch-case
。
场景:根据优先级查询用户。
foreach 标签
作用:遍历集合参数(如 List
、数组、Map
)。
属性:
collection
:集合参数名。item
:遍历时的元素别名。index
:遍历时的索引别名。open
/close
:循环体开始/结束时的字符。separator
:元素间的分隔符。
示例 1:IN
查询:
需要设置 @Param
属性名。
List<User> getUserByIds(@Param("ids") List<Integer> ids);
<select id="getUserByIds" resultType="User">
SELECT * FROM user
WHERE user_id IN
<foreach collection="ids" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
item
指定的属性名与 #{}
内名相同。
示例 2:批量插入
<insert id="batchInsertUsers">
INSERT INTO user (user_name, user_age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.userName}, #{user.userAge})
</foreach>
</insert>
sql 片段
当一条 sql 语句出现多次时,可以将这条 sql 语句提取出来进行复用。
提取 sql:
<!-- 使用sql标签抽取重复出现的SQL片段 -->
<sql id="mySelectSql">
select user_id,user_name,user_age from user
</sql>
引用 sql:
<!-- 使用include标签引用声明的SQL片段 -->
<include refid="mySelectSql"/>
Mybatis 插件机制
原理
MyBatis 的插件机制基于 动态代理 和 责任链模式,允许开发者拦截以下四大核心接口的方法:
Executor
:执行 SQL 的顶层对象,负责增删改查操作。StatementHandler
:处理 SQL 预编译和结果集。ParameterHandler
:处理 SQL 参数映射。ResultSetHandler
:处理结果集映射。
插件工作流程:
- 定义拦截器:实现
Interceptor
接口,指定拦截的方法。 - 创建代理对象:MyBatis 通过动态代理生成目标对象的代理。
- 责任链调用:多个插件按配置顺序形成拦截链,依次执行前置处理、原方法、后置处理。
实现步骤
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 拦截逻辑
return invocation.proceed();
}
}
在MyBatis配置文件中注册:
<plugins>
<plugin interceptor="com.example.MyPlugin"/>
</plugins>
分页插件 PageHelper
PageHelper 是基于 MyBatis 插件机制的分页工具,通过拦截 Executor
的查询方法,自动添加分页 SQL(如 MySQL 的 LIMIT
),并计算总记录数。
核心特性:
- 零侵入:无需修改原有 SQL,通过简单 API 触发分页。
- 支持多种数据库:自动识别数据库类型,生成适配的分页语句。
- 性能优化:通过
COUNT
查询获取总记录数,避免全表扫描。
使用步骤
- 添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
- 配置插件
在配置文件配置:
<configuration>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 配置数据库方言(可选,PageHelper 会自动检测) -->
<property name="helperDialect" value="mysql"/>
<!-- 合理化分页参数(页码超出范围时自动修正) -->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
</configuration>
- 使用
// 1. 开启分页:pageNum - 页码,pageSize - 每页数量
PageHelper.startPage(1, 10);
// 2. 执行查询(紧跟 startPage 后的第一个查询会自动分页)
List<User> users = userMapper.selectAll();
// 3. 封装分页结果
PageInfo<User> pageInfo = new PageInfo<>(users);
// 输出分页信息
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("当前页数据:" + pageInfo.getList());
核心原理
-
拦截
Executor
的查询方法:PageInterceptor
拦截Executor
的query()
方法,判断是否需要分页。 -
改写原始 SQL:
- 添加分页语句(如 MySQL 的
LIMIT
)。 - 执行
COUNT
查询获取总记录数。
- 添加分页语句(如 MySQL 的
-
封装分页结果:
将分页后的数据与总记录数封装到
Page
或PageInfo
对象中。
高级功能与配置
分页参数传递
-
直接传递参数:
PageHelper.startPage(pageNum, pageSize);
-
通过方法参数传递(需配置
supportMethodsArguments=true
):public PageInfo<User> findUsers(@Param("pageNum") int pageNum, @Param("pageSize") int pageSize) { // 无需手动调用 startPage return PageHelper.startPage(pageNum, pageSize) .doSelectPageInfo(() -> userMapper.selectAll()); }
排序支持
PageHelper.startPage(1, 10, "name desc, age asc");
List<User> users = userMapper.selectAll();
物理分页 vs 内存分页
-
物理分页:通过 SQL 分页(默认),性能高。
-
内存分页:先查全部数据,再在内存中分页(不推荐):
PageHelper.startPage(1, 10, false);
与其他分页方案对比
方案 | 优点 | 缺点 |
---|---|---|
PageHelper | 无侵入、支持多数据库、使用简单 | 依赖 MyBatis 插件机制 |
手动 LIMIT | 完全控制 SQL | 需手动编写分页逻辑和 COUNT 查询 |
Spring Data JPA | 与 Spring 生态集成好 | 仅适用于 JPA,灵活性较低 |
Mybatis 的缓存机制
MyBatis 提供了一套灵活的缓存机制,用于减少数据库访问次数,提升查询性能。其缓存分为 一级缓存 和 二级缓存,两者的作用范围、配置方式及生命周期不同。以下是详细解析:
一级缓存(本地缓存)
作用域
- SqlSession 级别:同一个
SqlSession
内有效(默认开启)。 - 生命周期:与
SqlSession
绑定,会话关闭或执行commit()
、rollback()
、clearCache()
时缓存失效。
工作机制
- 缓存范围:
同一SqlSession
多次执行相同的查询(完全相同的 SQL 和参数),会直接返回缓存的结果,不访问数据库。 - 失效条件:
- 执行
INSERT
/UPDATE
/DELETE
操作(无论是否修改缓存数据)。 - 手动调用
sqlSession.clearCache()
。 - 配置
flushCache="true"
的查询(如<select flushCache="true">
)。
- 执行
示例验证
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,访问数据库
User user1 = mapper.findUserById(1);
// 第二次查询,命中一级缓存
User user2 = mapper.findUserById(1);
System.out.println(user1 == user2); // true(同一对象引用)
session.commit(); // 提交后缓存失效
优缺点
优点 | 缺点 |
---|---|
自动开启,无需配置。 | 作用域小,仅限同一 SqlSession。 |
减少重复查询开销。 | 多个 SqlSession 无法共享缓存数据。 |
二级缓存(全局缓存)
作用域
- Mapper 级别:跨
SqlSession
共享(需手动开启)。 - 生命周期:与应用程序的生命周期一致,除非显式清除或配置过期时间。
工作机制
-
缓存共享:
不同
SqlSession
访问同一 Mapper 的查询,可以共享缓存数据。 -
存储方式:
默认使用内存存储(可集成 Redis、Ehcache 等第三方缓存)。
配置步骤
全局启用二级缓存
在 mybatis-config.xml
中配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
Mapper 文件中声明缓存
在对应的 Mapper.xml
中添加 <cache>
标签:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU" <!-- 淘汰策略(默认LRU) -->
flushInterval="60000" <!-- 自动刷新间隔(毫秒) -->
size="1024" <!-- 缓存最大对象数 -->
readOnly="true"/> <!-- 是否只读(默认false) -->
<select id="findUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
POJO 类实现序列化
若使用默认缓存,需确保返回的对象实现 Serializable
接口:
public class User implements Serializable {
private Integer id;
private String name;
// Getter & Setter
}
缓存淘汰策略(eviction
)
策略 | 说明 |
---|---|
LRU | 最近最少使用(默认),移除最长时间未被使用的对象。 |
FIFO | 先进先出,按对象进入缓存的顺序移除。 |
SOFT | 软引用,基于垃圾回收器状态和软引用规则移除对象。 |
WEAK | 弱引用,更积极地移除对象。 |
缓存失效条件
- 执行
<insert>
、<update>
、<delete>
操作时,同一 Mapper 的缓存自动清空。 - 手动调用
sqlSession.clearCache()
或配置flushCache="true"
。
示例验证
// 第一个 SqlSession
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findUserById(1); // 查询数据库
session1.close(); // 提交并关闭,数据存入二级缓存
// 第二个 SqlSession
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findUserById(1); // 命中二级缓存
session2.close();
System.out.println(user1 == user2); // false(不同会话,但数据相同)
优缺点
优点 | 缺点 |
---|---|
跨会话共享,减少数据库压力。 | 配置复杂,需处理序列化和并发问题。 |
支持第三方缓存扩展。 | 数据实时性差,可能读到旧数据。 |
缓存使用注意事项
避免脏读
-
场景:多个 Mapper 操作同一张表时,二级缓存可能未及时更新。
-
解决:在关联的 Mapper 配置中引用同一缓存命名空间:
<cache-ref namespace="com.example.mapper.UserMapper"/>
细粒度缓存控制
-
关闭单条语句的缓存:
<select id="findUser" resultType="User" useCache="false"> SELECT * FROM user </select>
-
强制刷新缓存:
<select id="findUser" resultType="User" flushCache="true"> SELECT * FROM user </select>
集成第三方缓存
以 Ehcache 为例:
-
添加依赖:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.2</version> </dependency>
-
配置 Mapper 缓存:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
缓存的适用场景
场景 | 推荐缓存 | 说明 |
---|---|---|
频繁读取,极少修改 | 二级缓存 | 如配置表、商品分类等静态数据。 |
会话内重复查询 | 一级缓存 | 同一事务中多次查询相同数据。 |
实时性要求高 | 禁用缓存 | 如订单状态、库存数量等需实时获取的数据。 |
总结
- 一级缓存:默认开启,会话级别,适合短周期、高重复查询。
- 二级缓存:需手动配置,全局共享,适合读多写少的静态数据。
- 缓存选择:根据业务场景权衡性能与数据一致性,必要时结合第三方缓存(如 Redis)提升扩展性。
- 避坑指南:注意事务提交、缓存刷新策略,避免脏读和内存溢出。
通过合理配置 MyBatis 缓存,可显著减少数据库负载,但在高并发或分布式场景中,建议结合分布式缓存(如 Redis)替代默认的本地二级缓存。