MyBatis核心技术全解
1.1、如何快速上手MyBatis?
要快速上手 MyBatis,建议按照以下步骤循序渐进,从核心概念到实际操作逐步掌握:
1. 理解 MyBatis 核心作用
MyBatis 是一款优秀的 持久层框架,主要解决:
- SQL 与 Java 代码分离,便于维护
- 自动完成 Java 对象与 SQL 结果集的映射(ORM)
- 简化 JDBC 操作的模板代码(如连接管理、Statement 创建等)
2. 环境搭建(最快方式)
推荐使用 Maven/Gradle 引入依赖
Maven 依赖(pom.xml):
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.13</version> <!-- 最新稳定版 -->
</dependency>
<!-- 数据库驱动(以MySQL为例) -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version>
</dependency>
3. 核心配置文件(mybatis-config.xml)
这是 MyBatis 的全局配置文件,主要配置:
- 数据库连接信息(数据源)
- 映射文件路径(SQL 语句所在位置)
示例:
<?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 default="development"><environment id="development"><transactionManager type="JDBC"/> <!-- 事务管理 --><dataSource type="POOLED"> <!-- 连接池 --><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/test"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><!-- 映射文件路径(SQL语句在这里) --><mappers><mapper resource="mapper/UserMapper.xml"/></mappers>
</configuration>
4. 定义实体类(POJO)
对应数据库表的 Java 类,属性名建议与表字段名一致。
示例(User.java):
public class User {private Integer id;private String name;private Integer age;// 必须有 getter/setter 和无参构造方法public Integer getId() { return id; }public void setId(Integer id) { this.id = id; }// ... 其他 getter/setter
}
5. 编写映射文件(SQL 语句)
这是 MyBatis 的核心,存放所有 SQL 语句,并定义 Java 对象与结果集的映射关系。
示例(UserMapper.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 通常对应接口全类名 -->
<mapper namespace="com.example.mapper.UserMapper"><!-- 查询:根据ID查用户 --><select id="getUserById" parameterType="int" resultType="com.example.pojo.User">SELECT id, name, age FROM user WHERE id = #{id}</select><!-- 新增用户 --><insert id="addUser" parameterType="com.example.pojo.User">INSERT INTO user (name, age) VALUES (#{name}, #{age})</insert>
</mapper>
6. 编写 Mapper 接口(可选但推荐)
通过接口调用 SQL,避免硬编码 SQL 的 ID。
示例(UserMapper.java):
public interface UserMapper {// 方法名与映射文件中 SQL 的 id 一致User getUserById(int id);void addUser(User user);
}
7. 核心 API 操作数据库
通过 SqlSessionFactory 构建 SqlSession,执行 SQL。
示例(测试代码):
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;public class MyBatisDemo {public static void main(String[] args) throws IOException {// 1. 加载配置文件InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");// 2. 创建 SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// 3. 获取 SqlSession(类似 JDBC 的 Connection)try (SqlSession session = sqlSessionFactory.openSession()) {// 4. 执行 SQL(两种方式)// 方式1:通过接口调用(推荐)UserMapper userMapper = session.getMapper(UserMapper.class);User user = userMapper.getUserById(1);System.out.println(user.getName());// 方式2:通过 SQL 的 id 调用(不推荐,硬编码)// User user = session.selectOne("com.example.mapper.UserMapper.getUserById", 1);// 增删改需要提交事务// session.commit();}}
}
8. 快速掌握的关键技巧
-
牢记核心流程:
配置文件 → 映射文件 → SqlSessionFactory → SqlSession → 执行 SQL -
重点理解 #{} 与 ${} 的区别:
#{}
:参数预编译(防止 SQL 注入,推荐)${}
:字符串拼接(有注入风险,用于表名/排序字段等动态场景)
-
学会调试 SQL:
在配置文件中开启日志,查看 MyBatis 生成的 SQL:<settings><setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 控制台打印日志 --> </settings>
-
使用注解简化(适合简单 SQL):
无需 XML,直接在接口方法上写 SQL:public interface UserMapper {@Select("SELECT * FROM user WHERE id = #{id}")User getUserById(int id); }
9. 推荐学习资源
- 官方文档:MyBatis 中文文档(最权威)
- 实战练习:结合 Spring Boot + MyBatis 快速开发(整合后更简单)
按照以上步骤,1-2 小时即可完成第一个 MyBatis 程序,后续再逐步深入动态 SQL、关联查询等高级特性。
1.2、MyBatis的主要特征有哪些?
MyBatis 作为一款优秀的持久层框架,具有以下主要特征:
-
SQL 与代码分离
允许将 SQL 语句写在 XML 映射文件中,与 Java 代码完全分离,便于 SQL 优化和维护,尤其适合复杂查询场景。 -
灵活的 SQL 控制
相比全自动 ORM 框架(如 Hibernate),MyBatis 不屏蔽 SQL,开发者可完全掌控 SQL 语句,能根据需求编写高效的原生 SQL。 -
强大的参数映射
支持多种参数传递方式(单个参数、对象、Map 等),通过#{}
语法实现参数预编译(防止 SQL 注入),也支持${}
进行字符串拼接。 -
自动结果映射
能自动将 SQL 执行结果映射为 Java 对象(POJO),支持通过resultType
或resultMap
定义映射规则,解决字段名与属性名不一致问题。 -
动态 SQL
提供<if>
、<where>
、<foreach>
等标签,可根据条件动态生成 SQL 语句,避免手动拼接 SQL 的繁琐和错误。 -
支持多种数据库
适配主流关系型数据库(MySQL、Oracle、SQL Server 等),通过更换驱动和调整 SQL 即可实现多数据库兼容。 -
缓存机制
内置一级缓存(SqlSession 级别)和二级缓存(Mapper 级别),减少数据库访问次数,提升性能。 -
接口式编程
支持通过 Mapper 接口调用 SQL,无需实现类,框架自动生成代理对象,简化代码结构。 -
事务管理
集成 JDBC 事务管理,也可与 Spring 等框架整合,支持声明式事务。 -
低侵入性
对现有代码影响小,核心类(如 SqlSession、Mapper)设计简洁,易于上手和扩展。
这些特征使 MyBatis 在保持灵活性的同时,兼顾了开发效率,尤其适合需要精细控制 SQL 的业务场景,因此成为企业级开发的主流选择之一。
1、简单介绍ORM模型。
ORM(Object-Relational Mapping,对象关系映射)是一种编程技术,用于解决面向对象编程(OOP)与关系型数据库(RDB)之间的数据交互问题。
其核心思想是:
- 将数据库中的表映射为程序中的类
- 将表中的行(记录) 映射为类的对象
- 将表中的列(字段) 映射为类的属性
- 将表之间的关系(如一对一、一对多)映射为对象之间的关联关系
通过 ORM,开发者可以直接用面向对象的语法(如调用对象的方法、操作属性)来操作数据库,而无需编写原生 SQL 语句。
举例说明:
不用 ORM 时,查询用户需要写 SQL:
// JDBC 方式(伪代码)
String sql = "SELECT id, name FROM user WHERE id = 1";
ResultSet rs = statement.executeQuery(sql);
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
用 ORM 后,只需:
// ORM 方式(伪代码)
User user = userRepository.findById(1); // 直接调用对象方法
ORM 的优势:
- 简化代码:无需编写重复的 SQL 和数据映射逻辑
- 降低学习成本:用熟悉的面向对象语法操作数据库
- 提高可维护性:SQL 逻辑与业务代码解耦
- 数据库无关性:更换数据库时无需大量修改代码
常见 ORM 框架:
- Java:MyBatis、Hibernate、Spring Data JPA
- Python:SQLAlchemy、Django ORM
- .NET:Entity Framework
ORM 并非完美,对于复杂 SQL 场景,过度依赖 ORM 可能导致性能问题,此时混合使用原生 SQL 会更合适(如 MyBatis 的设计思路)。
2、MyBatis与Hibernate的比较。
MyBatis 和 Hibernate 都是 Java 领域主流的持久层框架,但设计理念和适用场景有显著差异,以下是两者的核心对比:
1. 设计理念
-
MyBatis:
属于「半自动化 ORM」,不屏蔽 SQL,强调开发者对 SQL 的控制能力,需手动编写 SQL 语句(或通过注解简化),专注于 SQL 映射。
核心思想:「SQL 与代码分离,灵活控制 SQL」。 -
Hibernate:
属于「全自动化 ORM」,完全屏蔽 SQL,通过面向对象的方式操作数据库(如save()
、get()
),由框架自动生成 SQL。
核心思想:「让开发者以 OOP 思维操作数据库,无需关注 SQL」。
2. 核心差异
对比维度 | MyBatis | Hibernate |
---|---|---|
SQL 控制 | 需手动编写 SQL(XML 或注解),灵活度高 | 自动生成 SQL,几乎无需手写 |
学习成本 | 低(核心是 SQL 映射,上手快) | 高(需掌握 HQL、缓存、关联映射等概念) |
性能优化 | 可直接优化 SQL,调优简单直观 | 优化复杂,需理解其生成 SQL 的逻辑 |
数据库兼容性 | 较差(SQL 可能包含数据库特有语法) | 好(自动适配不同数据库,几乎无需修改) |
代码量 | 需编写 SQL 和映射文件,代码量略多 | 无需写 SQL,代码量少 |
适用场景 | 复杂 SQL 场景、性能要求高的系统 | 简单 CRUD 操作、快速开发的项目 |
3. 典型使用场景
-
选 MyBatis 当:
- 项目需要大量复杂 SQL(如多表关联、子查询、函数调用)
- 对 SQL 性能有极致要求(如电商订单、金融交易系统)
- 团队更熟悉 SQL 优化,希望掌控底层查询逻辑
-
选 Hibernate 当:
- 项目以简单 CRUD 为主,SQL 逻辑不复杂
- 需要快速开发原型或迭代(如内部管理系统)
- 可能需要切换数据库(如从 MySQL 迁移到 Oracle)
4. 总结
- MyBatis 是「SQL 友好型」框架,适合需要精细控制 SQL 的场景,灵活但需手动编写 SQL。
- Hibernate 是「对象友好型」框架,适合快速开发和数据库无关的场景,但复杂 SQL 场景下不够灵活。
实际开发中,MyBatis 因对 SQL 的可控性更强,在企业级系统中应用更广泛;而 Hibernate 更适合快速迭代的中小型项目。此外,Spring Data JPA(基于 Hibernate)近年也逐渐流行,平衡了开发效率和一定的灵活性。
3、入门案例
下面我将提供一个 MyBatis 入门案例,通过具体代码演示如何使用 MyBatis 完成基本的数据库操作。
我们将实现一个简单的用户管理功能,包括查询、新增用户操作。
mybatis入门案例.zip
案例说明
这个入门案例包含了使用 MyBatis 的完整流程:
-
环境配置:通过 Maven 引入 MyBatis 和 MySQL 驱动依赖
-
数据库准备:创建用户表并插入测试数据
-
核心配置:
mybatis-config.xml
配置数据库连接和映射文件- 配置日志输出以便调试
-
核心代码:
- 实体类
User
对应数据库表 UserMapper
接口定义操作方法UserMapper.xml
编写 SQL 语句和映射关系- 工具类
MyBatisUtils
简化 SqlSession 获取
- 实体类
-
测试:通过 JUnit 测试类验证查询和新增功能
使用步骤
- 首先创建数据库和表(运行
user.sql
脚本) - 修改
mybatis-config.xml
中的数据库连接信息(用户名和密码) - 按照 Maven 项目结构放置文件
- 依次运行测试类中的方法
通过这个案例,你可以了解 MyBatis 的基本使用流程,包括如何配置、编写映射关系以及执行数据库操作。在此基础上,你可以进一步学习 MyBatis 的动态 SQL、关联查询等高级特性。
4、MyBatis的常用配置和基本用法。
MyBatis 的核心在于配置文件和 SQL 映射,以下是其常用配置和基本用法的总结:
4.1、核心配置文件(mybatis-config.xml)
这是 MyBatis 的全局配置文件,主要配置数据库连接、映射文件路径等核心信息。
常用配置项:
<?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><!-- 1. 配置属性(可外部引入) --><properties resource="db.properties"/> <!-- 引入外部配置文件 --><!-- 2. 全局设置 --><settings><setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 日志输出 --><setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 下划线转驼峰命名 --><setting name="cacheEnabled" value="true"/> <!-- 开启二级缓存 --></settings><!-- 3. 类型别名(简化类全限定名) --><typeAliases><typeAlias type="com.example.pojo.User" alias="User"/> <!-- 单个类 --><package name="com.example.pojo"/> <!-- 包下所有类自动取别名(类名小写) --></typeAliases><!-- 4. 环境配置(数据库连接信息) --><environments default="development"><environment id="development"><transactionManager type="JDBC"/> <!-- 事务管理(JDBC/Managed) --><dataSource type="POOLED"> <!-- 数据源(POOLED/UNPOOLED/JNDI) --><property name="driver" value="${db.driver}"/> <!-- 从properties获取 --><property name="url" value="${db.url}"/><property name="username" value="${db.username}"/><property name="password" value="${db.password}"/></dataSource></environment></environments><!-- 5. 映射文件(SQL语句所在位置) --><mappers><mapper resource="mapper/UserMapper.xml"/> <!-- 资源路径 --><mapper class="com.example.mapper.UserMapper"/> <!-- 接口类(注解方式) --><package name="com.example.mapper"/> <!-- 包下所有接口 --></mappers>
</configuration>
4.2、映射文件(XXXMapper.xml)
存放 SQL 语句,定义 Java 对象与数据库的映射关系,是 MyBatis 的核心。
基本结构与常用标签:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 对应 Mapper 接口全类名 -->
<mapper namespace="com.example.mapper.UserMapper"><!-- 1. 查询(select) --><select id="getUserById" parameterType="int" resultType="User">SELECT id, name, age, email FROM user WHERE id = #{id}</select><!-- 2. 新增(insert) --><insert id="addUser" parameterType="User">INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})<!-- 获取自增主键 --><selectKey keyProperty="id" order="AFTER" resultType="int">SELECT LAST_INSERT_ID()</selectKey></insert><!-- 3. 更新(update) --><update id="updateUser" parameterType="User">UPDATE user SET name=#{name}, age=#{age} WHERE id=#{id}</update><!-- 4. 删除(delete) --><delete id="deleteUser" parameterType="int">DELETE FROM user WHERE id = #{id}</delete><!-- 5. 结果映射(解决字段名与属性名不一致) --><resultMap id="UserMap" type="User"><id column="user_id" property="id"/> <!-- 主键映射 --><result column="user_name" property="name"/> <!-- 普通字段映射 --></resultMap><!-- 6. 动态 SQL(if/where) --><select id="getUserByCondition" parameterType="User" resultMap="UserMap">SELECT * FROM user<where><if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age > #{age}</if></where></select><!-- 7. 批量操作(foreach) --><delete id="deleteBatch">DELETE FROM user WHERE id IN<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></delete>
</mapper>
4.3、Mapper 接口(推荐方式)
通过接口调用 SQL,无需实现类,方法名与映射文件中 id
一致。
public interface UserMapper {// 查询单个用户User getUserById(int id);// 新增用户int addUser(User user);// 更新用户int updateUser(User user);// 删除用户int deleteUser(int id);// 条件查询List<User> getUserByCondition(User user);// 批量删除int deleteBatch(List<Integer> ids);
}
4.4、基本用法流程
-
获取 SqlSessionFactory
加载核心配置文件,创建会话工厂:String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
获取 SqlSession
相当于数据库连接,用于执行 SQL:// 自动提交事务(默认 false,需手动 commit()) try (SqlSession session = sqlSessionFactory.openSession(true)) {// 操作数据库 }
-
执行 SQL
通过 Mapper 接口调用方法:UserMapper mapper = session.getMapper(UserMapper.class);// 查询 User user = mapper.getUserById(1);// 新增 User newUser = new User("张三", 25, "zhangsan@example.com"); mapper.addUser(newUser);
4.5、关键注意事项
- 事务管理:增删改操作需提交事务(
session.commit()
),查询无需。 - 参数传递:
- 单个参数:直接用
#{参数名}
- 多个参数:建议用
@Param
注解命名,如List<User> getUsers(@Param("name") String name, @Param("age") int age)
- 单个参数:直接用
- 结果映射:字段名与属性名不一致时,需用
resultMap
手动映射。 - 缓存机制:一级缓存默认开启(SqlSession 级别),二级缓存需在映射文件中加
<cache/>
标签开启。
掌握以上配置和用法,即可完成 MyBatis 的大部分基础操作,复杂场景可进一步学习动态 SQL、关联查询(association
/collection
)等高级特性。
5、MyBatis增删改查标签及案例实践。
MyBatis 提供了专门的标签用于数据库的增删改查操作,分别是 <insert>
、<delete>
、<update>
和 <select>
。这些标签集中定义在映射文件(XXXMapper.xml)中,与 Mapper 接口方法对应。下面详细介绍每个标签的用法及案例:
5.1、查询标签:<select>
用于执行查询语句,是最常用的标签之一。
核心属性:
id
:与 Mapper 接口方法名一致parameterType
:传入参数类型(可选,MyBatis 可自动推断)resultType
:返回结果类型(单条记录类型,如实体类、基本类型)resultMap
:复杂结果映射(当字段名与属性名不一致时使用)
案例:
1. 根据 ID 查询单个用户
<!-- UserMapper.xml -->
<select id="getUserById" parameterType="int" resultType="com.example.pojo.User">SELECT id, name, age, email FROM user WHERE id = #{id}
</select>
对应的 Mapper 接口:
public interface UserMapper {User getUserById(int id); // 方法名与 id 一致
}
2. 查询所有用户
<select id="getAllUsers" resultType="com.example.pojo.User">SELECT id, name, age, email FROM user
</select>
对应的 Mapper 接口:
List<User> getAllUsers();
5.2、新增标签:<insert>
用于执行插入语句,支持获取自增主键。
核心属性:
id
:与 Mapper 接口方法名一致parameterType
:传入的实体类类型useGeneratedKeys
:是否使用自增主键(配合keyProperty
使用)
案例:
1. 新增用户并获取自增 ID
<!-- UserMapper.xml -->
<insert id="addUser" parameterType="com.example.pojo.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})
</insert>
useGeneratedKeys="true"
:启用自增主键keyProperty="id"
:将自增 ID 赋值给实体类的id
属性
对应的 Mapper 接口:
int addUser(User user); // 返回值为影响行数
使用示例:
User newUser = new User("张三", 25, "zhangsan@example.com");
mapper.addUser(newUser);
System.out.println("新增用户的 ID:" + newUser.getId()); // 自动获取自增 ID
5.3、更新标签:<update>
用于执行更新语句,可根据条件更新部分或全部字段。
核心属性:
id
:与 Mapper 接口方法名一致parameterType
:传入的参数类型(实体类或 Map 等)
案例:
1. 更新用户信息
<!-- UserMapper.xml -->
<update id="updateUser" parameterType="com.example.pojo.User">UPDATE user SET name = #{name}, age = #{age}, email = #{email}WHERE id = #{id}
</update>
对应的 Mapper 接口:
int updateUser(User user); // 返回值为影响行数
2. 动态更新(只更新非空字段)
<update id="updateUserSelective" parameterType="com.example.pojo.User">UPDATE user<set><if test="name != null">name = #{name},</if><if test="age != null">age = #{age},</if><if test="email != null">email = #{email}</if></set>WHERE id = #{id}
</update>
- 使用
<set>
标签自动处理逗号,避免 SQL 语法错误 <if>
标签实现动态判断,只更新非空字段
5.4、删除标签:<delete>
用于执行删除语句,支持单条删除或批量删除。
核心属性:
id
:与 Mapper 接口方法名一致parameterType
:传入的参数类型(如基本类型、List 等)
案例:
1. 根据 ID 删除单个用户
<!-- UserMapper.xml -->
<delete id="deleteUser" parameterType="int">DELETE FROM user WHERE id = #{id}
</delete>
对应的 Mapper 接口:
int deleteUser(int id); // 返回值为影响行数
2. 批量删除用户
<delete id="deleteBatch" parameterType="java.util.List">DELETE FROM user WHERE id IN<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<foreach>
标签遍历 List 集合,生成(1,2,3)
格式的条件collection="list"
:表示参数是 List 类型item="id"
:遍历的元素别名
对应的 Mapper 接口:
int deleteBatch(List<Integer> ids);
5.5、完整实践流程
- 定义实体类(User.java):
public class User {private Integer id;private String name;private Integer age;private String email;// 省略 getter、setter、无参构造
}
- 编写 Mapper 接口(UserMapper.java):
public interface UserMapper {// 查User getUserById(int id);List<User> getAllUsers();// 增int addUser(User user);// 改int updateUser(User user);int updateUserSelective(User user);// 删int deleteUser(int id);int deleteBatch(List<Integer> ids);
}
- 编写映射文件(UserMapper.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper"><!-- 查 --><select id="getUserById" parameterType="int" resultType="com.example.pojo.User">SELECT id, name, age, email FROM user WHERE id = #{id}</select><select id="getAllUsers" resultType="com.example.pojo.User">SELECT id, name, age, email FROM user</select><!-- 增 --><insert id="addUser" parameterType="com.example.pojo.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email})</insert><!-- 改 --><update id="updateUser" parameterType="com.example.pojo.User">UPDATE user SET name=#{name}, age=#{age}, email=#{email} WHERE id=#{id}</update><update id="updateUserSelective" parameterType="com.example.pojo.User">UPDATE user<set><if test="name != null">name = #{name},</if><if test="age != null">age = #{age},</if><if test="email != null">email = #{email}</if></set>WHERE id = #{id}</update><!-- 删 --><delete id="deleteUser" parameterType="int">DELETE FROM user WHERE id = #{id}</delete><delete id="deleteBatch" parameterType="java.util.List">DELETE FROM user WHERE id IN<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></delete>
</mapper>
- 测试代码:
public class Test {public static void main(String[] args) throws IOException {// 获取 SqlSessionInputStream is = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);try (SqlSession session = factory.openSession(true)) { // 自动提交事务UserMapper mapper = session.getMapper(UserMapper.class);// 测试查询User user = mapper.getUserById(1);System.out.println("查询结果:" + user);// 测试新增User newUser = new User("李四", 30, "lisi@example.com");mapper.addUser(newUser);System.out.println("新增用户 ID:" + newUser.getId());// 测试更新user.setName("张三更新");mapper.updateUser(user);// 测试删除mapper.deleteUser(2);}}
}
总结
- 增删改查标签分别对应
<insert>
、<delete>
、<update>
、<select>
,与 Mapper 接口方法一一对应。 - 标签的
id
必须与接口方法名一致,参数和返回值通过parameterType
、resultType
或resultMap
关联。 - 动态 SQL 标签(如
<if>
、<foreach>
、<set>
)可灵活处理复杂场景,减少手动拼接 SQL 的错误。
掌握这些标签的用法,就能完成 MyBatis 大部分基础数据库操作,是后续学习动态 SQL、关联查询等高级特性的基础。
2.1、MyBatis的实现机制
MyBatis 的实现机制核心是通过配置解析、动态代理、SQL 执行与结果映射,将 Java 方法调用与 SQL 语句执行关联起来,简化数据库操作。其底层工作流程可拆解为配置加载、核心组件协作、SQL 执行与结果处理三个主要阶段。
1.1、整体架构与核心组件
MyBatis 的架构分为三层,核心组件各司其职:
层级 | 核心组件 | 作用 |
---|---|---|
接口层 | SqlSession | 提供用户操作数据库的 API(如 selectOne() 、update() 等) |
核心层 | Configuration | 存储全局配置信息(数据源、映射关系、参数等),MyBatis 的“大脑” |
Executor | 执行器,负责 SQL 执行的核心逻辑(包含一级缓存、事务管理) | |
StatementHandler | 处理 JDBC 的 Statement (预处理、参数设置、执行 SQL) | |
ParameterHandler | 处理参数映射(将 Java 参数转换为 SQL 占位符的值) | |
ResultSetHandler | 处理结果集映射(将 ResultSet 转换为 Java 对象) | |
MapperProxy | 动态代理对象,实现 Mapper 接口,将方法调用转发给 Executor | |
基础层 | DataSource | 数据源,管理数据库连接(池化、连接创建) |
Transaction | 事务管理(基于 JDBC 事务或整合 Spring 事务) | |
Cache | 缓存接口(一级缓存、二级缓存的实现基础) | |
TypeHandler | 类型处理器(Java 类型与 JDBC 类型的转换,如 String ↔ VARCHAR ) |
1.2、核心实现流程(以查询为例)
以调用 UserMapper.getUserById(1)
为例,MyBatis 的执行流程如下:
1. 初始化:配置加载与 SqlSessionFactory
创建
-
加载配置文件:
启动时,SqlSessionFactoryBuilder
读取mybatis-config.xml
和映射文件(如UserMapper.xml
),解析后将配置信息(数据源、SQL 语句、映射关系等)存入Configuration
对象。 -
创建
SqlSessionFactory
:
SqlSessionFactory
是单例的,基于Configuration
构建,用于创建SqlSession
(类似数据库连接)。// 初始化核心代码 InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
2. 执行:SqlSession
与动态代理
- 获取
SqlSession
:
调用factory.openSession()
创建SqlSession
,它内部持有Executor
(执行器)和Configuration
,是用户操作的入口。 - 生成 Mapper 代理对象:
调用sqlSession.getMapper(UserMapper.class)
时,MyBatis 通过 JDK 动态代理生成MapperProxy
对象(实现UserMapper
接口)。
代理对象的作用:将接口方法调用(如getUserById(1)
)转换为对SqlSession
的调用,并关联映射文件中对应的 SQL 语句(通过namespace + 方法名
匹配UserMapper.xml
中id="getUserById"
的标签)。
3. SQL 执行:参数处理与语句执行
Executor
调度:
代理对象触发Executor
执行,Executor
根据 SQL 类型(查询/更新)选择对应的处理逻辑,并检查一级缓存(若命中则直接返回结果)。StatementHandler
处理:
Executor
委托StatementHandler
创建 JDBC 的PreparedStatement
,并通过ParameterHandler
处理参数:- 将 Java 参数(如
1
)通过TypeHandler
转换为 JDBC 类型(如INTEGER
); - 替换 SQL 中的
#{id}
占位符,完成预编译。
- 将 Java 参数(如
- 执行 SQL:
StatementHandler
调用preparedStatement.execute()
执行 SQL,获取ResultSet
。
4. 结果处理:映射为 Java 对象
ResultSetHandler
处理结果集:
拿到ResultSet
后,ResultSetHandler
根据映射文件中的resultType
或resultMap
,通过TypeHandler
将数据库字段(如user_name
)转换为 Java 对象的属性(如userName
),最终生成User
对象。- 返回结果与缓存更新:
结果返回给用户,同时Executor
将结果存入一级缓存(SqlSession
级别),后续同一会话的相同查询可直接命中缓存。
5. 事务处理
- 若为增删改操作,
SqlSession
默认需要手动调用commit()
提交事务(可通过openSession(true)
开启自动提交); - 事务由
Transaction
组件管理,底层依赖 JDBC 的Connection
事务机制。
1.3、关键机制解析
-
映射机制
- 参数映射:通过
ParameterHandler
和TypeHandler
完成 Java 参数 → JDBC 参数的转换,支持对象、Map、数组等多种参数类型。 - 结果映射:通过
ResultSetHandler
和TypeHandler
完成ResultSet
→ Java 对象的转换,支持resultType
(简单映射)和resultMap
(复杂映射,解决字段名与属性名不一致问题)。
- 参数映射:通过
-
动态 SQL 解析
映射文件中的<if>
、<foreach>
等动态标签,在初始化时会被解析为SqlNode
树,执行时通过 OGNL 表达式计算条件,动态拼接 SQL 语句,最终生成可执行的 SQL。 -
缓存机制
- 一级缓存:默认开启,由
BaseExecutor
维护(PerpetualCache
实现),作用范围为SqlSession
,同一SqlSession
内相同查询(参数、SQL 一致)会命中缓存。 - 二级缓存:需在映射文件中通过
<cache/>
开启,作用范围为 Mapper 接口,多个SqlSession
共享,由CachingExecutor
管理。
- 一级缓存:默认开启,由
总结
MyBatis 的核心实现是通过配置驱动、动态代理连接接口与 SQL、分层组件协作完成 SQL 执行与结果映射。其设计既保留了 SQL 的灵活性(手动编写 SQL),又通过框架简化了 JDBC 的模板代码(连接管理、参数/结果处理),实现了“半自动化 ORM”的定位。
理解这一机制后,就能明白为何 MyBatis 既比原生 JDBC 高效,又比全自动 ORM(如 Hibernate)更灵活——本质是在“开发者控制 SQL”和“框架简化操作”之间找到了平衡。
2.2、MyBatis的运行原理
MyBatis 的运行原理可以概括为**「配置解析→会话创建→代理调用→SQL 执行→结果映射」**的完整流程,核心是通过框架封装 JDBC 操作,同时保留 SQL 控制权。以下是其运行的核心步骤和原理:
4.1、初始化阶段:加载配置,创建核心工厂
MyBatis 启动时会完成配置解析和核心对象初始化,为后续操作奠定基础:
-
加载配置文件
通过SqlSessionFactoryBuilder
读取全局配置文件(mybatis-config.xml
)和所有映射文件(XXXMapper.xml
),解析内容包括:- 数据库连接信息(数据源、事务管理器)
- 全局设置(如日志、驼峰命名转换、缓存开关)
- Mapper 映射关系(SQL 语句、参数类型、结果类型)
-
构建 Configuration 对象
解析后的配置信息会被封装到Configuration
对象中,这是 MyBatis 的“全局上下文”,存储了所有核心配置和映射元数据(如 Mapper 接口与 SQL 的对应关系)。 -
创建 SqlSessionFactory
SqlSessionFactoryBuilder
根据Configuration
创建SqlSessionFactory
(会话工厂),它是单例的,负责生成SqlSession
(数据库会话)。// 初始化核心代码 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
4.2、执行阶段:从方法调用到 SQL 执行
当调用 Mapper 接口方法(如 userMapper.getUserById(1)
)时,MyBatis 会经历以下流程:
1. 获取 SqlSession
SqlSession
是用户与数据库交互的会话对象,通过 SqlSessionFactory.openSession()
创建,内部持有:
Executor
:执行器(核心组件,负责 SQL 执行和缓存管理)Configuration
:全局配置引用- 数据库连接(
Connection
)和事务状态
2. 生成 Mapper 代理对象
调用 sqlSession.getMapper(UserMapper.class)
时,MyBatis 通过JDK 动态代理生成 UserMapper
接口的代理对象(MapperProxy
)。
代理的作用:将接口方法调用转换为对 SQL 的执行,无需手动实现接口。
3. 匹配 SQL 语句
代理对象通过「接口全类名 + 方法名」(如 com.example.UserMapper.getUserById
),在 Configuration
中匹配映射文件中对应的 SQL 标签(id
与方法名一致的 <select>
/<insert>
等)。
4. 执行器(Executor)调度
代理对象将请求转发给 SqlSession
,最终由 Executor
处理:
- 缓存检查:先查询一级缓存(
SqlSession
级别),若命中则直接返回结果。 - 创建 Statement:
Executor
委托StatementHandler
创建 JDBC 的PreparedStatement
,并绑定 SQL 语句。
5. 参数处理
ParameterHandler
将 Java 方法参数(如 1
)转换为 JDBC 兼容类型,替换 SQL 中的 #{}
占位符(预编译,防止 SQL 注入)。
6. 执行 SQL 并处理结果
StatementHandler
执行PreparedStatement
,获取数据库返回的ResultSet
。ResultSetHandler
根据映射规则(resultType
或resultMap
),将ResultSet
转换为 Java 对象(如User
),过程中通过TypeHandler
处理类型转换(如数据库VARCHAR
→ JavaString
)。
7. 事务处理
- 对于查询操作,直接返回结果;
- 对于增删改操作,需调用
sqlSession.commit()
提交事务(默认手动提交,可配置自动提交)。
4.3、核心原理总结
- 配置驱动:通过 XML 或注解定义映射关系,框架解析后存入
Configuration
,实现 SQL 与代码分离。 - 动态代理:无需编写 Mapper 接口实现类,代理对象自动关联 SQL 语句,简化开发。
- 分层处理:通过
Executor
、StatementHandler
等组件分工协作,封装 JDBC 细节(如连接管理、参数绑定、结果转换)。 - 缓存优化:一级缓存(
SqlSession
级别)和二级缓存(Mapper
级别)减少数据库访问,提升性能。
简化流程图
初始化:
配置文件 → SqlSessionFactoryBuilder → Configuration → SqlSessionFactory执行:
SqlSession → Mapper代理对象 → 匹配SQL → Executor调度 →
StatementHandler(SQL执行)→ ParameterHandler(参数处理)→
ResultSetHandler(结果映射)→ 返回Java对象
通过这套机制,MyBatis 既保留了 SQL 的灵活性(开发者可手动优化),又避免了 JDBC 的繁琐代码,实现了“半自动化 ORM”的高效开发模式。
2.3、MyBatis的全局配置文件
MyBatis 的全局配置文件(通常命名为 mybatis-config.xml
)是框架的核心配置入口,用于定义 MyBatis 的全局行为、数据库连接信息、映射关系等核心参数。其配置严格遵循固定顺序(MyBatis 解析时会校验顺序,错误会导致启动失败),整体结构如下:
<?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><!-- 1. properties --><!-- 2. settings --><!-- 3. typeAliases --><!-- 4. typeHandlers --><!-- 5. objectFactory --><!-- 6. objectWrapperFactory --><!-- 7. reflectorFactory --><!-- 8. plugins --><!-- 9. environments --><!-- 10. databaseIdProvider --><!-- 11. mappers -->
</configuration>
下面逐一剖析各核心配置项的作用和用法:
1. <properties>
:属性配置
用于引入外部配置文件(如数据库连接信息),支持动态替换配置值,避免硬编码。
用法:
<properties resource="db.properties"><!-- 内部默认属性(外部配置会覆盖内部) --><property name="db.username" value="root"/><property name="db.password" value="123456"/>
</properties>
resource
:类路径下的配置文件(如src/main/resources/db.properties
)。url
:网络路径或本地文件系统路径的配置文件(如file:///D:/db.properties
)。- 引入后,可在其他配置中通过
${key}
引用(如${db.driver}
)。
2. <settings>
:全局行为设置
MyBatis 最核心的配置,控制框架的运行时行为(如缓存、日志、命名转换等),每个设置对应一个具体功能。
常用设置:
<settings><!-- 日志实现(STDOUT_LOGGING 控制台输出;LOG4J 需引入依赖) --><setting name="logImpl" value="STDOUT_LOGGING"/><!-- 下划线命名(数据库字段 user_name)转驼峰命名(Java 属性 userName) --><setting name="mapUnderscoreToCamelCase" value="true"/><!-- 开启二级缓存(默认 true) --><setting name="cacheEnabled" value="true"/><!-- 允许 JDBC 支持自动生成主键(适用于自增 ID) --><setting name="useGeneratedKeys" value="true"/><!-- 执行器类型(SIMPLE 简单执行器;REUSE 重用 Statement;BATCH 批量执行器) --><setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
- 所有设置见 官方文档,根据需求开启,无需全部配置。
3. <typeAliases>
:类型别名
为 Java 类型定义简短别名,简化映射文件中 parameterType
、resultType
的书写(无需写全类名)。
用法:
<typeAliases><!-- 方式1:为单个类定义别名 --><typeAlias type="com.example.pojo.User" alias="User"/><!-- 方式2:扫描包下所有类,自动生成别名(默认类名首字母小写,如 User → user) --><package name="com.example.pojo"/><!-- 可通过 @Alias 注解自定义别名(优先级高于默认) --><!-- 例:@Alias("myUser") public class User { ... } -->
</typeAliases>
- 内置别名:MyBatis 为常见类型提供了别名(如
int
→Integer
,map
→HashMap
)。
4. <typeHandlers>
:类型处理器
负责 Java 类型与 JDBC 类型的转换(如 Java LocalDateTime
↔ JDBC Timestamp
)。MyBatis 内置了大部分类型的处理器,也支持自定义。
用法(自定义示例):
<typeHandlers><!-- 注册自定义类型处理器 --><typeHandler handler="com.example.handler.MyDateHandler"/><!-- 扫描包下所有类型处理器 --><package name="com.example.handler"/>
</typeHandlers>
- 自定义处理器需实现
TypeHandler<T>
接口,重写setParameter
(设置参数)和getResult
(获取结果)方法。
5. <environments>
:环境配置
配置数据库连接环境(支持多环境,如开发、测试、生产),包含事务管理器和数据源。
用法:
<environments default="development"> <!-- 默认环境 --><!-- 开发环境 --><environment id="development"><!-- 事务管理器:JDBC(依赖 JDBC 事务);MANAGED(交给容器管理,如 Spring) --><transactionManager type="JDBC"/><!-- 数据源:POOLED(连接池,推荐);UNPOOLED(无池化);JNDI(容器数据源) --><dataSource type="POOLED"><property name="driver" value="${db.driver}"/> <!-- 从 properties 引入 --><property name="url" value="${db.url}"/><property name="username" value="${db.username}"/><property name="password" value="${db.password}"/></dataSource></environment><!-- 测试环境 --><environment id="test"><!-- 配置类似,略 --></environment>
</environments>
default
属性指定默认使用的环境 ID(如development
)。- 连接池配置(
POOLED
)可优化性能,减少频繁创建连接的开销。
6. <mappers>
:映射器注册
注册 SQL 映射文件(XXXMapper.xml
)或 Mapper 接口,告诉 MyBatis 哪里可以找到 SQL 语句。
常用注册方式:
<mappers><!-- 方式1:类路径下的 XML 映射文件(推荐) --><mapper resource="mapper/UserMapper.xml"/><!-- 方式2:接口全类名(需 XML 与接口同包同名,或接口用注解写 SQL) --><mapper class="com.example.mapper.UserMapper"/><!-- 方式3:扫描包下所有接口(需 XML 与接口同包同名) --><package name="com.example.mapper"/><!-- 方式4:网络路径的 XML 文件(较少用) --><mapper url="file:///D:/UserMapper.xml"/>
</mappers>
- 核心:确保 MyBatis 能找到 SQL 语句(XML 中
namespace
需与接口全类名一致)。
其他次要配置(了解即可)
<objectFactory>
:自定义对象工厂,用于创建结果对象实例(默认使用DefaultObjectFactory
)。<plugins>
:插件(如分页插件PageHelper
),可拦截 MyBatis 核心方法(如Executor
、StatementHandler
)。<databaseIdProvider>
:多数据库支持,通过databaseId
区分不同数据库的 SQL(如 MySQL 与 Oracle 语法差异)。
核心总结
全局配置文件的作用是定义 MyBatis 的“全局规则”:
- 数据库连接信息(
environments
)是运行基础; settings
控制框架行为(如日志、缓存、命名转换);typeAliases
和mappers
简化映射配置;- 配置顺序严格,错误会导致启动失败。
实际开发中,无需配置所有项,根据需求选择核心配置(如 properties
、settings
、environments
、mappers
)即可。
2.4、MyBatis关键API
MyBatis 的核心功能通过一系列 API 实现,理解这些关键 API 是掌握 MyBatis 工作流程的关键。以下是最核心的 API 及其作用详解:
一、SqlSessionFactoryBuilder
作用:根据配置信息构建 SqlSessionFactory
(会话工厂),是 MyBatis 初始化的入口。
生命周期:临时对象,创建完 SqlSessionFactory
后即可销毁。
核心方法:
// 1. 从 InputStream 加载配置文件(最常用)
SqlSessionFactory build(InputStream inputStream);// 2. 带环境参数(指定使用哪个环境配置,如开发/生产)
SqlSessionFactory build(InputStream inputStream, String environment);// 3. 带 Properties 参数(动态替换配置中的属性)
SqlSessionFactory build(InputStream inputStream, Properties properties);
使用示例:
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
二、SqlSessionFactory
作用:创建 SqlSession
(数据库会话),是 MyBatis 的核心工厂类。
生命周期:应用级,全局单例,随应用启动创建,随应用关闭销毁。
核心方法:
// 1. 获取默认 SqlSession(手动提交事务)
SqlSession openSession();// 2. 指定事务是否自动提交
SqlSession openSession(boolean autoCommit);// 3. 指定执行器类型(SIMPLE/REUSE/BATCH)
SqlSession openSession(ExecutorType execType);// 4. 指定数据库连接(极少用)
SqlSession openSession(Connection connection);
说明:
ExecutorType.SIMPLE
:默认执行器,每次执行 SQL 新建Statement
。ExecutorType.REUSE
:重用Statement
,适合频繁执行相同 SQL。ExecutorType.BATCH
:批量执行器,适合大量增删改操作(提升性能)。
三、SqlSession
作用:代表与数据库的一次会话,提供增删改查的 API,是用户操作数据库的直接入口。
生命周期:方法级或请求级,用完需关闭(推荐用 try-with-resources 自动关闭)。
核心方法:
1. 通用查询方法
// 查询单个结果(返回对象)
<T> T selectOne(String statement); // statement = "namespace.id"
<T> T selectOne(String statement, Object parameter);// 查询多个结果(返回列表)
<E> List<E> selectList(String statement);
<E> List<E> selectList(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds); // 分页
2. 增删改方法(需事务提交)
// 插入
int insert(String statement);
int insert(String statement, Object parameter);// 更新
int update(String statement);
int update(String statement, Object parameter);// 删除
int delete(String statement);
int delete(String statement, Object parameter);
3. 事务控制
void commit(); // 提交事务
void commit(boolean force); // 强制提交(即使无更新)
void rollback(); // 回滚事务
void rollback(boolean force); // 强制回滚
4. 获取 Mapper 接口(推荐方式)
<T> T getMapper(Class<T> type); // 返回 Mapper 接口的代理对象
5. 其他方法
void close(); // 关闭会话(释放连接)
Connection getConnection(); // 获取底层 JDBC 连接(极少直接使用)
使用示例:
// 推荐方式:通过 Mapper 接口操作
try (SqlSession session = factory.openSession(true)) { // 自动提交事务UserMapper mapper = session.getMapper(UserMapper.class);User user = mapper.getUserById(1); // 调用接口方法
}// 传统方式:通过 statement 字符串操作(不推荐)
try (SqlSession session = factory.openSession()) {User user = session.selectOne("com.example.UserMapper.getUserById", 1);
}
四、Configuration
作用:存储 MyBatis 所有配置信息(全局设置、映射关系、数据源等),是 MyBatis 的“配置中枢”。
生命周期:与 SqlSessionFactory
一致,全局唯一。
核心功能:
- 解析并存储
mybatis-config.xml
和映射文件的配置。 - 提供获取映射语句(
MappedStatement
)、类型处理器、别名等信息的方法。 - 是 MyBatis 各组件(如
Executor
、StatementHandler
)的配置来源。
常用方法:
// 获取映射语句(包含 SQL、参数、结果映射等信息)
MappedStatement getMappedStatement(String id);// 获取类型别名对应的类
Class<?> getTypeAliasRegistry().getTypeAlias(String alias);// 获取数据源
DataSource getDataSource();
五、MapperProxy
(动态代理类)
作用:实现 Mapper 接口的动态代理对象,将接口方法调用转换为 SQL 执行。
生命周期:随 SqlSession
存在,每次 getMapper()
生成新代理对象。
工作原理:
- 当调用
mapper.getUserById(1)
时,实际调用的是MapperProxy
的invoke()
方法。 MapperProxy
根据“接口全类名 + 方法名”匹配Configuration
中的MappedStatement
(映射语句)。- 将方法参数传递给
SqlSession
,触发 SQL 执行并返回结果。
说明:开发者无需手动创建 MapperProxy
,MyBatis 会在 getMapper()
时自动生成。
六、Executor
(执行器)
作用:MyBatis 的核心执行组件,负责 SQL 执行、缓存管理、事务控制等底层逻辑。
生命周期:与 SqlSession
一致,每个 SqlSession
持有一个 Executor
。
主要实现类:
BaseExecutor
:基础执行器,实现了一级缓存和事务管理,是其他执行器的父类。SimpleExecutor
:默认执行器,每次执行 SQL 新建Statement
。ReuseExecutor
:重用Statement
(根据 SQL 缓存Statement
)。BatchExecutor
:批量执行器,合并多个增删改操作批量提交。
CachingExecutor
:缓存执行器,装饰BaseExecutor
实现二级缓存。
核心方法:
// 查询(带缓存逻辑)
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);// 更新(增删改,带事务逻辑)
int update(MappedStatement ms, Object parameter);
七、StatementHandler
、ParameterHandler
、ResultSetHandler
这三个组件是 Executor
的“助手”,负责 SQL 执行的具体细节:
-
StatementHandler
作用:创建 JDBC 的Statement
(PreparedStatement
/CallableStatement
),设置 SQL 语句,执行 SQL。
核心方法:prepare()
(创建 Statement)、parameterize()
(设置参数)、execute()
(执行 SQL)。 -
ParameterHandler
作用:处理参数映射,将 Java 方法参数转换为 JDBCPreparedStatement
的参数。
核心方法:setParameters(PreparedStatement ps)
(为 SQL 占位符设置值)。 -
ResultSetHandler
作用:处理结果集映射,将 JDBC 的ResultSet
转换为 Java 对象。
核心方法:handleResultSets(Statement stmt)
(解析结果集为对象)。
核心 API 关系图
SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → MapperProxy(代理接口)↑
Configuration(全局配置)← Executor(执行器)← StatementHandler/ParameterHandler/ResultSetHandler
总结
MyBatis 的 API 设计遵循“工厂模式”和“代理模式”:
SqlSessionFactoryBuilder
是“创建者”,负责初始化框架。SqlSessionFactory
是“工厂”,负责生产会话。SqlSession
是“会话”,负责用户交互。Executor
及相关处理器是“执行者”,负责底层 SQL 操作。
理解这些 API 的职责和协作关系,就能清晰掌握 MyBatis 从配置到执行的完整流程。实际开发中,开发者主要使用 SqlSessionFactory
、SqlSession
和 Mapper 接口,底层组件(如 Executor
)一般无需直接操作。
3.1、MyBatis查询操作
MyBatis 的查询操作是最常用的数据库交互场景,涉及参数处理、结果映射、动态 SQL 等核心功能。以下从基础查询到复杂场景,详细解析 MyBatis 查询的实现方式和技巧:
一、基础查询:<select>
标签核心配置
查询操作通过映射文件中的 <select>
标签定义,核心属性如下:
属性 | 作用 |
---|---|
id | 与 Mapper 接口方法名一致,作为查询的唯一标识 |
parameterType | 传入参数类型(可选,MyBatis 可自动推断) |
resultType | 单条结果类型(如实体类、Integer 、Map 等) |
resultMap | 复杂结果映射的 ID(用于字段名与属性名不一致、关联查询等场景) |
fetchSize | 驱动每次批量返回的结果行数 |
timeout | 查询超时时间(秒) |
二、参数传递与处理
MyBatis 支持多种参数类型,不同类型的参数传递方式略有差异:
1. 单个参数
- 直接在 SQL 中用
#{参数名}
引用,参数名可任意(建议与方法参数名一致)。
示例:
<!-- UserMapper.xml -->
<select id="getUserById" resultType="User">SELECT id, name, age FROM user WHERE id = #{id}
</select>
Mapper 接口:
User getUserById(int id); // 参数名与 #{id} 对应
2. 多个参数(推荐用 @Param
注解)
- 无注解时,MyBatis 会将参数封装为
Map
,键为param1, param2...
,可读性差。 - 推荐用
@Param
注解指定参数名,SQL 中直接引用注解值。
示例:
<select id="getUserByNameAndAge" resultType="User">SELECT id, name, age FROM user WHERE name = #{name} AND age = #{age}
</select>
Mapper 接口:
User getUserByNameAndAge(@Param("name") String name, @Param("age") int age);
3. 实体类参数
- 当参数较多时,用实体类封装,SQL 中通过
#{属性名}
引用(需保证属性有 getter 方法)。
示例:
<select id="getUserByCondition" resultType="User">SELECT id, name, age FROM user WHERE name LIKE #{name} AND age > #{age}
</select>
Mapper 接口:
List<User> getUserByCondition(User user); // User 类包含 name、age 属性
4. Map 参数
- 用
Map
传递参数,SQL 中通过#{key}
引用 Map 的键。
示例:
<select id="getUserByMap" resultType="User">SELECT id, name, age FROM user WHERE name = #{userName} AND age = #{userAge}
</select>
Mapper 接口:
List<User> getUserByMap(Map<String, Object> params); // map 包含 key: userName, userAge
三、结果映射(解决字段名与属性名不一致)
当数据库字段名(如 user_name
)与 Java 实体属性名(如 userName
)不一致时,需通过以下方式处理:
1. 方式一:SQL 别名(简单场景)
在 SQL 中为字段起别名,与实体属性名保持一致。
<select id="getUserById" resultType="User">SELECT id, user_name AS userName, <!-- 别名匹配属性名 -->user_age AS userAge FROM user WHERE id = #{id}
</select>
2. 方式二:resultMap
(推荐,复杂场景)
在映射文件中定义 resultMap
,明确字段与属性的映射关系,可复用。
<!-- 定义 resultMap -->
<resultMap id="UserResultMap" type="User"><id column="id" property="id"/> <!-- 主键映射 --><result column="user_name" property="userName"/> <!-- 普通字段映射 --><result column="user_age" property="userAge"/>
</resultMap><!-- 使用 resultMap -->
<select id="getUserById" resultMap="UserResultMap">SELECT id, user_name, user_age FROM user WHERE id = #{id}
</select>
3. 方式三:全局开启驼峰命名转换(推荐)
在 mybatis-config.xml
中配置,自动将下划线命名(user_name
)转换为驼峰命名(userName
):
<settings><setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
配置后,无需手动映射,直接使用 resultType
即可:
<select id="getUserById" resultType="User">SELECT id, user_name, user_age FROM user WHERE id = #{id}
</select>
四、动态查询(条件判断与拼接)
通过 MyBatis 动态 SQL 标签,可根据参数条件动态生成 SQL,避免手动拼接的繁琐和错误。
1. <if>
:条件判断
<select id="getUserByCondition" resultType="User">SELECT * FROM userWHERE 1=1 <!-- 避免所有条件不满足时的 SQL 语法错误 --><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age > #{age}</if>
</select>
2. <where>
:自动处理 AND/OR
替代 WHERE 1=1
,自动去除多余的 AND
/OR
:
<select id="getUserByCondition" resultType="User">SELECT * FROM user<where><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age > #{age}</if></where>
</select>
3. <foreach>
:遍历集合(批量查询)
用于 IN
条件或批量操作,遍历 List、数组等集合:
<select id="getUserByIds" resultType="User">SELECT * FROM userWHERE id IN<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
collection
:集合类型(list
对应 List,array
对应数组,或@Param
注解的名称)item
:遍历的元素别名open
/close
:包裹遍历结果的符号(如(
和)
)separator
:元素间的分隔符(如,
)
Mapper 接口:
List<User> getUserByIds(List<Integer> ids);
4. <choose>
/<when>
/<otherwise>
:分支选择
类似 Java 的 switch-case
,只执行第一个满足条件的分支:
<select id="getUserByChoose" resultType="User">SELECT * FROM user<where><choose><when test="id != null">AND id = #{id}</when><when test="name != null">AND name = #{name}</when><otherwise>AND age > 18</otherwise></choose></where>
</select>
五、关联查询(多表查询)
处理多表关联(如一对一、一对多)时,使用 resultMap
的 <association>
和 <collection>
标签。
1. 一对一关联(如 User 与 Card 一对一)
<resultMap id="UserWithCardMap" type="User"><id column="id" property="id"/><result column="name" property="name"/><!-- 一对一关联,property 为 User 类中的 Card 属性 --><association property="card" javaType="Card"><id column="card_id" property="id"/><result column="card_no" property="cardNo"/></association>
</resultMap><select id="getUserWithCard" resultMap="UserWithCardMap">SELECT u.id, u.name, c.id card_id, c.card_no FROM user uLEFT JOIN card c ON u.card_id = c.idWHERE u.id = #{id}
</select>
2. 一对多关联(如 User 与 Order 一对多)
<resultMap id="UserWithOrdersMap" type="User"><id column="id" property="id"/><result column="name" property="name"/><!-- 一对多关联,property 为 User 类中的 List<Order> 属性 --><collection property="orders" ofType="Order"><id column="order_id" property="id"/><result column="order_no" property="orderNo"/></collection>
</resultMap><select id="getUserWithOrders" resultMap="UserWithOrdersMap">SELECT u.id, u.name, o.id order_id, o.order_noFROM user uLEFT JOIN `order` o ON u.id = o.user_idWHERE u.id = #{id}
</select>
六、分页查询
MyBatis 本身不直接支持分页,需通过以下方式实现:
1. 原生分页(RowBounds
)
// Mapper 接口
List<User> getUserByPage(RowBounds rowBounds);// 调用时传入分页参数(offset:起始行,limit:每页条数)
RowBounds rowBounds = new RowBounds(0, 10); // 第 1 页,10 条/页
List<User> users = mapper.getUserByPage(rowBounds);
注意:原生分页是“内存分页”(查询所有结果后截取),大数据量下性能差,不推荐。
2. 插件分页(推荐,如 PageHelper)
- 引入 PageHelper 依赖,自动拦截 SQL 并添加分页语句(如
LIMIT
)。
使用示例:
// 开启分页(PageHelper 会自动拦截后续的查询)
PageHelper.startPage(1, 10); // 第 1 页,10 条/页
List<User> users = mapper.getAllUsers(); // 执行查询// 封装分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
七、查询缓存
MyBatis 提供两级缓存,减少数据库访问,提升查询性能:
1. 一级缓存(默认开启,SqlSession
级别)
- 同一
SqlSession
内,相同 SQL 和参数的查询会命中缓存,直接返回结果。 - 增删改操作会清空一级缓存(避免数据不一致)。
2. 二级缓存(Mapper
级别,需手动开启)
- 步骤 1:在
mybatis-config.xml
中开启全局缓存(默认已开启):<settings><setting name="cacheEnabled" value="true"/> </settings>
- 步骤 2:在映射文件中添加
<cache/>
标签:<mapper namespace="com.example.mapper.UserMapper"><cache/> <!-- 开启当前 Mapper 的二级缓存 --><!-- ... 查询标签 ... --> </mapper>
- 注意:实体类需实现
Serializable
接口,否则缓存失效。
八、查询执行流程总结
- 调用 Mapper 接口方法(如
userMapper.getUserById(1)
)。 - 动态代理(
MapperProxy
)根据方法名匹配映射文件中的<select>
标签。 Executor
执行器检查缓存(一级→二级),未命中则继续。ParameterHandler
处理参数,替换 SQL 中的#{}
占位符。StatementHandler
执行 SQL,获取ResultSet
。ResultSetHandler
根据resultType
或resultMap
转换结果为 Java 对象。- 将结果存入一级缓存,并返回给用户。
通过以上内容,可覆盖 MyBatis 查询的绝大多数场景。实际开发中,需根据业务复杂度选择合适的参数传递方式、结果映射策略和动态 SQL 标签,同时注意缓存和分页的性能优化。
3.2、MyBatis获取自增的主键值
在 MyBatis 中,当插入数据时需要获取数据库自动生成的主键(如 MySQL 的自增 ID),可以通过两种方式实现:使用 useGeneratedKeys
属性 或 使用 <selectKey>
标签。以下是详细说明和案例:
一、方式一:使用 useGeneratedKeys
属性(推荐)
这是最简单的方式,适用于支持自增主键的数据库(如 MySQL、SQL Server)。
核心配置:
在 <insert>
标签中添加两个属性:
useGeneratedKeys="true"
:告诉 MyBatis 要使用数据库生成的主键。keyProperty="id"
:指定将生成的主键值赋给实体类的哪个属性(如User
类的id
属性)。
案例:
- 实体类(User.java):
public class User {private Integer id; // 自增主键private String name;private Integer age;// getter/setter 必须存在(MyBatis 会通过反射赋值)public Integer getId() { return id; }public void setId(Integer id) { this.id = id; }// 其他 getter/setter
}
- 映射文件(UserMapper.xml):
<insert id="addUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
- Mapper 接口(UserMapper.java):
int addUser(User user); // 返回值为影响行数,主键会自动注入到 user 对象中
- 调用示例:
try (SqlSession session = factory.openSession(true)) {UserMapper mapper = session.getMapper(UserMapper.class);User user = new User();user.setName("张三");user.setAge(25);mapper.addUser(user); // 执行插入System.out.println("生成的主键 ID:" + user.getId()); // 插入后可直接获取 ID
}
二、方式二:使用 <selectKey>
标签(通用方式)
适用于不支持自增主键的数据库(如 Oracle 序列),或需要自定义获取主键逻辑的场景。
核心配置:
在 <insert>
标签内部添加 <selectKey>
标签,指定获取主键的 SQL 语句和时机。
关键属性:
keyProperty
:主键值要注入的实体类属性。order
:获取主键的时机(BEFORE
:插入前获取;AFTER
:插入后获取)。resultType
:主键的数据类型。
案例(MySQL 自增 ID):
<insert id="addUser" parameterType="User"><!-- 插入后获取自增 ID(MySQL 适用) --><selectKey keyProperty="id" order="AFTER" resultType="int">SELECT LAST_INSERT_ID() -- MySQL 内置函数,返回最后插入的 ID</selectKey>INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
案例(Oracle 序列):
<insert id="addUser" parameterType="User"><!-- 插入前获取序列值(Oracle 适用) --><selectKey keyProperty="id" order="BEFORE" resultType="int">SELECT SEQ_USER.NEXTVAL FROM DUAL -- Oracle 序列获取下一个值</selectKey>INSERT INTO user (id, name, age) VALUES (#{id}, #{name}, #{age})
</insert>
三、两种方式的区别与选择
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
useGeneratedKeys | 支持自增主键的数据库(MySQL 等) | 配置简单,性能好 | 不支持非自增主键数据库 |
<selectKey> | 所有数据库(尤其是 Oracle 序列) | 通用,可自定义获取逻辑 | 配置略复杂,需手动写 SQL |
推荐原则:
- 若使用 MySQL、SQL Server 等支持自增主键的数据库,优先用
useGeneratedKeys
。 - 若使用 Oracle 等依赖序列的数据库,必须用
<selectKey>
。
四、注意事项
- 实体类必须有 setter 方法:MyBatis 需要通过
setId()
方法将生成的主键注入到对象中。 - 主键值在插入后获取:无论哪种方式,主键值都会被注入到传入的实体对象中,而非通过 Mapper 方法的返回值(返回值是影响行数)。
- 批量插入时的主键获取:批量插入(如
insert into user(...) values (...), (...), (...)
)时,useGeneratedKeys
也能正常工作,每个对象的id
都会被正确赋值。
通过以上方式,MyBatis 能轻松获取插入数据时自动生成的主键,满足后续业务逻辑(如关联插入子表时需要父表的主键)。
4.1、MyBatis的关联映射
MyBatis 的关联映射用于处理数据库中表之间的关系(如一对一、一对多、多对多),并将这些关系映射到 Java 对象的关联关系中。核心通过 <resultMap>
中的 <association>
(一对一)和 <collection>
(一对多)标签实现。
一、关联映射的核心概念
数据库表之间的关系本质是通过外键关联,而 Java 对象通过属性引用关联(如 User
类包含 List<Order>
属性表示一对多)。MyBatis 关联映射的核心是:
通过 SQL 关联查询获取关联数据,再通过 resultMap
定义对象间的映射规则。
二、一对一关联(association
)
场景:两个表是一对一关系(如 user
表和 card
表,一个用户对应一张身份证)。
1. 数据库表结构
-- 用户表
CREATE TABLE `user` (`id` int PRIMARY KEY AUTO_INCREMENT,`name` varchar(20),`card_id` int UNIQUE -- 外键,关联身份证表
);-- 身份证表
CREATE TABLE `card` (`id` int PRIMARY KEY AUTO_INCREMENT,`card_no` varchar(18), -- 身份证号`address` varchar(100) -- 地址
);
2. 实体类设计
// 用户类(包含一对一关联的 Card 属性)
public class User {private Integer id;private String name;private Card card; // 关联的身份证对象(一对一)// getter/setter
}// 身份证类
public class Card {private Integer id;private String cardNo;private String address;// getter/setter
}
3. 映射配置(UserMapper.xml
)
通过 <association>
标签定义一对一映射,有两种实现方式:
方式一:嵌套结果(单 SQL 关联查询)
一次 SQL 查询关联两张表,直接获取所有数据,性能更优。
<!-- 定义包含一对一关联的 resultMap -->
<resultMap id="UserWithCardMap" type="User"><id column="user_id" property="id"/><result column="user_name" property="name"/><!-- 一对一关联配置 --><association property="card" <!-- User 类中关联属性名 -->javaType="Card" <!-- 关联对象的类型 -->><id column="card_id" property="id"/><result column="card_no" property="cardNo"/><result column="address" property="address"/></association>
</resultMap><!-- 关联查询 SQL -->
<select id="getUserWithCard" resultMap="UserWithCardMap">SELECT u.id user_id, u.name user_name, c.id card_id, c.card_no, c.address FROM user uLEFT JOIN card c ON u.card_id = c.idWHERE u.id = #{id}
</select>
方式二:嵌套查询(多 SQL 分步查询)
先查主表,再根据主表结果查询关联表(支持懒加载)。
<!-- 1. 主查询:查询用户 -->
<resultMap id="UserMap" type="User"><id column="id" property="id"/><result column="name" property="name"/><!-- 嵌套查询:通过 card_id 查 card 表 --><association property="card"javaType="Card"column="card_id" <!-- 传递给子查询的参数(用户表的 card_id) -->select="com.example.mapper.CardMapper.getCardById" <!-- 子查询的全路径 -->/>
</resultMap><select id="getUserById" resultMap="UserMap">SELECT id, name, card_id FROM user WHERE id = #{id}
</select><!-- 2. 子查询(CardMapper.xml):查询身份证 -->
<select id="getCardById" resultType="Card">SELECT id, card_no, address FROM card WHERE id = #{id}
</select>
4. Mapper 接口与调用
// UserMapper 接口
User getUserWithCard(Integer id); // 嵌套结果方式
User getUserById(Integer id); // 嵌套查询方式// 调用示例
User user = userMapper.getUserWithCard(1);
System.out.println(user.getName()); // 用户名
System.out.println(user.getCard().getCardNo()); // 关联的身份证号
三、一对多关联(collection
)
场景:两个表是一对多关系(如 user
表和 order
表,一个用户对应多个订单)。
1. 数据库表结构
-- 订单表(外键关联用户表)
CREATE TABLE `order` (`id` int PRIMARY KEY AUTO_INCREMENT,`order_no` varchar(20), -- 订单号`user_id` int, -- 外键,关联用户表FOREIGN KEY (user_id) REFERENCES user(id)
);
2. 实体类设计
// 用户类(包含一对多关联的 List<Order> 属性)
public class User {private Integer id;private String name;private List<Order> orders; // 关联的订单集合(一对多)// getter/setter
}// 订单类
public class Order {private Integer id;private String orderNo;private Integer userId; // 外键,关联用户 ID// getter/setter
}
3. 映射配置(UserMapper.xml
)
通过 <collection>
标签定义一对多映射,同样支持嵌套结果和嵌套查询。
方式一:嵌套结果(单 SQL 关联查询)
<!-- 定义包含一对多关联的 resultMap -->
<resultMap id="UserWithOrdersMap" type="User"><id column="user_id" property="id"/><result column="user_name" property="name"/><!-- 一对多关联配置 --><collection property="orders" <!-- User 类中关联集合属性名 -->ofType="Order" <!-- 集合中元素的类型(区别于 javaType) -->><id column="order_id" property="id"/><result column="order_no" property="orderNo"/><result column="user_id" property="userId"/></collection>
</resultMap><!-- 关联查询 SQL -->
<select id="getUserWithOrders" resultMap="UserWithOrdersMap">SELECT u.id user_id, u.name user_name, o.id order_id, o.order_no, o.user_id FROM user uLEFT JOIN `order` o ON u.id = o.user_idWHERE u.id = #{id}
</select>
方式二:嵌套查询(多 SQL 分步查询)
<!-- 1. 主查询:查询用户 -->
<resultMap id="UserMap" type="User"><id column="id" property="id"/><result column="name" property="name"/><!-- 嵌套查询:通过 user_id 查 order 表 --><collection property="orders"ofType="Order"column="id" <!-- 传递用户 ID 给子查询 -->select="com.example.mapper.OrderMapper.getOrdersByUserId" <!-- 子查询路径 -->/>
</resultMap><select id="getUserById" resultMap="UserMap">SELECT id, name FROM user WHERE id = #{id}
</select><!-- 2. 子查询(OrderMapper.xml):查询用户的所有订单 -->
<select id="getOrdersByUserId" resultType="Order">SELECT id, order_no, user_id FROM `order` WHERE user_id = #{userId}
</select>
4. 调用示例
User user = userMapper.getUserWithOrders(1);
System.out.println("用户:" + user.getName());
// 遍历订单集合
for (Order order : user.getOrders()) {System.out.println("订单号:" + order.getOrderNo());
}
四、多对多关联(拆分为两个一对多)
场景:两个表是多对多关系(如 student
表和 course
表,一个学生可选多门课程,一门课程可被多个学生选)。
实现方式:通过中间表(如 student_course
)拆分为“学生→中间表”和“课程→中间表”两个一对多关联。
1. 数据库表结构
-- 学生表
CREATE TABLE `student` (id int PRIMARY KEY AUTO_INCREMENT, name varchar(20));-- 课程表
CREATE TABLE `course` (id int PRIMARY KEY AUTO_INCREMENT, name varchar(20));-- 中间表(关联学生和课程)
CREATE TABLE `student_course` (`id` int PRIMARY KEY AUTO_INCREMENT,`student_id` int,`course_id` int,FOREIGN KEY (student_id) REFERENCES student(id),FOREIGN KEY (course_id) REFERENCES course(id)
);
2. 实体类设计
// 学生类(包含多门课程)
public class Student {private Integer id;private String name;private List<Course> courses; // 多对多关联的课程集合// getter/setter
}// 课程类
public class Course {private Integer id;private String name;// getter/setter
}
3. 映射配置(StudentMapper.xml
)
通过关联查询中间表,实现多对多映射:
<resultMap id="StudentWithCoursesMap" type="Student"><id column="student_id" property="id"/><result column="student_name" property="name"/><!-- 多对多本质是一对多:学生→中间表→课程 --><collection property="courses" ofType="Course"><id column="course_id" property="id"/><result column="course_name" property="name"/></collection>
</resultMap><select id="getStudentWithCourses" resultMap="StudentWithCoursesMap">SELECT s.id student_id, s.name student_name, c.id course_id, c.name course_name FROM student sLEFT JOIN student_course sc ON s.id = sc.student_idLEFT JOIN course c ON sc.course_id = c.idWHERE s.id = #{id}
</select>
五、关联映射的关键属性与注意事项
-
association
vscollection
:association
:用于一对一关联,javaType
指定关联对象类型。collection
:用于一对多关联,ofType
指定集合中元素类型(易错点:不要用javaType
)。
-
嵌套结果 vs 嵌套查询:
- 嵌套结果:单 SQL 关联查询,性能好(减少数据库访问),但 SQL 较复杂。
- 嵌套查询:多 SQL 分步查询,代码清晰,支持懒加载,但可能产生“N+1 问题”(1 次主查询 + N 次子查询)。
-
懒加载(延迟加载):
只在真正使用关联数据时才执行子查询(如调用user.getOrders()
时才查订单),减少不必要的查询。
需在mybatis-config.xml
中开启:<settings><setting name="lazyLoadingEnabled" value="true"/> <!-- 开启懒加载 --><setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载 --> </settings>
-
N+1 问题解决方案:
嵌套查询可能导致 N+1 问题(如查询 10 个用户,每个用户查一次订单,共 11 次查询)。
解决:使用嵌套结果(单 SQL 查询),或通过fetchType="eager"
强制立即加载(适合数据量小的场景)。
总结
关联映射是 MyBatis 处理表关系的核心能力,核心要点:
- 一对一用
<association>
,一对多用<collection>
。 - 优先使用嵌套结果(单 SQL)优化性能,复杂场景可考虑嵌套查询 + 懒加载。
- 多对多通过中间表拆分为两个一对多实现。
根据业务场景选择合适的映射方式,平衡代码可读性和性能。
4.2、MyBatis的自动映射和自定义结果映射
MyBatis 提供了两种结果映射方式:自动映射和自定义结果映射,用于将数据库查询结果(ResultSet
)转换为 Java 对象。两种方式适用于不同场景,下面详细解析:
一、自动映射(Auto-mapping)
自动映射是 MyBatis 的默认行为,无需手动配置映射关系,框架会自动将数据库字段与 Java 对象的属性进行匹配。
1. 自动映射的规则
- 默认匹配:数据库字段名与 Java 属性名完全一致时,自动映射(如
user_name
无法匹配userName
,需额外配置)。 - 驼峰命名转换:开启后,数据库下划线命名(
user_name
)可自动匹配 Java 驼峰命名(userName
)。
2. 开启驼峰命名转换(推荐)
在全局配置文件 mybatis-config.xml
中添加设置:
<settings><!-- 下划线转驼峰(如 user_name → userName) --><setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
3. 自动映射的使用场景
适用于表字段名与实体属性名一致(或符合驼峰规则) 的简单场景,无需编写 resultMap
。
示例:
- 数据库表
user
字段:id
、user_name
、user_age
- Java 实体类
User
属性:id
、userName
、userAge
(驼峰命名)
映射文件配置:
<!-- 直接使用 resultType,MyBatis 自动映射 -->
<select id="getUserById" parameterType="int" resultType="User">SELECT id, user_name, user_age FROM user WHERE id = #{id}
</select>
4. 自动映射的优点与局限
- 优点:配置简单,无需手动编写映射关系,开发效率高。
- 局限:
- 无法处理字段名与属性名完全不匹配的场景(如
u_id
对应userId
)。 - 不支持复杂关联查询(如一对一、一对多)。
- 无法自定义映射逻辑(如类型转换、字段过滤)。
- 无法处理字段名与属性名完全不匹配的场景(如
二、自定义结果映射(resultMap
)
当自动映射无法满足需求时,需通过 <resultMap>
标签手动定义映射关系,支持复杂场景。
1. 自定义结果映射的核心标签
标签 | 作用 | 常用属性 |
---|---|---|
<resultMap> | 定义映射关系的根标签 | id (唯一标识)、type (映射类型) |
<id> | 映射主键字段(性能优化) | column (数据库字段名)、property (Java 属性名) |
<result> | 映射普通字段 | 同 <id> |
<association> | 映射一对一关联对象 | property 、javaType (关联对象类型) |
<collection> | 映射一对多关联集合 | property 、ofType (集合元素类型) |
2. 自定义结果映射的使用场景
场景 1:字段名与属性名完全不匹配
示例:
- 数据库字段:
u_id
、u_name
- Java 属性:
userId
、userName
映射配置:
<!-- 定义自定义结果映射 -->
<resultMap id="UserMap" type="User"><id column="u_id" property="userId"/> <!-- 主键映射 --><result column="u_name" property="userName"/> <!-- 普通字段映射 -->
</resultMap><!-- 使用自定义 resultMap -->
<select id="getUserById" resultMap="UserMap">SELECT u_id, u_name FROM user WHERE u_id = #{id}
</select>
场景 2:复杂类型转换
当数据库字段类型与 Java 属性类型需要特殊转换时(如数据库 VARCHAR
存储的日期字符串 → Java LocalDate
),可结合 typeHandler
自定义映射。
示例:
<resultMap id="UserMap" type="User"><id column="id" property="id"/><!-- 自定义类型处理器转换日期 --><result column="birth_day" property="birthDay" typeHandler="com.example.handler.MyDateHandler"/>
</resultMap>
自定义类型处理器需实现 TypeHandler<T>
接口,重写参数设置和结果转换方法。
场景 3:关联查询(一对一/一对多)
如用户与订单的一对多关系,需通过 <collection>
标签映射:
<resultMap id="UserWithOrdersMap" type="User"><id column="user_id" property="id"/><result column="user_name" property="name"/><!-- 一对多关联映射 --><collection property="orders" ofType="Order"><id column="order_id" property="id"/><result column="order_no" property="orderNo"/></collection>
</resultMap><select id="getUserWithOrders" resultMap="UserWithOrdersMap">SELECT u.id user_id, u.name user_name, o.id order_id, o.order_noFROM user u LEFT JOIN `order` o ON u.id = o.user_idWHERE u.id = #{id}
</select>
3. 自定义结果映射的优点
- 灵活处理字段名与属性名不匹配的场景。
- 支持复杂关联查询和自定义类型转换。
- 可复用(多个 SQL 语句可引用同一个
resultMap
)。
三、自动映射与自定义映射的对比与选择
维度 | 自动映射(resultType ) | 自定义映射(resultMap ) |
---|---|---|
配置复杂度 | 简单(无需额外配置) | 复杂(需手动编写 resultMap ) |
适用场景 | 字段名与属性名匹配(或驼峰规则) | 字段名不匹配、关联查询、自定义转换 |
性能 | 略优(无需解析 resultMap ) | 略差(需解析映射规则) |
灵活性 | 低(仅支持默认规则) | 高(支持复杂场景) |
四、最佳实践
- 优先使用自动映射:简单场景下,通过开启驼峰命名转换(
mapUnderscoreToCamelCase
)减少配置。 - 复杂场景用自定义映射:字段不匹配、关联查询、类型转换等场景,使用
resultMap
。 - 复用
resultMap
:通过<resultMap extends="父resultMap">
继承已有映射,减少重复配置。
<!-- 父 resultMap -->
<resultMap id="BaseUserMap" type="User"><id column="id" property="id"/><result column="name" property="name"/>
</resultMap><!-- 子 resultMap 继承并扩展 -->
<resultMap id="UserWithAgeMap" type="User" extends="BaseUserMap"><result column="age" property="age"/> <!-- 新增字段映射 -->
</resultMap>
通过合理选择映射方式,既能保证开发效率,又能满足复杂业务场景的需求。自动映射适合简单查询,自定义映射适合复杂场景,两者结合可最大化发挥 MyBatis 的灵活性。
4.3、MyBatis的延迟加载
MyBatis 的延迟加载(Lazy Loading)又称懒加载,是一种优化数据库查询性能的机制:在关联查询时,只加载主表数据,当真正需要访问关联表数据时才会执行关联查询,避免不必要的数据库访问。
一、延迟加载的核心思想
- 默认行为:不开启延迟加载时,关联查询(如查询用户同时查询其订单)会一次性执行所有 SQL,即使后续不使用关联数据。
- 延迟加载:开启后,只先执行主表查询(如查询用户),关联表查询(如查询订单)会延迟到第一次访问关联属性(如
user.getOrders()
)时才执行。
优势:减少无效查询,提升性能(尤其适合关联数据量大或不常访问关联数据的场景)。
二、延迟加载的配置
MyBatis 延迟加载需要在全局配置文件中手动开启,步骤如下:
1. 开启全局延迟加载开关
在 mybatis-config.xml
中添加 <settings>
配置:
<settings><!-- 开启延迟加载(默认 false) --><setting name="lazyLoadingEnabled" value="true"/><!-- 关闭积极加载(默认 true),改为按需加载 --><!-- 积极加载:访问主对象的任何属性都会触发关联查询 --><!-- 按需加载:只有访问关联属性时才触发关联查询 --><setting name="aggressiveLazyLoading" value="false"/>
</settings>
2. 配置关联查询为延迟加载
延迟加载仅对嵌套查询(分步查询) 有效,需在 <association>
或 <collection>
中配置 select
属性指定子查询。
示例(一对多关联):
<!-- UserMapper.xml:查询用户(主查询) -->
<resultMap id="UserMap" type="User"><id column="id" property="id"/><result column="name" property="name"/><!-- 订单关联(延迟加载) --><collection property="orders" <!-- User 类中的订单集合属性 -->ofType="Order" <!-- 集合元素类型 -->column="id" <!-- 传递给子查询的参数(用户 ID) -->select="com.example.mapper.OrderMapper.getOrdersByUserId" <!-- 子查询路径 -->fetchType="lazy" <!-- 显式指定延迟加载(默认,可省略) -->/>
</resultMap><select id="getUserById" resultMap="UserMap">SELECT id, name FROM user WHERE id = #{id}
</select><!-- OrderMapper.xml:查询订单(子查询,延迟执行) -->
<select id="getOrdersByUserId" resultType="Order">SELECT id, order_no, user_id FROM `order` WHERE user_id = #{userId}
</select>
三、延迟加载的执行流程
以 getUserById(1)
为例,执行流程如下:
- 调用
userMapper.getUserById(1)
时,MyBatis 执行主查询 SQL:
SELECT id, name FROM user WHERE id = 1
,返回 User 对象(此时orders
属性为代理对象)。 - 当首次调用
user.getOrders()
时,触发延迟加载:
MyBatis 执行子查询 SQL:SELECT ... FROM order WHERE user_id = 1
,获取订单数据并赋值给orders
属性。 - 后续再次调用
user.getOrders()
时,直接使用已加载的订单数据,不再执行 SQL。
四、延迟加载的关键属性
-
fetchType
:在<association>
或<collection>
中指定加载方式:fetchType="lazy"
:延迟加载(优先于全局配置)。fetchType="eager"
:立即加载(忽略全局延迟配置,适合必须加载关联数据的场景)。
<!-- 单个关联强制立即加载 --> <collection property="orders"ofType="Order"column="id"select="..."fetchType="eager" <!-- 忽略全局延迟,立即执行子查询 --> />
五、注意事项
-
仅支持嵌套查询:延迟加载只对“嵌套查询(分步查询)”有效,对“嵌套结果(单 SQL 关联查询)”无效(因为单 SQL 已一次性查询所有数据)。
-
SqlSession
需保持打开:延迟加载执行子查询时需要用到SqlSession
,如果在获取主对象后关闭了SqlSession
,访问关联属性会报错。// 错误示例:关闭 SqlSession 后访问关联属性 SqlSession session = factory.openSession(); User user = session.getMapper(UserMapper.class).getUserById(1); session.close(); // 关闭会话 user.getOrders(); // 报错:无法执行子查询(会话已关闭)
正确做法:在
SqlSession
关闭前访问关联属性,或使用try-with-resources
确保会话在使用期间有效。 -
N+1 问题:延迟加载可能导致“N+1 问题”——查询 N 个主对象时,会触发 N 次子查询(1 次主查询 + N 次子查询),反而降低性能。
解决方案:
- 数据量小时,用
fetchType="eager"
强制立即加载。 - 数据量大时,改用“嵌套结果(单 SQL 关联查询)”一次性获取所有数据。
- 数据量小时,用
总结
- 适用场景:关联数据不常访问、关联表数据量大的场景,可减少无效查询。
- 配置要点:开启全局
lazyLoadingEnabled
和关闭aggressiveLazyLoading
,结合嵌套查询使用。 - 避坑指南:注意
SqlSession
生命周期,避免 N+1 问题。
延迟加载是 MyBatis 优化查询性能的重要手段,合理使用可显著提升系统效率,但需根据业务场景权衡利弊。
5.1、MyBatis的动态SQL
MyBatis 的动态 SQL 是其核心特性之一,用于根据不同条件动态生成 SQL 语句,解决了手动拼接 SQL 时的繁琐和语法错误问题(如多余的 AND/OR
、逗号等)。动态 SQL 通过一系列标签实现,以下是详细解析:
一、动态 SQL 核心标签
MyBatis 提供了 7 种常用动态 SQL 标签,覆盖大多数条件判断和拼接场景:
标签 | 作用 | 适用场景 |
---|---|---|
<if> | 单条件判断 | 简单的条件查询(如非空判断) |
<choose> | 多条件分支选择(类似 switch-case ) | 多条件互斥场景(只执行一个条件) |
<when> | <choose> 的子标签,定义分支条件 | 配合 <choose> 使用 |
<otherwise> | <choose> 的子标签,定义默认分支 | 配合 <choose> 使用 |
<where> | 自动处理 WHERE 子句的 AND/OR 前缀 | 条件查询的 WHERE 拼接 |
<set> | 自动处理 SET 子句的逗号后缀 | 更新操作的 SET 拼接 |
<foreach> | 遍历集合,生成批量操作语句 | IN 条件、批量插入/删除 |
<bind> | 定义变量,简化参数处理 | 模糊查询(兼容不同数据库) |
二、常用标签详解与案例
1. <if>
:单条件判断
根据 test
属性的表达式(OGNL 语法)决定是否包含标签内的 SQL 片段。
语法:
<if test="条件表达式">SQL 片段(如 AND 字段 = #{参数})
</if>
案例:多条件查询用户
根据 name
(模糊查询)和 age
(大于)筛选用户,参数非空时才添加条件:
<select id="getUserByCondition" resultType="User">SELECT id, name, age FROM userWHERE 1=1 <!-- 避免所有条件不满足时的 SQL 语法错误 --><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if><if test="age != null">AND age > #{age}</if>
</select>
说明:
test
表达式中,字符串判断需同时检查!= null
和!= ''
(避免空串)。- 数字/日期类型只需判断
!= null
(如age != null
)。
2. <where>
+ <if>
:优化 WHERE 子句
<where>
标签会自动处理条件前的 AND/OR
,并在有条件时添加 WHERE
关键字,无需手动写 WHERE 1=1
。
案例:优化上面的多条件查询
<select id="getUserByCondition" resultType="User">SELECT id, name, age FROM user<where> <!-- 替代 WHERE 1=1 --><if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%') <!-- 多余的 AND 会被自动去除 --></if><if test="age != null">AND age > #{age}</if></where>
</select>
效果:
- 若两个条件都满足:
WHERE name LIKE ... AND age > ...
- 若仅
name
满足:WHERE name LIKE ...
(自动去掉AND
) - 若都不满足:不生成
WHERE
子句(查询所有数据)
3. <choose>
+ <when>
+ <otherwise>
:多条件分支
类似 Java 的 switch-case
,只执行第一个满足条件的 <when>
,若都不满足则执行 <otherwise>
。
案例:优先按 ID 查询,其次按名称,否则查询默认数据
<select id="getUserByChoose" resultType="User">SELECT id, name, age FROM user<where><choose><when test="id != null"> <!-- 条件1:有ID则按ID查 -->AND id = #{id}</when><when test="name != null and name != ''"> <!-- 条件2:无ID但有名称则按名称查 -->AND name = #{name}</when><otherwise> <!-- 默认:查询年龄大于18的用户 -->AND age > 18</otherwise></choose></where>
</select>
4. <set>
+ <if>
:优化 UPDATE 语句
<set>
标签用于 UPDATE
语句,自动处理字段后的逗号,确保 SET
子句语法正确。
案例:动态更新用户信息(只更新非空字段)
<update id="updateUserSelective" parameterType="User">UPDATE user<set> <!-- 自动处理逗号 --><if test="name != null and name != ''">name = #{name}, <!-- 多余的逗号会被自动去除 --></if><if test="age != null">age = #{age}</if></set>WHERE id = #{id}
</update>
效果:
- 若
name
和age
都非空:UPDATE user SET name = ..., age = ... WHERE id = ...
- 若仅
name
非空:UPDATE user SET name = ... WHERE id = ...
(自动去掉逗号)
5. <foreach>
:遍历集合(批量操作)
用于遍历数组、List 等集合,生成批量 SQL(如 IN
条件、批量插入/删除)。
核心属性:
collection
:集合类型(list
对应 List,array
对应数组,或@Param
注解的名称)。item
:遍历的元素别名(如id
)。open
:拼接结果的前缀(如(
)。close
:拼接结果的后缀(如)
)。separator
:元素间的分隔符(如,
)。
案例 1:批量查询(IN 条件)
根据 ID 列表查询多个用户:
<select id="getUserByIds" resultType="User">SELECT id, name, age FROM userWHERE id IN<foreach collection="list" item="id" open="(" separator="," close=")">#{id} <!-- 遍历结果:(1,2,3) --></foreach>
</select>
Mapper 接口:
List<User> getUserByIds(List<Integer> ids);
案例 2:批量插入
一次性插入多个用户:
<insert id="batchInsert" parameterType="java.util.List">INSERT INTO user (name, age) VALUES<foreach collection="list" item="user" separator=",">(#{user.name}, #{user.age}) <!-- 遍历结果:(张三,20),(李四,25) --></foreach>
</insert>
Mapper 接口:
int batchInsert(List<User> users);
6. <bind>
:定义变量(兼容多数据库)
在 SQL 中定义变量,避免不同数据库的函数差异(如模糊查询的 CONCAT
与 ||
)。
案例:模糊查询(兼容 MySQL 和 Oracle)
MySQL 用 CONCAT('%', name, '%')
,Oracle 用 '%' || name || '%'
,bind
可统一处理:
<select id="getUserLikeName" resultType="User"><!-- 定义变量:将 name 拼接为 %name% --><bind name="likeName" value="'%' + name + '%'"/>SELECT id, name, age FROM userWHERE name LIKE #{likeName}
</select>
Mapper 接口:
List<User> getUserLikeName(@Param("name") String name);
三、动态 SQL 的工作原理
- 解析阶段:MyBatis 初始化时,将动态 SQL 标签解析为
SqlNode
对象(如IfSqlNode
、ForEachSqlNode
),形成 SQL 节点树。 - 执行阶段:调用 Mapper 方法时,根据传入的参数,通过 OGNL 表达式计算标签的
test
条件,动态拼接 SQL 片段,最终生成可执行的 SQL 语句。
四、注意事项
-
OGNL 表达式语法:
- 判断字符串非空:
test="name != null and name != ''"
- 判断集合非空:
test="list != null and list.size() > 0"
- 判断数组非空:
test="array != null and array.length > 0"
- 逻辑运算符:
&&
(或and
)、||
(或or
)、!
(或not
)
- 判断字符串非空:
-
参数传递:
- 多参数建议用
@Param
注解命名(如@Param("ids") List<Integer> ids
),避免collection
属性值混淆。 - 传递对象时,
test
表达式直接引用对象属性(如user.name != null
)。
- 多参数建议用
-
性能优化:复杂动态 SQL 可能影响解析性能,建议避免过度嵌套(如多层
<if>
嵌套)。
总结
动态 SQL 是 MyBatis 应对复杂查询场景的核心工具,通过 <if>
、<where>
、<foreach>
等标签,可灵活生成符合条件的 SQL 语句,避免手动拼接的错误。掌握动态 SQL 能显著提升 MyBatis 的使用效率,尤其适合多条件查询、批量操作等场景。
实际开发中,需根据业务需求选择合适的标签组合,并注意 OGNL 表达式的正确使用,确保动态 SQL 的可读性和性能。
5.2、MyBatis的<trim><sql>
和<include>
标签
在 MyBatis 中,<trim>
、<sql>
和 <include>
标签是用于优化 SQL 配置、减少重复代码的实用工具标签。它们常与动态 SQL 结合使用,提升映射文件的可维护性。以下是详细解析:
一、<trim>
标签:灵活处理 SQL 片段的前后缀
<trim>
标签用于动态添加或去除 SQL 片段的前缀、后缀,比 <where>
和 <set>
更灵活(<where>
和 <set>
可视为 <trim>
的特殊实现)。
核心属性:
属性 | 作用 | 示例 |
---|---|---|
prefix | 当标签内有内容时,添加指定前缀 | prefix="WHERE" |
suffix | 当标签内有内容时,添加指定后缀 | suffix=";" |
prefixOverrides | 去除内容前多余的指定字符(用 ` | ` 分隔多个) |
suffixOverrides | 去除内容后多余的指定字符(用 ` | ` 分隔多个) |
典型用法:
1. 替代 <where>
标签
<select id="getUserByCondition" resultType="User">SELECT id, name, age FROM user<trim prefix="WHERE" prefixOverrides="AND|OR"><if test="name != null">AND name = #{name}</if><if test="age != null">AND age > #{age}</if></trim>
</select>
- 效果:若条件成立,自动添加
WHERE
前缀,并去除多余的AND/OR
,与<where>
功能一致。
2. 替代 <set>
标签
<update id="updateUser" parameterType="User">UPDATE user<trim prefix="SET" suffixOverrides=","><if test="name != null">name = #{name},</if><if test="age != null">age = #{age},</if></trim>WHERE id = #{id}
</update>
- 效果:自动添加
SET
前缀,并去除多余的逗号,与<set>
功能一致。
3. 自定义场景(如批量插入的 VALUES 拼接)
<insert id="batchInsert" parameterType="list">INSERT INTO user (name, age) VALUES<trim suffixOverrides=","><foreach collection="list" item="user" separator=",">(#{user.name}, #{user.age}),</foreach></trim>
</insert>
- 作用:去除
foreach
循环最后多余的逗号,避免 SQL 语法错误。
二、<sql>
和 <include>
标签:SQL 片段复用
当多个 SQL 语句需要重复使用相同的片段(如字段列表、条件)时,可通过 <sql>
定义片段,再用 <include>
引用,减少代码冗余。
1. <sql>
标签:定义可复用的 SQL 片段
- 作用:将重复的 SQL 片段(如字段列表、条件)抽取为独立模块。
- 属性:
id
(片段唯一标识,用于<include>
引用)。
示例:定义字段列表片段
<!-- 定义用户表的通用查询字段 -->
<sql id="userColumns">id, name, age, create_time, update_time
</sql>
2. <include>
标签:引用 <sql>
定义的片段
- 作用:在 SQL 语句中引入
<sql>
定义的片段。 - 属性:
refid
(指定要引用的<sql>
片段的id
)。
示例:引用字段列表片段
<!-- 查询用户时引用通用字段 -->
<select id="getUserById" resultType="User">SELECT <include refid="userColumns"/> FROM user WHERE id = #{id}
</select><!-- 分页查询时复用同一字段列表 -->
<select id="getUserByPage" resultType="User">SELECT <include refid="userColumns"/> FROM user LIMIT #{offset}, #{limit}
</select>
3. 带参数的 SQL 片段(动态传递变量)
<sql>
片段中可通过 <property>
标签定义参数,引用时用 <include>
的 <property>
传递值,实现动态片段。
示例:动态条件片段
<!-- 定义带参数的条件片段 -->
<sql id="dynamicCondition"><if test="status != null">AND status = #{status}</if><if test="startTime != null">AND create_time >= #{startTime}</if>
</sql><!-- 引用时传递参数 -->
<select id="getUserByDynamic" resultType="User">SELECT <include refid="userColumns"/> FROM user<where><include refid="dynamicCondition"><!-- 可传递额外参数(非必需,视片段是否需要而定) --><property name="status" value="1"/> <!-- 固定值 --><!-- <property name="startTime" value="#{startTime}"/> 动态值 --></include></where>
</select>
三、组合使用:<trim>
+ <sql>
+ <include>
三者结合可最大化减少重复代码,同时处理动态 SQL 的前缀/后缀问题。
示例:通用更新语句模板
<!-- 定义通用更新字段片段 -->
<sql id="updateFields"><if test="name != null">name = #{name},</if><if test="age != null">age = #{age},</if><if test="updateTime != null">update_time = #{updateTime}</if>
</sql><!-- 复用片段并通过 trim 处理后缀 -->
<update id="updateUserSelective" parameterType="User">UPDATE user<trim prefix="SET" suffixOverrides=","><include refid="updateFields"/> <!-- 引入更新字段片段 --></trim>WHERE id = #{id}
</update>
四、优势与最佳实践
-
优势:
- 减少冗余:
<sql>
+<include>
避免相同 SQL 片段重复编写,便于统一维护(如修改字段时只需改一处)。 - 灵活处理语法:
<trim>
可自定义前缀/后缀规则,覆盖<where>
和<set>
无法满足的场景。 - 提升可读性:将复杂 SQL 拆分为片段,结构更清晰。
- 减少冗余:
-
最佳实践:
- 抽取高频重复片段(如字段列表、通用条件)为
<sql>
。 - 复杂动态 SQL 用
<trim>
替代手动拼接,避免语法错误。 - 片段
id
命名规范(如表名_功能
,如user_columns
、order_conditions
),便于识别。
- 抽取高频重复片段(如字段列表、通用条件)为
总结
<trim>
:灵活处理 SQL 片段的前缀、后缀,是动态 SQL 的“万能工具”。<sql>
+<include>
:实现 SQL 片段复用,减少重复代码,提升可维护性。- 三者结合使用可显著优化 MyBatis 映射文件的结构,尤其适合大型项目中复杂 SQL 的管理。
6.1、MyBatis的缓存机制
MyBatis 提供了完善的缓存机制,用于减少数据库访问次数、提升查询性能。其缓存分为一级缓存和二级缓存,核心思想是将查询结果暂存起来,相同查询再次执行时直接从缓存获取,避免重复访问数据库。
一、一级缓存(Local Cache)
一级缓存是 MyBatis 的默认缓存,属于SqlSession 级别(会话级缓存),即同一个 SqlSession 内的缓存共享。
1. 工作原理
- 缓存范围:绑定到 SqlSession,每个 SqlSession 拥有独立的一级缓存。
- 触发时机:
- 执行
select
语句时,MyBatis 会先查询一级缓存,若存在相同查询(SQL 语句、参数、RowBounds 等完全一致),直接返回缓存结果。 - 若缓存未命中,则执行数据库查询,将结果存入一级缓存后返回。
- 执行
- 失效场景:
- 同一 SqlSession 内执行
insert
/update
/delete
操作(会自动清空一级缓存,避免数据不一致)。 - 调用
SqlSession.clearCache()
手动清空缓存。 - SqlSession 关闭(
close()
)后,缓存销毁。
- 同一 SqlSession 内执行
2. 示例验证
try (SqlSession session = factory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);// 第一次查询:缓存未命中,执行 SQLUser user1 = mapper.getUserById(1);// 第二次查询:相同 SqlSession,缓存命中,不执行 SQLUser user2 = mapper.getUserById(1);System.out.println(user1 == user2); // true(同一对象,来自缓存)
}
二、二级缓存(Second Level Cache)
二级缓存是Mapper 接口级别的缓存,多个 SqlSession 共享同一个 Mapper 的缓存,作用范围比一级缓存更广。
1. 工作原理
- 缓存范围:绑定到 Mapper 接口(即映射文件的
namespace
),同一 Mapper 的所有 SqlSession 共享缓存。 - 触发时机:
- 当 SqlSession 关闭(
close()
)或提交(commit()
)时,其一级缓存中的数据会被同步到二级缓存。 - 其他 SqlSession 查询同一 Mapper 的相同语句时,会先查询二级缓存,未命中再查一级缓存和数据库。
- 当 SqlSession 关闭(
- 失效场景:
- 同一 Mapper 执行
insert
/update
/delete
操作并提交后,二级缓存会被清空。 - 手动配置缓存过期时间或大小限制(如超出容量时 LRU 淘汰)。
- 同一 Mapper 执行
2. 开启与配置二级缓存
二级缓存默认关闭,需手动开启:
步骤 1:全局配置开启(默认已开启,可省略)
在 mybatis-config.xml
中确保:
<settings><!-- 开启二级缓存(默认值为 true) --><setting name="cacheEnabled" value="true"/>
</settings>
步骤 2:在 Mapper 映射文件中声明缓存
在需要开启二级缓存的映射文件中添加 <cache/>
标签:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper"><!-- 开启二级缓存 --><cache eviction="LRU" <!-- 缓存淘汰策略:LRU(默认,最近最少使用) -->flushInterval="60000" <!-- 自动刷新时间(毫秒),60秒 -->size="1024" <!-- 最大缓存对象数 -->readOnly="false"/> <!-- 是否只读(false 表示可读写,默认) -->
</mapper>
步骤 3:实体类实现序列化(可选但推荐)
若缓存需要序列化(如分布式场景),实体类需实现 Serializable
接口:
public class User implements Serializable {private Integer id;private String name;// ... getter/setter
}
3. 示例验证
// 第一个 SqlSession
try (SqlSession session1 = factory.openSession()) {UserMapper mapper1 = session1.getMapper(UserMapper.class);User user1 = mapper1.getUserById(1); // 查库,存入一级缓存
} // session1 关闭,数据同步到二级缓存// 第二个 SqlSession
try (SqlSession session2 = factory.openSession()) {UserMapper mapper2 = session2.getMapper(UserMapper.class);User user2 = mapper2.getUserById(1); // 命中二级缓存,不查库
}
三、缓存相关配置
1. 缓存淘汰策略(eviction
)
LRU
(默认):最近最少使用,移除最长时间未被使用的对象。FIFO
:先进先出,按对象进入缓存的顺序移除。SOFT
:软引用,基于 JVM 软引用策略,内存不足时移除。WEAK
:弱引用,JVM 垃圾回收时优先移除。
2. 禁用指定语句的缓存
通过 useCache
和 flushCache
属性控制单个语句的缓存行为:
<!-- 禁用当前查询的二级缓存(一级缓存仍生效) -->
<select id="getUserById" resultType="User" useCache="false">SELECT * FROM user WHERE id = #{id}
</select><!-- 执行后清空二级缓存(适用于增删改) -->
<update id="updateUser" flushCache="true">UPDATE user SET name = #{name} WHERE id = #{id}
</update>
四、一级缓存与二级缓存的对比
维度 | 一级缓存(SqlSession 级) | 二级缓存(Mapper 级) |
---|---|---|
作用范围 | 单个 SqlSession | 多个 SqlSession 共享(同一 Mapper) |
默认状态 | 自动开启,无法关闭 | 需手动开启 |
数据存储 | 内存对象(未序列化) | 可序列化对象(默认内存,可扩展) |
失效时机 | 增删改、SqlSession 关闭/清空 | 增删改提交后、缓存策略淘汰 |
适用场景 | 单会话内的重复查询 | 多会话共享的高频查询 |
五、缓存的局限性与最佳实践
-
局限性:
- 缓存数据可能与数据库不一致(尤其是高频更新的表)。
- 二级缓存对关联查询支持较差(可能导致关联数据过时)。
- 分布式环境下,本地二级缓存无法共享(需集成 Redis 等分布式缓存)。
-
最佳实践:
- 读多写少的表(如字典表)适合开启二级缓存。
- 高频更新的表(如订单表)建议禁用二级缓存。
- 关联查询优先使用 SQL JOIN 而非缓存(避免数据不一致)。
- 分布式系统中,用 Redis 等替代 MyBatis 内置二级缓存,实现跨服务缓存共享。
总结
MyBatis 缓存机制通过一级缓存(会话内复用)和二级缓存(跨会话复用)减少数据库访问,核心是权衡缓存命中率与数据一致性。实际开发中,应根据业务特点选择缓存策略:简单场景依赖一级缓存即可,多会话共享场景合理配置二级缓存,分布式场景需集成外部缓存中间件。
6.2、二级缓存属性
MyBatis 的二级缓存通过 <cache>
标签配置,其属性用于控制缓存的行为(如淘汰策略、生命周期、读写规则等)。理解这些属性是优化二级缓存的关键,以下是详细解析:
一、<cache>
标签的核心属性
<cache>
标签定义在 Mapper 映射文件中,属性如下(均为可选,有默认值):
属性 | 作用说明 | 可选值/默认值 |
---|---|---|
eviction | 缓存淘汰策略(当缓存满时,如何移除旧数据) | 默认 LRU ;可选 FIFO 、SOFT 、WEAK |
flushInterval | 缓存自动刷新时间(毫秒),超时后自动清空缓存 | 默认 null (不自动刷新,仅在增删改时手动刷新) |
size | 缓存可存储的最大对象数量(超出后触发淘汰策略) | 默认 1024 (需根据内存大小调整) |
readOnly | 缓存对象是否只读 | 默认 false (可读写);true 表示只读 |
type | 自定义缓存实现类(默认使用 MyBatis 内置缓存) | 需实现 org.apache.ibatis.cache.Cache 接口,如集成 Redis 时指定自定义实现类 |
二、属性详解与使用场景
1. eviction
:缓存淘汰策略
当缓存对象数量达到 size
限制时,触发淘汰策略移除旧数据。
策略 | 含义(淘汰规则) | 适用场景 |
---|---|---|
LRU | 最近最少使用:移除最长时间未被访问的对象(默认策略) | 大部分场景,优先保留高频访问数据 |
FIFO | 先进先出:按对象进入缓存的顺序移除最早的对象 | 数据访问顺序固定的场景(如日志查询) |
SOFT | 软引用:基于 JVM 软引用机制,内存不足时才移除对象(依赖 JVM 垃圾回收) | 内存敏感场景,允许缓存临时溢出 |
WEAK | 弱引用:基于 JVM 弱引用机制,垃圾回收时立即移除对象(存活时间最短) | 缓存数据可随时重建的场景(如实时性要求高的数据) |
示例:使用 FIFO 策略
<cache eviction="FIFO"/>
2. flushInterval
:自动刷新时间
设置缓存的过期时间(毫秒),超时后缓存自动清空,避免数据长期未更新导致的不一致。
- 默认值:
null
(不自动刷新,仅在执行insert
/update
/delete
并提交后手动刷新)。 - 使用场景:数据更新频率较低但有周期性(如每小时更新一次的统计数据)。
示例:设置 30 分钟自动刷新
<cache flushInterval="1800000"/> <!-- 30分钟 = 30*60*1000 = 1800000毫秒 -->
3. size
:最大缓存对象数量
控制缓存可存储的对象总数,超出后触发 eviction
策略淘汰旧数据。
- 默认值:
1024
(约 1000 个对象)。 - 注意:
- 数值过大会占用过多内存,可能导致 OOM;
- 数值过小会频繁触发淘汰,降低缓存命中率。
- 建议:根据业务数据量调整(如高频查询的表可设为
5000
,低频查询设为500
)。
示例:设置最大缓存 2000 个对象
<cache size="2000"/>
4. readOnly
:缓存读写权限
控制缓存对象是否允许修改,影响缓存的存储方式和性能。
值 | 含义 | 性能 | 适用场景 |
---|---|---|---|
false | 可读写(默认):缓存存储对象的副本(序列化后反序列化生成新对象) | 较低 | 需要修改缓存对象的场景(避免线程安全问题) |
true | 只读:缓存存储对象的引用(所有线程共享同一个对象) | 较高 | 缓存对象无需修改的场景(如字典表、静态数据) |
注意:
readOnly="true"
时,实体类无需实现Serializable
(因为不序列化);readOnly="false"
时,实体类必须实现Serializable
(因为缓存会序列化存储副本)。
示例:设置为只读缓存(适合静态数据)
<cache readOnly="true"/>
5. type
:自定义缓存实现
MyBatis 内置缓存是本地内存缓存(仅单进程有效),分布式系统中需集成 Redis、Memcached 等分布式缓存,此时通过 type
指定自定义缓存实现类。
- 要求:自定义类必须实现
org.apache.ibatis.cache.Cache
接口。 - 示例:集成 Redis 缓存(假设已实现
RedisCache
类)
<cache type="com.example.cache.RedisCache"/>
RedisCache
需实现 Cache
接口的核心方法(getObject
、putObject
、removeObject
等),实现缓存数据的 Redis 存储与读取。
三、二级缓存的辅助配置(语句级控制)
除了 <cache>
标签的全局配置,还可在单个 SQL 语句中通过属性控制缓存行为:
1. useCache
:是否使用二级缓存
- 默认为
true
(查询语句使用二级缓存)。 - 设为
false
可禁用当前查询的二级缓存(一级缓存仍生效)。
示例:禁用某查询的二级缓存
<select id="getUserById" resultType="User" useCache="false">SELECT * FROM user WHERE id = #{id}
</select>
2. flushCache
:是否刷新缓存
- 默认为
false
(查询不刷新缓存)。 - 增删改语句默认
flushCache="true"
(执行后清空二级缓存,避免数据不一致)。 - 可手动设置查询语句
flushCache="true"
(每次查询前清空缓存,强制查库)。
示例:查询前清空缓存(强制实时数据)
<select id="getRealTimeData" resultType="Data" flushCache="true">SELECT * FROM real_time_data
</select>
四、二级缓存配置的最佳实践
- 读多写少表优先开启:如字典表、配置表等更新频率低的表,缓存命中率高。
- 高频更新表禁用:如订单表、用户表等,频繁更新会导致缓存频繁失效,反而增加开销。
- 合理设置
size
和eviction
:- 高频访问数据用
LRU
策略,size
设为实际查询量的 1.5-2 倍; - 顺序访问数据用
FIFO
策略(如按时间排序的日志)。
- 高频访问数据用
- 分布式系统必用
type
集成外部缓存:避免本地缓存导致的多节点数据不一致。 - 复杂关联查询慎用:关联表更新时可能导致缓存数据过时,建议用 SQL JOIN 替代缓存。
总结
二级缓存的属性通过控制淘汰策略、生命周期、读写规则等,实现了对缓存行为的精细化管理。核心是根据业务特点(数据更新频率、访问模式、是否分布式)选择合适的配置,在缓存命中率和数据一致性之间找到平衡。对于简单场景,使用默认配置即可;复杂场景需结合自定义缓存实现(如 Redis)提升性能和可靠性。
6.3、以Ehcache为例介绍MyBatis如何整合第三方缓存
MyBatis 可以通过自定义缓存实现类整合第三方缓存框架(如 EhEhcache、Redis 等),替代其默认的本地内存缓存。以 Ehcache 为例,整合步骤如下:
一、准备工作:引入依赖
需要引入 MyBatis、Ehcache 及相关整合依赖。以 Maven 为例,在 pom.xml
中添加:
<!-- MyBatis 核心依赖 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.10</version>
</dependency><!-- Ehcache 核心依赖 -->
<dependency><groupId>org.ehcache</groupId><artifactId>ehcache</artifactId><version>3.10.0</version>
</dependency><!-- MyBatis-Ehcache 整合适配包(可选,简化整合) -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.2.2</version>
</dependency>
二、步骤 1:配置 Ehcache 缓存(ehcache.xml)
在 src/main/resources
下创建 Ehcache 配置文件 ehcache.xml
,定义缓存策略(如过期时间、最大容量等):
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"updateCheck="false"><!-- 磁盘缓存位置 --><diskStore path="java.io.tmpdir/ehcache"/><!-- 默认缓存配置(所有缓存的通用设置) --><defaultCachemaxElementsInMemory="10000" <!-- 内存中最大缓存对象数 -->eternal="false" <!-- 是否永久保存(true 则忽略过期时间) -->timeToIdleSeconds="600" <!-- 对象空闲多久后过期(秒) -->timeToLiveSeconds="3600" <!-- 对象存活多久后过期(秒) -->overflowToDisk="true" <!-- 内存不足时是否写入磁盘 -->diskPersistent="false" <!-- 重启时是否保留磁盘缓存 -->diskExpiryThreadIntervalSeconds="60"/> <!-- 磁盘缓存清理线程间隔(秒) --><!-- 针对 UserMapper 的专用缓存配置(可选,覆盖默认配置) --><cache name="com.example.mapper.UserMapper" <!-- 需与 Mapper 接口的全类名一致 -->maxElementsInMemory="5000"timeToIdleSeconds="300"timeToLiveSeconds="1800"/>
</ehcache>
三、步骤 2:自定义 MyBatis 缓存实现类(适配 Ehcache)
MyBatis 要求第三方缓存实现 org.apache.ibatis.cache.Cache
接口。由于 mybatis-ehcache
已提供现成实现类 EhcacheCache
,可直接使用(无需重复开发)。
若需自定义,核心实现逻辑如下(了解即可):
MyBatis与Ehcache的适配类:
import org.apache.ibatis.cache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class EhcacheAdapter implements Cache {private final String id; // 缓存ID(对应Mapper接口的全类名)private final Ehcache ehcache; // Ehcache缓存实例private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 构造方法:MyBatis会自动传入当前Mapper的namespace作为idpublic EhcacheAdapter(String id) {this.id = id;// 初始化Ehcache:从配置文件加载缓存管理器CacheManager cacheManager = CacheManager.create(this.getClass().getResourceAsStream("/ehcache.xml"));// 获取缓存实例(若ehcache.xml中定义了对应name的缓存,则使用其配置;否则用默认配置)this.ehcache = cacheManager.getCache(id);if (this.ehcache == null) {// 若缓存未定义,则创建一个默认缓存cacheManager.addCache(id);this.ehcache = cacheManager.getCache(id);}}@Overridepublic String getId() {return id;}// 存入缓存:key为MyBatis生成的缓存键,value为查询结果对象@Overridepublic void putObject(Object key, Object value) {ehcache.put(new Element(key, value));}// 获取缓存:根据key查询缓存@Overridepublic Object getObject(Object key) {Element element = ehcache.get(key);return element != null ? element.getObjectValue() : null;}// 移除缓存:根据key删除缓存@Overridepublic Object removeObject(Object key) {Object value = getObject(key);ehcache.remove(key);return value;}// 清空缓存:执行增删改时触发@Overridepublic void clear() {ehcache.removeAll();}// 获取缓存大小@Overridepublic int getSize() {return ehcache.getSize();}@Overridepublic ReadWriteLock getReadWriteLock() {return readWriteLock; // 提供读写锁,保证线程安全}
}
四、步骤 3:在 MyBatis 映射文件中配置二级缓存
在需要使用缓存的 Mapper 映射文件(如 UserMapper.xml
)中,通过 <cache>
标签的 type
属性指定缓存实现类(使用 mybatis-ehcache
提供的 EhcacheCache
或自定义的 EhcacheAdapter
)。
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper"><!-- 配置二级缓存,使用Ehcache实现 --><cache type="org.mybatis.caches.ehcache.EhcacheCache" <!-- 若用自定义类则改为EhcacheAdapter的全类名 -->eviction="LRU" <!-- 缓存淘汰策略(Ehcache配置文件中定义的策略优先级更高) -->flushInterval="60000" <!-- 自动刷新时间(毫秒) -->size="1024" readOnly="false"/><!-- 查询语句:默认使用二级缓存(useCache="true") --><select id="getUserById" resultType="User">SELECT * FROM user WHERE id = #{id}</select>
</mapper>
- 若
ehcache.xml
中已为com.example.mapper.UserMapper
定义了专用缓存配置,则会覆盖<cache>
标签中的属性(如size
、flushInterval
)。
五、步骤 4:验证缓存是否生效
通过测试代码验证缓存是否正常工作:
public class CacheTest {public static void main(String[] args) throws IOException {// 初始化MyBatisInputStream is = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);// 第一次查询:缓存未命中,执行SQLtry (SqlSession session1 = factory.openSession()) {UserMapper mapper1 = session1.getMapper(UserMapper.class);User user1 = mapper1.getUserById(1);System.out.println("第一次查询:" + user1);} // session1关闭,数据写入Ehcache// 第二次查询:缓存命中,不执行SQLtry (SqlSession session2 = factory.openSession()) {UserMapper mapper2 = session2.getMapper(UserMapper.class);User user2 = mapper2.getUserById(1);System.out.println("第二次查询:" + user2);}}
}
- 若控制台只打印一次 SQL 执行日志,说明缓存生效。
六、核心原理
- MyBatis 初始化时,会为每个 Mapper 接口创建对应的缓存实例(
EhcacheCache
或自定义实现类)。 - 执行查询时,MyBatis 先调用缓存实现类的
getObject(key)
方法查询缓存,命中则直接返回。 - 缓存未命中时,执行数据库查询,再调用
putObject(key, value)
方法将结果存入 Ehcache。 - 执行增删改操作并提交后,MyBatis 调用
clear()
方法清空当前 Mapper 的缓存,避免数据不一致。
七、注意事项
- 实体类需序列化:若
readOnly="false"
(可读写缓存),实体类必须实现Serializable
接口(Ehcache 会序列化存储对象)。 - 缓存配置优先级:
ehcache.xml
中定义的缓存策略优先级高于 MyBatis 映射文件的<cache>
标签属性。 - 分布式场景:Ehcache 是本地缓存,多节点部署时需使用
Terracotta
扩展实现分布式缓存,或直接替换为 Redis 等分布式缓存框架。 - 缓存粒度控制:通过
<select>
标签的useCache="false"
禁用特定查询的缓存,通过flushCache="true"
强制刷新缓存。
通过以上步骤,MyBatis 成功整合 Ehcache 作为二级缓存,利用 Ehcache 更完善的缓存管理能力(如磁盘持久化、过期策略)提升系统性能。
6.4、ehcache.xml的主要标签和属性
ehcache.xml
是 Ehcache 缓存框架的核心配置文件,用于定义缓存的存储策略、过期规则、持久化方式等。以下是其主要标签和属性的详细解析:
一、根标签 <ehcache>
整个配置文件的根标签,包含缓存的全局设置和命名空间声明。
属性:
xmlns:xsi
:XML 命名空间声明(固定值http://www.w3.org/2001/XMLSchema-instance
)。xsi:noNamespaceSchemaLocation
:指定 XML schema 位置(固定值http://ehcache.org/ehcache.xsd
)。updateCheck
:是否自动检查 Ehcache 版本更新(默认true
,建议设为false
关闭)。
示例:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"updateCheck="false"><!-- 子标签配置 -->
</ehcache>
二、<diskStore>
:磁盘缓存路径
定义缓存对象溢出到磁盘时的存储目录(仅当内存不足且 overflowToDisk="true"
时生效)。
属性:
path
:磁盘存储路径,支持以下值:java.io.tmpdir
:系统临时目录(推荐,自动适配不同操作系统)。- 绝对路径:如
D:/ehcache/disk
(Windows)或/var/ehcache/disk
(Linux)。
示例:
<diskStore path="java.io.tmpdir/ehcache"/> <!-- 临时目录下的 ehcache 子目录 -->
三、<defaultCache>
:默认缓存配置
所有未单独定义的缓存(即 <cache>
标签)都会继承此配置,是全局默认策略。
核心属性(均为可选,有默认值):
属性 | 作用说明 | 默认值 |
---|---|---|
maxElementsInMemory | 内存中最大缓存对象数量(超出后触发淘汰策略或写入磁盘) | 10000 |
eternal | 是否永久缓存(true 则忽略 timeToIdleSeconds 和 timeToLiveSeconds ) | false |
timeToIdleSeconds | 对象空闲时间(秒):多久未被访问则过期 | 120 |
timeToLiveSeconds | 对象存活时间(秒):从创建到过期的总时长 | 120 |
overflowToDisk | 内存不足时是否溢出到磁盘 | true |
diskPersistent | 重启 JVM 后是否保留磁盘缓存(需配合 overflowToDisk="true" ) | false |
diskExpiryThreadIntervalSeconds | 磁盘缓存清理线程的执行间隔(秒) | 120 |
memoryStoreEvictionPolicy | 内存缓存淘汰策略(当 maxElementsInMemory 满时) | LRU |
示例:
<defaultCachemaxElementsInMemory="5000"eternal="false"timeToIdleSeconds="300" <!-- 5分钟未访问则过期 -->timeToLiveSeconds="1800" <!-- 30分钟后强制过期 -->overflowToDisk="true"diskPersistent="false"diskExpiryThreadIntervalSeconds="60"memoryStoreEvictionPolicy="LRU"/>
四、<cache>
:自定义缓存配置
为特定缓存(如 MyBatis 中某 Mapper 接口)定义独立配置,覆盖 <defaultCache>
。
核心属性:
- 继承
<defaultCache>
的所有属性。 name
:缓存唯一标识(必填),在 MyBatis 中需与 Mapper 接口的全类名一致(如com.example.mapper.UserMapper
)。
示例:
<!-- 为 UserMapper 定义专用缓存 -->
<cache name="com.example.mapper.UserMapper"maxElementsInMemory="2000" <!-- 比默认值小,因为用户数据访问更频繁 -->timeToIdleSeconds="600" <!-- 10分钟未访问过期 -->timeToLiveSeconds="3600" <!-- 1小时强制过期 -->memoryStoreEvictionPolicy="FIFO"/> <!-- 按访问顺序淘汰 -->
五、<cacheManagerEventListenerFactory>
:缓存管理器监听器
用于监听缓存管理器的生命周期事件(如初始化、销毁),可自定义扩展。
属性:
class
:实现CacheManagerEventListenerFactory
接口的类全路径。
示例:
<cacheManagerEventListenerFactory class="com.example.cache.MyCacheManagerListener"/>
六、<bootstrapCacheLoaderFactory>
:缓存预热加载器
在缓存管理器初始化时,自动加载预设数据到缓存(如字典表初始化数据)。
属性:
class
:实现BootstrapCacheLoaderFactory
接口的类全路径。properties
:传递给加载器的参数(可选)。
示例:
<bootstrapCacheLoaderFactory class="com.example.cache.MyBootstrapLoader"><properties><property name="initData" value="true"/></properties>
</bootstrapCacheLoaderFactory>
七、关键属性详解
1. 过期时间相关
-
eternal
:true
:缓存对象永不过期(忽略timeToIdle
和timeToLive
),适合静态数据(如省份列表)。false
:按timeToIdle
和timeToLive
过期,适合动态数据。
-
timeToIdleSeconds
vstimeToLiveSeconds
:- 两者取最小值生效(即只要一个条件满足就过期)。
- 例:
timeToIdle=300
(5分钟未访问)和timeToLive=1800
(30分钟存活),若对象6分钟未访问,即使未到30分钟,也会因timeToIdle
过期。
2. 内存淘汰策略(memoryStoreEvictionPolicy
)
当内存中缓存对象数达到 maxElementsInMemory
时,触发以下策略:
LRU
(默认):最近最少使用,移除最长时间未被访问的对象(推荐大多数场景)。FIFO
:先进先出,按对象进入缓存的顺序移除最早的对象(适合顺序访问的数据)。LFU
:最不经常使用,移除访问次数最少的对象(适合统计访问频率的场景)。
3. 磁盘存储相关
overflowToDisk
:内存不足时是否写入磁盘(true
可避免 OOM,但性能略低)。diskPersistent
:重启后是否保留磁盘缓存(true
需谨慎,可能导致数据不一致)。
八、配置优先级
- 若
<cache>
标签未定义某属性,则继承<defaultCache>
的对应属性。 - 若
<cache>
标签定义了某属性,则覆盖<defaultCache>
的对应属性。 - MyBatis 映射文件中
<cache>
标签的属性(如size
)会被ehcache.xml
中的配置覆盖(Ehcache 配置优先级更高)。
总结
ehcache.xml
通过 <defaultCache>
和 <cache>
标签控制缓存行为,核心是根据业务数据特点(更新频率、访问模式、数据量)配置:
- 静态数据:
eternal="true"
,永久缓存。 - 高频访问动态数据:
timeToIdle
设短(如5分钟),maxElementsInMemory
设大。 - 低频访问数据:
overflowToDisk="true"
,利用磁盘存储节省内存。
合理配置可最大化发挥 Ehcache 的性能,减少数据库访问压力。
7、MyBatis的注解开发
MyBatis 注解开发是通过 Java 注解替代传统的 XML 映射文件,实现 SQL 语句与 Mapper 接口的绑定,简化配置流程。注解开发适用于 SQL 逻辑简单的场景,复杂场景(如动态 SQL、多表关联)仍推荐 XML 配置。以下是注解开发的详细解析:
一、核心注解与基本使用
MyBatis 提供了一系列注解对应 XML 标签,核心注解如下:
注解 | 作用 | 对应 XML 标签 |
---|---|---|
@Select | 定义查询语句 | <select> |
@Insert | 定义插入语句 | <insert> |
@Update | 定义更新语句 | <update> |
@Delete | 定义删除语句 | <delete> |
@Results | 定义结果映射(替代 resultMap ) | <resultMap> |
@Result | 单个字段映射(替代 <result> ) | <result> /<id> |
@Param | 定义参数名称 | - |
@One | 一对一关联查询 | <association> |
@Many | 一对多关联查询 | <collection> |
@SelectProvider | 动态 SQL 查询(替代动态标签) | 动态 <select> |
1. 基本 CRUD 操作示例
步骤 1:创建 Mapper 接口,添加注解
public interface UserMapper {// 查询:根据 ID 查询用户@Select("SELECT id, name, age FROM user WHERE id = #{id}")User getUserById(Integer id);// 插入:新增用户(返回自增 ID)@Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")@Options(useGeneratedKeys = true, keyProperty = "id") // 获取自增主键int addUser(User user);// 更新:根据 ID 更新用户@Update("UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}")int updateUser(User user);// 删除:根据 ID 删除用户@Delete("DELETE FROM user WHERE id = #{id}")int deleteUser(Integer id);
}
步骤 2:MyBatis 配置文件中注册 Mapper 接口
在 mybatis-config.xml
中指定接口路径:
<mappers><!-- 方式 1:直接注册接口 --><mapper class="com.example.mapper.UserMapper"/><!-- 方式 2:扫描包下所有接口(推荐) --><package name="com.example.mapper"/>
</mappers>
2. 关键注解说明
-
@Options
:辅助配置(如获取自增主键、超时时间等):useGeneratedKeys = true
:开启自增主键获取(同 XML 的useGeneratedKeys
)。keyProperty = "id"
:指定主键注入到实体类的id
属性(同 XML 的keyProperty
)。timeout = 30
:设置查询超时时间(秒)。
-
@Param
:多参数传递时指定参数名(避免param1/param2
命名):// 多参数查询 @Select("SELECT * FROM user WHERE name = #{name} AND age = #{age}") User getUserByNameAndAge(@Param("name") String name, @Param("age") int age);
二、结果映射(@Results
与 @Result
)
当数据库字段名与实体属性名不一致时,需通过 @Results
和 @Result
手动映射(替代 XML 的 resultMap
)。
示例:字段名与属性名映射
public interface UserMapper {// 定义结果映射(id 为映射唯一标识,方便复用)@Results(id = "userResultMap", value = {@Result(column = "user_id", property = "id", id = true), // id=true 表示主键@Result(column = "user_name", property = "name"),@Result(column = "user_age", property = "age")})@Select("SELECT user_id, user_name, user_age FROM user WHERE user_id = #{id}")User getUserById(Integer id);// 复用已定义的结果映射@ResultMap("userResultMap") // 引用上面定义的 "userResultMap"@Select("SELECT user_id, user_name, user_age FROM user")List<User> getAllUsers();
}
@Results
的id
属性:给结果映射命名,通过@ResultMap
复用(类似 XML 中resultMap
的id
)。@Result
的id = true
:标记为主键映射(优化性能)。
三、关联查询(@One
与 @Many
)
注解开发支持一对一和一对多关联查询,通过 @One
和 @Many
实现(替代 XML 的 <association>
和 <collection>
)。
1. 一对一关联(@One
)
场景:用户(User)与身份证(Card)一对一关联。
public interface UserMapper {@Results(value = {@Result(column = "id", property = "id", id = true),@Result(column = "name", property = "name"),// 一对一关联:通过 user 的 card_id 查询 card@Result(column = "card_id", // 传递给子查询的参数(用户表的 card_id)property = "card", // User 类中的 Card 属性one = @One(select = "com.example.mapper.CardMapper.getCardById") // 子查询方法)})@Select("SELECT id, name, card_id FROM user WHERE id = #{id}")User getUserWithCard(Integer id);
}// CardMapper 接口(子查询)
public interface CardMapper {@Select("SELECT id, card_no, address FROM card WHERE id = #{id}")Card getCardById(Integer id);
}
@One
的select
属性:指定子查询的 Mapper 方法全路径。column
属性:传递主查询的字段值给子查询。
2. 一对多关联(@Many
)
场景:用户(User)与订单(Order)一对多关联。
public interface UserMapper {@Results(value = {@Result(column = "id", property = "id", id = true),@Result(column = "name", property = "name"),// 一对多关联:通过 user 的 id 查询订单@Result(column = "id", // 传递用户 ID 给子查询property = "orders", // User 类中的 List<Order> 属性many = @Many(select = "com.example.mapper.OrderMapper.getOrdersByUserId") // 子查询方法)})@Select("SELECT id, name FROM user WHERE id = #{id}")User getUserWithOrders(Integer id);
}// OrderMapper 接口(子查询)
public interface OrderMapper {@Select("SELECT id, order_no, user_id FROM `order` WHERE user_id = #{userId}")List<Order> getOrdersByUserId(Integer userId);
}
@Many
用于一对多关联,ofType
由返回值类型自动推断(需保证子查询返回List<实体类>
)。
四、动态 SQL(@SelectProvider
等)
注解开发中动态 SQL 通过 Provider 注解 实现,需创建 Provider 类生成动态 SQL 字符串。
常用 Provider 注解:
@SelectProvider
:动态查询 SQL@InsertProvider
:动态插入 SQL@UpdateProvider
:动态更新 SQL@DeleteProvider
:动态删除 SQL
示例:动态条件查询
步骤 1:创建 Provider 类,生成动态 SQL
public class UserProvider {// 动态生成查询 SQL(根据 name 和 age 条件)public String getUserByCondition(User user) {SQL sql = new SQL().SELECT("id, name, age").FROM("user");// 条件判断:name 非空则添加 LIKE 条件if (user.getName() != null && !user.getName().isEmpty()) {sql.WHERE("name LIKE CONCAT('%', #{name}, '%')");}// 条件判断:age 非空则添加 > 条件if (user.getAge() != null) {sql.WHERE("age > #{age}");}return sql.toString(); // 生成最终 SQL}
}
步骤 2:在 Mapper 接口中引用 Provider
public interface UserMapper {// 引用 Provider 类的方法生成动态 SQL@SelectProvider(type = UserProvider.class, method = "getUserByCondition")List<User> getUserByCondition(User user);
}
type
:指定 Provider 类的 Class 对象。method
:指定 Provider 类中生成 SQL 的方法名。SQL
是 MyBatis 提供的工具类,简化 SQL 拼接(避免手动拼接字符串的错误)。
五、注解开发 vs XML 开发
维度 | 注解开发 | XML 开发 |
---|---|---|
适用场景 | 简单 SQL(CRUD、单表查询) | 复杂 SQL(动态 SQL、多表关联) |
可读性 | 简单 SQL 直观,复杂 SQL 混乱 | 结构化强,复杂 SQL 易维护 |
动态 SQL | 需通过 Provider 类实现,较繁琐 | 通过 <if> /<foreach> 等标签,灵活简洁 |
复用性 | 结果映射需通过 @ResultMap 复用 | resultMap 可直接引用,支持继承 |
维护性 | 改 SQL 需修改接口代码,需重新编译 | 改 XML 无需重新编译,热部署友好 |
六、最佳实践
-
混合使用:简单操作(如单表 CRUD)用注解,复杂操作(如动态多表查询)用 XML,MyBatis 支持同一接口同时存在注解和 XML 配置(XML 优先级更高)。
-
结果映射复用:通过
@Results(id)
+@ResultMap
复用结果映射,减少重复代码。 -
动态 SQL 谨慎使用:复杂动态 SQL 优先用 XML,避免 Provider 类逻辑臃肿。
-
参数传递规范:多参数必用
@Param
命名,避免param1
等模糊命名。
总结
MyBatis 注解开发简化了简单场景的配置,无需编写 XML 文件,开发效率更高。但对于复杂 SQL 和动态逻辑,XML 配置仍更具优势。实际开发中,应根据 SQL 复杂度灵活选择,必要时混合使用两种方式,兼顾效率与可维护性。
8.1、MyBatis的分页插件PageHelper
PageHelper 是 MyBatis 最常用的分页插件,它通过拦截 SQL 语句自动添加分页条件(如 LIMIT
或 ROW_NUMBER()
),简化分页实现。以下是其详细用法和原理:
一、PageHelper 核心优势
- 使用简单:只需在查询前调用分页方法,无需手动编写分页 SQL。
- 支持多数据库:自动适配 MySQL、Oracle、SQL Server 等不同数据库的分页语法。
- 功能丰富:支持排序、分页合理化、统计总数等。
- 低侵入性:无需修改 Mapper 接口和 SQL 语句,仅通过代码层面控制。
二、快速入门
1. 引入依赖(Maven)
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId> <!-- Spring Boot 环境 --><version>1.4.6</version> <!-- 最新版本可到 Maven 仓库查询 -->
</dependency><!-- 非 Spring Boot 环境需引入核心包和 MyBatis 依赖 -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.3.3</version>
</dependency>
2. 配置分页插件
Spring Boot 环境(推荐):在 application.yml
中配置:
pagehelper:helper-dialect: mysql # 指定数据库方言(自动检测可省略)reasonable: true # 分页合理化(默认 false)support-methods-arguments: true # 支持通过 Mapper 接口参数传递分页参数params: count=countSql # 用于从对象中根据属性名取值
非 Spring Boot 环境:在 MyBatis 配置文件中添加插件:
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"><!-- 数据库方言 --><property name="helperDialect" value="mysql"/><!-- 分页合理化 --><property name="reasonable" value="true"/></plugin>
</plugins>
3. 基本使用步骤
步骤 1:在查询前调用 PageHelper.startPage()
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public PageInfo<User> getUserByPage(int pageNum, int pageSize) {// 1. 开启分页(pageNum:页码,pageSize:每页条数)PageHelper.startPage(pageNum, pageSize);// 2. 执行查询(无需修改 SQL,PageHelper 会自动拦截并添加分页条件)List<User> userList = userMapper.getAllUsers();// 3. 封装分页结果(PageInfo 包含分页详细信息)return new PageInfo<>(userList);}
}
步骤 2:Mapper 接口和 SQL(无需任何分页相关代码)
public interface UserMapper {// SQL 无需包含 LIMIT 等分页条件,PageHelper 会自动处理List<User> getAllUsers();
}
<select id="getAllUsers" resultType="User">SELECT id, name, age FROM user
</select>
4. 分页结果 PageInfo
详解
PageInfo
包含丰富的分页信息,常用属性:
PageInfo<User> pageInfo = getUserByPage(1, 10);
pageInfo.getPageNum(); // 当前页码(1)
pageInfo.getPageSize(); // 每页条数(10)
pageInfo.getTotal(); // 总记录数
pageInfo.getPages(); // 总页数
pageInfo.getList(); // 当前页数据列表
pageInfo.isHasNextPage(); // 是否有下一页
pageInfo.isHasPreviousPage(); // 是否有上一页
三、高级功能
1. 排序
在分页时指定排序字段和方向:
// 按 age 降序,name 升序
PageHelper.startPage(1, 10).setOrderBy("age DESC, name ASC");
List<User> userList = userMapper.getAllUsers();
2. 分页合理化
当 reasonable: true
时,自动处理不合理的页码:
- 若
pageNum < 1
,自动查询第 1 页。 - 若
pageNum > 总页数
,自动查询最后一页。
3. 只查询总数
如需单独获取总数(不查询数据):
Long total = PageHelper.count(() -> userMapper.getAllUsers());
4. 多参数分页
通过 @Param
传递分页参数(需开启 support-methods-arguments: true
):
// Mapper 接口
List<User> getUserByCondition(@Param("name") String name,@Param("pageNum") int pageNum,@Param("pageSize") int pageSize
);// 调用时无需手动 startPage,插件会自动识别 pageNum 和 pageSize 参数
List<User> userList = userMapper.getUserByCondition("张三", 1, 10);
5. 自定义分页查询
对于复杂查询,可通过 Page
对象手动处理:
// 手动创建 Page 对象
Page<User> page = new Page<>(1, 10);
// 执行查询并绑定到 Page
page.setRecords(userMapper.getUserByCondition(page, "张三"));
// 单独查询总数(可选)
page.setTotal(userMapper.countByCondition("张三"));
四、工作原理
- 拦截器机制:PageHelper 实现了 MyBatis 的
Interceptor
接口,会拦截Executor.query()
方法。 - SQL 改写:拦截到查询后,根据数据库方言自动在 SQL 后添加分页条件(如 MySQL 加
LIMIT
,Oracle 用ROW_NUMBER()
)。 - count 查询:自动生成并执行
COUNT(*)
语句获取总记录数(可通过PageHelper.startPage(pageNum, pageSize, false)
关闭)。 - 结果封装:将查询结果封装到
Page
对象,包含分页信息。
五、注意事项
-
线程安全:
PageHelper.startPage()
是线程安全的,通过ThreadLocal
存储分页参数。 -
只对紧随的第一个查询有效:
startPage()
仅对之后执行的第一个 MyBatis 映射方法生效,避免在中间插入其他查询。PageHelper.startPage(1, 10); userMapper.getAllUsers(); // 会分页 userMapper.getOtherData(); // 不会分页(不受前一个 startPage 影响)
-
避免在嵌套查询中使用:分页插件可能无法正确处理嵌套查询(如
<select>
中嵌套<include>
或子查询)。 -
数据库方言适配:确保
helper-dialect
与实际数据库一致,否则可能生成错误的分页 SQL。
六、常见问题
-
分页不生效:
- 检查插件是否正确配置(拦截器是否注册)。
- 确认
startPage()
调用在查询方法之前。 - 检查是否有多个查询语句,
startPage()
只对第一个有效。
-
总页数计算错误:
- 确保
COUNT
查询正确(可开启 SQL 日志查看生成的 count 语句)。 - 避免 SQL 中包含
DISTINCT
或复杂聚合函数导致 count 不准确。
- 确保
-
排序字段报错:
- 确保排序字段是数据库表实际存在的列名(而非实体类属性名)。
总结
PageHelper 极大简化了 MyBatis 的分页实现,通过拦截 SQL 自动添加分页条件,支持多数据库和丰富的分页功能。核心用法是在查询前调用 PageHelper.startPage()
,然后通过 PageInfo
获取分页详情。使用时需注意插件配置和线程安全,避免在复杂查询中滥用。
对于 Spring Boot 项目,推荐使用 pagehelper-spring-boot-starter
简化配置,开箱即用。
8.2、PageInfo源码
PageInfo
是 PageHelper 插件中用于封装分页查询结果的核心类,它在 Page
对象(存储当前页数据)的基础上,扩展了更多分页相关的辅助信息(如总页数、导航页码、前后页判断等),方便开发者快速获取分页详情。以下从源码角度剖析其核心实现。
一、类定义与核心属性
PageInfo
的源码位于 com.github.pagehelper.PageInfo
,其核心是通过构造方法接收查询结果(List
或 Page
),并计算分页相关参数。
核心属性(简化版):
public class PageInfo<T> implements Serializable {private static final long serialVersionUID = 1L;private int pageNum; // 当前页码private int pageSize; // 每页条数private long total; // 总记录数private int pages; // 总页数private List<T> list; // 当前页数据列表private boolean hasPreviousPage; // 是否有上一页private boolean hasNextPage; // 是否有下一页private boolean isFirstPage; // 是否为第一页private boolean isLastPage; // 是否为最后一页private int navigatePages; // 导航页码数(默认8)private int[] navigatepageNums; // 导航页码数组(如 [1,2,3,4,5])private int navigateFirstPage; // 导航栏第一页private int navigateLastPage; // 导航栏最后一页// 省略其他次要属性(如prePage、nextPage等)
}
- 核心属性说明:
pageNum
/pageSize
:分页查询时传入的页码和每页大小。total
:总记录数(从Page
对象中获取,或默认0)。pages
:总页数(通过total
和pageSize
计算得出)。list
:当前页的数据列表(实际查询结果)。- 导航相关属性(
navigatepageNums
等):用于前端分页导航展示(如页码控件)。
二、构造方法:初始化分页信息
PageInfo
有两个核心构造方法,分别处理普通 List
和 Page
对象(Page
是 PageHelper 中存储分页数据的类,继承自 ArrayList
)。
1. 接收 List
的构造方法(默认分页逻辑)
public PageInfo(List<T> list) {this(list, 8); // 默认导航页码数为8
}public PageInfo(List<T> list, int navigatePages) {if (list instanceof Page) { // 如果是Page对象,提取分页信息Page<T> page = (Page<T>) list;this.pageNum = page.getPageNum();this.pageSize = page.getPageSize();this.pages = page.getPages();this.list = page;this.total = page.getTotal();} else { // 普通List,默认视为第一页,每页大小为List.size()this.pageNum = 1;this.pageSize = list.size();this.pages = 1;this.list = list;this.total = list.size();}this.navigatePages = navigatePages;calcNavigatepageNums(); // 计算导航页码calcPage(); // 计算前后页、首尾页等判断
}
- 逻辑说明:
- 若传入的是
Page
对象(PageHelper 拦截查询后返回的结果),则直接从Page
中提取pageNum
、total
等核心参数。 - 若传入普通
List
(非分页查询结果),则默认视为“第1页”,总页数为1,总记录数为List
大小。 - 调用
calcNavigatepageNums()
和calcPage()
计算导航相关属性。
- 若传入的是
2. 接收 Page
的构造方法(内部调用上面的方法)
public PageInfo(Page<T> page) {this(page, 8);
}
三、核心计算方法
PageInfo
的核心逻辑集中在两个计算方法:calcNavigatepageNums()
(计算导航页码)和 calcPage()
(计算分页状态)。
1. calcNavigatepageNums()
:生成导航页码数组
该方法根据当前页码、总页数和导航页码数(navigatePages
),生成前端展示的页码数组(如 [2,3,4,5,6]
)。
private void calcNavigatepageNums() {if (pages <= navigatePages) { // 总页数 <= 导航页码数:显示所有页码navigatepageNums = new int[pages];for (int i = 0; i < pages; i++) {navigatepageNums[i] = i + 1;}} else { // 总页数 > 导航页码数:显示当前页为中心的连续页码navigatepageNums = new int[navigatePages];int startNum = pageNum - navigatePages / 2; // 计算起始页码int endNum = pageNum + navigatePages / 2; // 计算结束页码// 调整起始页码(避免小于1)if (startNum < 1) {startNum = 1;for (int i = 0; i < navigatePages; i++) {navigatepageNums[i] = startNum++;}} else if (endNum > pages) { // 调整结束页码(避免大于总页数)endNum = pages;for (int i = navigatePages - 1; i >= 0; i--) {navigatepageNums[i] = endNum--;}} else { // 正常情况:以当前页为中心for (int i = 0; i < navigatePages; i++) {navigatepageNums[i] = startNum++;}}}
}
- 示例:
若pageNum=5
,pages=10
,navigatePages=5
,则导航页码数组为[3,4,5,6,7]
。
2. calcPage()
:计算分页状态(前后页、首尾页)
private void calcPage() {// 计算导航栏首尾页if (navigatepageNums != null && navigatepageNums.length > 0) {navigateFirstPage = navigatepageNums[0];navigateLastPage = navigatepageNums[navigatepageNums.length - 1];// 是否为第一页isFirstPage = pageNum == 1;// 是否为最后一页isLastPage = pageNum == pages || pages == 0;// 是否有上一页hasPreviousPage = pageNum > 1;// 是否有下一页hasNextPage = pageNum < pages;}
}
- 逻辑说明:通过当前页码
pageNum
与总页数pages
的比较,判断是否为首页/末页,是否有前后页。
四、分页合理化处理
当 PageHelper 配置了 reasonable: true
(分页合理化)时,Page
对象会先对 pageNum
进行调整(如 pageNum < 1
时强制为1,pageNum > pages
时强制为 pages
),而 PageInfo
会基于调整后的 pageNum
计算分页信息。
- 例如:若总页数为5,传入
pageNum=10
,在合理化配置下,pageNum
会被调整为5,isLastPage
会被计算为true
。
五、与 Page
类的关系
Page
是 PageHelper 中直接存储分页数据的类(继承自 ArrayList
),包含 pageNum
、pageSize
、total
等基础分页信息。PageInfo
则是对 Page
的进一步封装,增加了导航页码、前后页判断等辅助信息,更适合前端展示。
- 简单说:
Page
是“原始分页数据”,PageInfo
是“增强版分页信息”。
六、常用方法解析
PageInfo
提供了一系列 getter 方法方便获取分页信息,核心方法如下:
方法 | 作用 | 实现逻辑 |
---|---|---|
getList() | 获取当前页数据 | 直接返回 list 属性 |
getTotal() | 获取总记录数 | 返回 total 属性 |
getPages() | 获取总页数 | 返回 pages 属性(由 total/pageSize 计算) |
isHasNextPage() | 是否有下一页 | 返回 hasNextPage 属性(pageNum < pages ) |
getNavigatepageNums() | 获取导航页码数组 | 返回 navigatepageNums 数组 |
总结
PageInfo
的设计核心是**“封装分页细节,简化前端使用”**:
- 通过构造方法接收查询结果(
List
或Page
),自动提取/计算基础分页参数(页码、总记录数等)。 - 通过
calcNavigatepageNums()
生成导航页码数组,方便前端渲染分页控件。 - 通过
calcPage()
计算分页状态(前后页、首尾页),减少前端逻辑判断。
理解 PageInfo
的源码有助于更好地使用其提供的分页信息,尤其在自定义分页导航展示时,可直接利用其计算好的 navigatepageNums
等属性,避免重复开发。
9、MyBatis逆向工程的使用
MyBatis 逆向工程(MyBatis Generator,简称 MBG)是 MyBatis 官方提供的代码生成工具,能够根据数据库表结构自动生成实体类(POJO)、Mapper 接口以及 Mapper XML 文件,极大减少重复开发工作。以下是其详细使用方法:
一、逆向工程核心作用
- 自动生成数据库表对应的实体类(包含字段、getter/setter、toString 等)。
- 生成 Mapper 接口(包含基本 CRUD 方法)。
- 生成 Mapper XML 文件(包含对应 SQL 语句)。
- 支持基本的条件查询(通过
Example
类实现)。
二、使用步骤(以 Maven 为例)
1. 引入依赖
在 pom.xml
中添加 MBG 依赖和数据库驱动:
<!-- MyBatis 逆向工程核心包 -->
<dependency><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-core</artifactId><version>1.4.1</version>
</dependency><!-- 数据库驱动(以 MySQL 为例) -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version>
</dependency><!-- MyBatis 核心包(可选,生成后使用) -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.10</version>
</dependency>
2. 创建配置文件(generatorConfig.xml
)
在 src/main/resources
下创建配置文件,定义数据库连接、生成规则等:
MyBatis逆向工程配置文件:
<?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><!-- 数据库驱动路径(本地 Maven 仓库路径) --><classPathEntry location="C:\Users\用户名\.m2\repository\mysql\mysql-connector-java\8.0.30\mysql-connector-java-8.0.30.jar"/><context id="DB2Tables" targetRuntime="MyBatis3"><!-- 关闭注释生成 --><commentGenerator><property name="suppressAllComments" value="true"/></commentGenerator><!-- 数据库连接配置 --><jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"connectionURL="jdbc:mysql://localhost:3306/mybatis_db?serverTimezone=UTC"userId="root"password="123456"></jdbcConnection><!-- 生成实体类时是否使用 BigDecimal 类型(默认 false) --><javaTypeResolver><property name="forceBigDecimals" value="false"/></javaTypeResolver><!-- 实体类生成配置 --><javaModelGenerator targetPackage="com.example.pojo" targetProject="src/main/java"><!-- 启用子包(如根据表名生成子包) --><property name="enableSubPackages" value="true"/><!-- 清理字段前后空格 --><property name="trimStrings" value="true"/></javaModelGenerator><!-- Mapper XML 生成配置 --><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- Mapper 接口生成配置 --><javaClientGenerator type="XMLMAPPER"targetPackage="com.example.mapper"targetProject="src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 数据库表与实体类的映射配置 --><!-- tableName:数据库表名;domainObjectName:生成的实体类名 --><table tableName="user" domainObjectName="User" /><table tableName="order" domainObjectName="Order" /><!-- 可选:指定字段映射规则(如忽略某些字段、自定义类型) --><!-- <table tableName="product"><columnOverride column="price" javaType="java.math.BigDecimal" /><ignoreColumn column="description" /> <!-- 忽略此字段 --></table>--></context>
</generatorConfiguration>
3. 配置文件关键参数说明
classPathEntry
:数据库驱动的本地路径(需替换为自己的 Maven 仓库路径)。jdbcConnection
:数据库连接信息(URL、用户名、密码)。javaModelGenerator
:targetPackage
:实体类存放的包路径(如com.example.pojo
)。targetProject
:生成到项目的src/main/java
目录。
sqlMapGenerator
:Mapper XML 文件的生成路径(如src/main/resources/mapper
)。javaClientGenerator
:type="XMLMAPPER"
:生成基于 XML 的 Mapper 接口。targetPackage
:Mapper 接口存放的包路径(如com.example.mapper
)。
table
:指定要生成的表,tableName
为数据库表名,domainObjectName
为对应实体类名。
4. 编写启动类执行生成
创建 Java 类触发逆向工程:MyBatis逆向工程启动类:
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.exception.InvalidConfigurationException;
import org.mybatis.generator.exception.XMLParserException;
import org.mybatis.generator.internal.DefaultShellCallback;import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;public class GeneratorRunner {public static void main(String[] args) throws Exception {List<String> warnings = new ArrayList<>();boolean overwrite = true; // 覆盖已存在的文件// 加载配置文件File configFile = new File("src/main/resources/generatorConfig.xml");ConfigurationParser cp = new ConfigurationParser(warnings);Configuration config = cp.parseConfiguration(configFile);DefaultShellCallback callback = new DefaultShellCallback(overwrite);MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);// 执行生成myBatisGenerator.generate(null);// 打印警告信息for (String warning : warnings) {System.out.println(warning);}}
}
5. 执行生成
运行 GeneratorRunner.main()
方法,成功后会在指定目录生成以下文件:
- 实体类:
com/example/pojo/User.java
、com/example/pojo/Order.java
- Mapper 接口:
com/example/mapper/UserMapper.java
、com/example/mapper/OrderMapper.java
- Mapper XML:
src/main/resources/mapper/UserMapper.xml
、src/main/resources/mapper/OrderMapper.xml
三、生成文件解析
1. 实体类(如 User.java
)
包含表中所有字段的属性及 getter/setter,示例:
public class User {private Integer id;private String username;private Integer age;// 省略 getter/setter/toString
}
2. Mapper 接口(如 UserMapper.java
)
包含基本 CRUD 方法,示例:
public interface UserMapper {int deleteByPrimaryKey(Integer id);int insert(User record);int insertSelective(User record); // 只插入非空字段User selectByPrimaryKey(Integer id);int updateByPrimaryKeySelective(User record); // 只更新非空字段int updateByPrimaryKey(User record);List<User> selectByExample(UserExample example); // 条件查询
}
3. Example 类(条件查询工具)
自动生成 UserExample.java
,用于构建复杂查询条件:
// 示例:查询 age > 18 且 username 包含 "张" 的用户
UserExample example = new UserExample();
UserExample.Criteria criteria = example.createCriteria();
criteria.andAgeGreaterThan(18);
criteria.andUsernameLike("%张%");List<User> userList = userMapper.selectByExample(example);
4. Mapper XML(如 UserMapper.xml
)
包含所有方法对应的 SQL 语句,示例:
<select id="selectByPrimaryKey" resultType="com.example.pojo.User">select id, username, age from user where id = #{id,jdbcType=INTEGER}
</select><update id="updateByPrimaryKeySelective" parameterType="com.example.pojo.User">update user<set><if test="username != null">username = #{username,jdbcType=VARCHAR},</if><if test="age != null">age = #{age,jdbcType=INTEGER},</if></set>where id = #{id,jdbcType=INTEGER}
</update>
四、高级配置与扩展
1. 生成带分页的 Example 方法
在 context
标签中添加配置,生成支持分页的条件查询:
<context id="DB2Tables" targetRuntime="MyBatis3"><!-- 生成支持分页的 Example 方法 --><plugin type="org.mybatis.generator.plugins.RowBoundsPlugin"/>
</context>
生成后 Mapper 接口会增加 selectByExampleWithRowbounds
方法,支持分页查询。
2. 自定义注释生成器
实现 CommentGenerator
接口,自定义实体类和方法的注释格式:
<commentGenerator type="com.example.generator.MyCommentGenerator"><!-- 自定义属性 --><property name="author" value="yourname"/>
</commentGenerator>
3. 忽略指定表或字段
<table tableName="user"><ignoreColumn column="password"/> <!-- 忽略 password 字段 -->
</table><!-- 忽略整个表 -->
<table tableName="log" ignore="true"/>
五、注意事项
- 生成后不要手动修改:逆向工程生成的文件建议作为基础模板,如需修改,应复制后修改,避免重新生成时被覆盖。
- 数据库表结构变更:表结构修改后,需重新执行生成(开启
overwrite=true
覆盖旧文件)。 - 复杂业务需手动扩展:逆向工程仅生成基础 CRUD,复杂业务逻辑(如多表关联)需手动在 Mapper 中添加。
- 版本兼容:MBG 版本需与 MyBatis 版本兼容(如 MBG 1.4.x 兼容 MyBatis 3.5.x)。
总结
MyBatis 逆向工程通过配置文件自动生成实体类、Mapper 接口和 XML,大幅减少重复编码工作,尤其适合初期项目搭建和表结构稳定的场景。核心步骤是配置数据库连接和生成规则,通过启动类触发生成,生成后可直接使用基础 CRUD 方法,复杂逻辑再手动扩展。
10.1、MyBatis-Plus入门案例
MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,在 MyBatis 基础上只做增强不做改变,简化了 CRUD 操作,提供了丰富的功能(如分页、条件查询、代码生成等)。以下是 MyBatis-Plus 的入门案例:
一、环境准备
1. 数据库表设计
创建 user
表并插入测试数据:
CREATE TABLE `user` (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',`name` VARCHAR(30) DEFAULT NULL COMMENT '姓名',`age` INT DEFAULT NULL COMMENT '年龄',`email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- 插入测试数据
INSERT INTO `user` (`name`, `age`, `email`) VALUES
('张三', 20, 'zhangsan@example.com'),
('李四', 22, 'lisi@example.com'),
('王五', 25, 'wangwu@example.com');
2. 项目依赖(Spring Boot)
在 pom.xml
中添加依赖:
<!-- Spring Boot 父依赖 -->
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.0</version>
</parent><dependencies><!-- Spring Boot 启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis-Plus 启动器 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><!-- MySQL 驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Lombok(简化实体类) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>
二、核心配置
1. 数据库配置(application.yml
)
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/mybatis_plus_db?serverTimezone=UTC&useSSL=falseusername: rootpassword: 123456# MyBatis-Plus 配置
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志global-config:db-config:table-prefix: # 表名前缀(如无则不配置)id-type: auto # 主键自增策略
2. 主启动类
添加 @MapperScan
注解扫描 Mapper 接口:
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描Mapper接口所在包
public class MyBatisPlusDemoApplication {public static void main(String[] args) {SpringApplication.run(MyBatisPlusDemoApplication.class, args);}
}
三、核心代码实现
1. 实体类(User.java
)
使用 Lombok 简化 getter/setter 代码:
import lombok.Data;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;@Data // Lombok注解,自动生成getter/setter/toString等
@TableName("user") // 指定对应数据库表名(表名与类名一致可省略)
public class User {@TableId(type = IdType.AUTO) // 主键自增private Long id;private String name;private Integer age;private String email;
}
2. Mapper 接口
继承 BaseMapper
接口,无需编写 XML 和方法定义:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.User;// 继承BaseMapper,泛型为实体类
public interface UserMapper extends BaseMapper<User> {// 无需编写方法,BaseMapper已提供CRUD方法
}
3. 服务层(可选)
创建 Service 接口和实现类,继承 IService
和 ServiceImpl
:
// Service接口
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.User;public interface UserService extends IService<User> {
}// Service实现类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
四、测试 CRUD 操作
使用 Spring Boot Test 测试基本操作:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;@SpringBootTest
public class UserTest {// 方式1:直接使用Mapper@Autowiredprivate UserMapper userMapper;// 方式2:使用Service(推荐)@Autowiredprivate UserService userService;// 1. 查询所有@Testpublic void testSelectAll() {List<User> userList = userMapper.selectList(null); // null表示无查询条件userList.forEach(System.out::println);}// 2. 根据ID查询@Testpublic void testSelectById() {User user = userMapper.selectById(1L);System.out.println(user);}// 3. 新增@Testpublic void testInsert() {User user = new User();user.setName("赵六");user.setAge(28);user.setEmail("zhaoliu@example.com");int rows = userMapper.insert(user); // 返回影响行数System.out.println("新增成功,影响行数:" + rows + ",新记录ID:" + user.getId());}// 4. 更新(根据ID)@Testpublic void testUpdate() {User user = new User();user.setId(1L); // 必须设置IDuser.setAge(21); // 只更新age字段int rows = userMapper.updateById(user);System.out.println("更新成功,影响行数:" + rows);}// 5. 删除(根据ID)@Testpublic void testDelete() {int rows = userMapper.deleteById(4L); // 删除ID=4的记录System.out.println("删除成功,影响行数:" + rows);}// 6. 条件查询(年龄>20)@Testpublic void testSelectByCondition() {QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.gt("age", 20); // 年龄>20queryWrapper.like("name", "李"); // 姓名包含"李"List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);}// 7. 分页查询(第1页,每页2条)@Testpublic void testSelectPage() {// 分页插件需要配置,见下方说明Page<User> page = new Page<>(1, 2); // 第1页,每页2条IPage<User> userPage = userMapper.selectPage(page, null); // null表示无查询条件System.out.println("总记录数:" + userPage.getTotal());System.out.println("总页数:" + userPage.getPages());userPage.getRecords().forEach(System.out::println); // 当前页数据}
}
五、分页插件配置
MyBatis-Plus 的分页需要单独配置插件:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyBatisPlusConfig {// 注册分页插件@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 添加分页拦截器return interceptor;}
}
六、核心原理
BaseMapper
接口:封装了 17 个常用 CRUD 方法(如selectList
、insert
、updateById
等),继承后可直接使用。QueryWrapper
:条件构造器,通过链式调用构建 SQL 条件(如gt
、like
、eq
等),替代手动编写WHERE
子句。- 分页插件:通过拦截器自动在 SQL 后添加分页条件(如 MySQL 的
LIMIT
),无需手动编写分页 SQL。
七、运行结果说明
- 执行
testSelectAll()
会查询所有用户并打印。 - 执行
testInsert()
会新增一条记录,并返回自增 ID。 - 执行
testSelectPage()
会分页查询,打印总记录数、总页数和当前页数据。
通过以上案例,可见 MyBatis-Plus 极大简化了开发流程,无需编写 XML 和基础 CRUD 方法,专注于业务逻辑即可。后续可深入学习其条件构造器、代码生成器、多表关联等高级功能。
10.2、MyBatis-Plus雪花算法
MyBatis-Plus 内置的雪花算法(Snowflake)是一种分布式 ID 生成策略,用于在分布式系统中生成全局唯一、有序的 ID。它解决了传统自增 ID 在分布式场景下的并发冲突和ID重复问题,是 MyBatis-Plus 推荐的主键生成方式之一。
1、雪花算法的核心原理
雪花算法生成的 ID 是一个 64位的长整型(Long),结构如下(从高位到低位):
位数 | 含义 | 作用 |
---|---|---|
1位 | 符号位 | 固定为 0(保证 ID 为正数) |
41位 | 时间戳 | 记录生成 ID 的毫秒级时间戳(相对于某个起始时间,可使用约69年) |
10位 | 机器标识 | 包含 5位数据中心 ID + 5位机器 ID(支持最多 32个数据中心 × 32台机器) |
12位 | 序列号 | 同一毫秒内生成的不同 ID 序列号(支持同一机器同一毫秒生成 4096个 ID) |
2、MyBatis-Plus 中雪花算法的使用
在 MyBatis-Plus 中使用雪花算法非常简单,只需在实体类的主键字段上通过注解配置即可:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;@Data
public class User {// 配置主键生成策略为雪花算法@TableId(type = IdType.ASSIGN_ID)private Long id; // 注意:必须使用 Long 类型(64位)private String name;private Integer age;
}
- 关键注解:
@TableId(type = IdType.ASSIGN_ID)
表示使用 MyBatis-Plus 内置的雪花算法生成主键。 - 注意:主键字段必须是
Long
类型(兼容 64位 ID),若用Integer
会因位数不足导致溢出。
3、雪花算法的优势
- 全局唯一:通过时间戳、机器标识和序列号的组合,确保分布式环境下 ID 不重复。
- 趋势递增:ID 随时间递增,适合作为数据库主键(有利于索引性能)。
- 高性能:本地生成 ID,无需网络请求(如数据库自增 ID 需要竞争锁),生成速度快。
- 灵活配置:可根据业务需求调整机器标识位数(默认 10位),适应不同规模的分布式系统。
- 无侵入性:MyBatis-Plus 自动集成,无需手动调用生成方法,插入数据时自动生成 ID。
4、注意事项
- 时钟回拨问题:若服务器时钟回拨(如时间校准),可能导致生成重复 ID。MyBatis-Plus 的雪花算法实现会检测并处理时钟回拨(等待时钟追赶上一次生成时间)。
- 机器标识配置:默认情况下,MyBatis-Plus 会自动生成机器标识(通过本地网卡 MAC 地址计算)。在容器化环境(如 Docker)中,若 MAC 地址可能重复,需手动配置
mybatis-plus.global-config.db-config.worker-id
和data-center-id
避免冲突。# application.yml 中手动配置机器ID和数据中心ID mybatis-plus:global-config:db-config:worker-id: 1 # 机器ID(0-31)data-center-id: 1 # 数据中心ID(0-31)
- ID 长度:生成的 ID 是 64位 Long 类型,前端接收时需注意 JavaScript 对大整数的处理问题(可转为字符串传输)。
总结
MyBatis-Plus 内置的雪花算法是分布式系统中生成唯一主键的理想选择,它通过时间戳、机器标识和序列号的组合,保证了 ID 的唯一性和递增性,且使用简单(只需通过 @TableId(type = IdType.ASSIGN_ID)
配置)。在实际使用中,需注意机器标识的配置和时钟回拨问题,尤其在容器化环境中。
10.3、MyBatis-Plus的BaseMapper接口
BaseMapper
是 MyBatis-Plus(MP)的核心接口之一,它封装了 17 个常用的 CRUD 方法,让开发者无需编写 XML 和 SQL 语句,即可快速实现对单表的操作。以下是 BaseMapper
的详细解析:
一、BaseMapper
概述
BaseMapper
是一个泛型接口,定义了单表的基本操作。通过让自定义 Mapper 接口继承它,可直接复用这些方法,极大减少重复代码。
// BaseMapper 接口定义(简化版)
public interface BaseMapper<T> extends Mapper<T> {// 17 个核心 CRUD 方法...
}// 自定义 Mapper 继承 BaseMapper
public interface UserMapper extends BaseMapper<User> {// 无需编写方法,直接使用继承的 CRUD 功能
}
二、核心方法分类与详解
BaseMapper
的方法可分为查询、插入、更新、删除四大类,以下是常用方法的详细说明:
1. 查询方法
方法签名 | 功能描述 | 参数说明 |
---|---|---|
T selectById(Serializable id) | 根据主键 ID 查询 | id :主键值(支持 Integer、Long 等 Serializable 类型) |
List<T> selectList(Wrapper<T> queryWrapper) | 条件查询(返回列表) | queryWrapper :条件构造器(null 表示查询所有) |
T selectOne(Wrapper<T> queryWrapper) | 条件查询(返回单条记录) | 若结果多于一条,会抛出 TooManyResultsException |
Long selectCount(Wrapper<T> queryWrapper) | 条件查询(返回记录总数) | queryWrapper :统计条件(null 表示查询总记录数) |
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper) | 分页查询 | page :分页参数(页码、每页条数);queryWrapper :查询条件 |
2. 插入方法
方法签名 | 功能描述 | 参数说明 |
---|---|---|
int insert(T entity) | 插入一条记录 | entity :实体对象(会根据注解自动映射字段) |
3. 更新方法
方法签名 | 功能描述 | 参数说明 |
---|---|---|
int updateById(T entity) | 根据主键 ID 更新 | entity :实体对象(必须包含主键,仅更新非 null 字段) |
int update(T entity, Wrapper<T> updateWrapper) | 条件更新 | entity :更新的字段值;updateWrapper :更新条件(where 子句) |
4. 删除方法
方法签名 | 功能描述 | 参数说明 |
---|---|---|
int deleteById(Serializable id) | 根据主键 ID 删除 | id :主键值 |
int deleteByMap(Map<String, Object> columnMap) | 根据字段条件删除(多字段 and 关系) | columnMap :键为字段名,值为字段值(如 {name: "张三", age: 20} ) |
int delete(Wrapper<T> queryWrapper) | 条件删除 | queryWrapper :删除条件(where 子句) |
int deleteBatchIds(Collection<? extends Serializable> idList) | 批量删除 | idList :主键集合(如 Arrays.asList(1,2,3) ) |
三、使用示例
以 UserMapper
为例,展示 BaseMapper
方法的实际用法:
@Autowired
private UserMapper userMapper;// 1. 根据 ID 查询
User user = userMapper.selectById(1L);// 2. 查询所有
List<User> userList = userMapper.selectList(null); // queryWrapper 为 null 表示无条件// 3. 条件查询(年龄 > 20 且姓名包含 "张")
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20).like("name", "张");
List<User> list = userMapper.selectList(queryWrapper);// 4. 插入
User newUser = new User();
newUser.setName("赵六");
newUser.setAge(28);
userMapper.insert(newUser); // 自动生成 ID(如雪花算法)// 5. 根据 ID 更新(只更新非 null 字段)
User updateUser = new User();
updateUser.setId(1L);
updateUser.setAge(21); // 仅更新 age 字段
userMapper.updateById(updateUser);// 6. 条件更新(将年龄 < 18 的用户邮箱改为 "teen@example.com")
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.lt("age", 18).set("email", "teen@example.com");
userMapper.update(null, updateWrapper); // entity 为 null,用 updateWrapper 设置字段// 7. 根据 ID 删除
userMapper.deleteById(4L);// 8. 分页查询(第 1 页,每页 2 条)
Page<User> page = new Page<>(1, 2);
IPage<User> userPage = userMapper.selectPage(page, null);
List<User> records = userPage.getRecords(); // 当前页数据
long total = userPage.getTotal(); // 总记录数
四、关键特性解析
-
自动 SQL 生成:
BaseMapper
的方法通过 MyBatis 的拦截器机制,根据实体类注解(如@TableName
、@TableField
)和方法参数,自动生成对应的 SQL 语句,无需手动编写 XML。 -
条件构造器(
Wrapper
):QueryWrapper
:用于查询和删除条件。UpdateWrapper
:用于更新条件(支持set
方法设置字段值)。
通过链式调用(如gt()
、like()
、eq()
)构建复杂条件,替代手动拼接 SQL。
-
null 值处理:
- 插入和更新时,默认忽略实体类中的
null
字段(即不生成对应字段的 SQL)。 - 如需强制更新
null
,可在实体类字段上添加@TableField(updateStrategy = FieldStrategy.IGNORED)
。
- 插入和更新时,默认忽略实体类中的
-
主键策略适配:
自动适配实体类配置的主键生成策略(如IdType.AUTO
自增、IdType.ASSIGN_ID
雪花算法),插入时无需手动设置主键。
五、扩展与自定义
-
方法扩展:
若BaseMapper
的方法无法满足需求,可在自定义 Mapper 中添加新方法并编写对应的 XML 或注解 SQL:public interface UserMapper extends BaseMapper<User> {// 自定义方法:查询年龄大于指定值的用户数量@Select("SELECT COUNT(*) FROM user WHERE age > #{age}")int countByAgeGreaterThan(Integer age); }
-
通用 Service 层封装:
MP 还提供了IService
和ServiceImpl
,在 Service 层进一步封装BaseMapper
的方法,并提供批量操作(如saveBatch
、updateBatchById
)等增强功能:public interface UserService extends IService<User> { }@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
六、适用场景与局限性
- 适用场景:单表的 CRUD 操作,尤其是简单查询、插入、更新场景。
- 局限性:
- 不支持多表关联查询(需手动编写 SQL 或使用 MP 的
@TableField
注解配置关联)。 - 极端复杂的 SQL(如多表联查+子查询+聚合函数)仍需手动编写。
- 不支持多表关联查询(需手动编写 SQL 或使用 MP 的
总结
BaseMapper
是 MyBatis-Plus 简化开发的核心组件,通过封装 17 个单表操作方法,实现了“零 SQL”完成基础 CRUD。其核心优势在于自动生成 SQL、支持条件构造器和主键策略,极大提升了开发效率。实际使用中,可结合条件构造器灵活实现复杂查询,同时保留自定义 SQL 的扩展能力,兼顾便捷性和灵活性。
10.4、MyBatis-Plus的条件构造器
MyBatis-Plus 的条件构造器(Wrapper)是构建 SQL 条件(WHERE
子句)的核心工具,通过链式调用的方式替代手动拼接 SQL 片段,支持复杂条件组合,且能避免 SQL 注入风险。以下是条件构造器的详细解析:
一、核心接口与实现类
条件构造器的顶层接口是 Wrapper<T>
,其核心实现类如下:
类名 | 作用场景 | 特点 |
---|---|---|
QueryWrapper<T> | 用于 查询、删除 操作的条件构造 | 支持所有查询条件(如 WHERE 、ORDER BY 、GROUP BY 等) |
UpdateWrapper<T> | 用于 更新 操作的条件构造 | 在 QueryWrapper 基础上增加 set 方法(设置更新字段) |
LambdaQueryWrapper<T> | 基于 Lambda 的查询条件构造 | 通过实体类方法引用字段(如 User::getName ),避免硬编码字段名 |
LambdaUpdateWrapper<T> | 基于 Lambda 的更新条件构造 | 结合 Lambda 表达式和 set 方法,更安全地更新字段 |
二、基础用法:QueryWrapper
QueryWrapper
是最常用的条件构造器,用于构建查询和删除的 WHERE
条件。
1. 基本条件方法(比较运算)
方法名 | SQL 对应 | 示例(查询年龄>20且姓名为"张三"的用户) |
---|---|---|
eq(R column, Object val) | WHERE column = val | queryWrapper.eq("name", "张三") |
ne(R column, Object val) | WHERE column != val | queryWrapper.ne("age", 18) |
gt(R column, Object val) | WHERE column > val | queryWrapper.gt("age", 20) |
ge(R column, Object val) | WHERE column >= val | queryWrapper.ge("age", 20) |
lt(R column, Object val) | WHERE column < val | queryWrapper.lt("age", 30) |
le(R column, Object val) | WHERE column <= val | queryWrapper.le("age", 30) |
2. 模糊查询
方法名 | SQL 对应 | 示例(查询姓名包含"张"的用户) |
---|---|---|
like(R column, Object val) | WHERE column LIKE '%val%' | queryWrapper.like("name", "张") |
likeLeft(R column, Object val) | WHERE column LIKE '%val' | queryWrapper.likeLeft("name", "三") // 匹配"张三"、“李三” |
likeRight(R column, Object val) | WHERE column LIKE 'val%' | queryWrapper.likeRight("name", "张") // 匹配"张三"、“张四” |
3. 范围查询
方法名 | SQL 对应 | 示例(查询年龄在20-30之间的用户) |
---|---|---|
between(R column, Object val1, Object val2) | WHERE column BETWEEN val1 AND val2 | queryWrapper.between("age", 20, 30) |
notBetween(R column, Object val1, Object val2) | WHERE column NOT BETWEEN val1 AND val2 | queryWrapper.notBetween("age", 20, 30) |
in(R column, Collection<?> coll) | WHERE column IN (val1, val2...) | queryWrapper.in("id", Arrays.asList(1,2,3)) |
notIn(R column, Collection<?> coll) | WHERE column NOT IN (val1, val2...) | queryWrapper.notIn("id", 4,5,6) |
4. 逻辑运算(AND
/OR
)
默认条件之间用 AND
连接,可通过 or()
方法切换为 OR
:
// 示例:查询(年龄>20 且 姓名包含"张")OR(邮箱不为空)的用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20).like("name", "张").or() // 切换为 OR.isNotNull("email");
// 生成 SQL:WHERE age > 20 AND name LIKE '%张%' OR email IS NOT NULL
嵌套逻辑:使用 and(Consumer)
或 or(Consumer)
实现分组条件:
// 示例:查询(年龄>20 且 姓名包含"张")OR(年龄<18 且 邮箱不为空)的用户
queryWrapper.and(qw -> qw.gt("age", 20).like("name", "张")) // 分组1:AND连接.or(qw -> qw.lt("age", 18).isNotNull("email")); // 分组2:AND连接
// 生成 SQL:WHERE (age > 20 AND name LIKE '%张%') OR (age < 18 AND email IS NOT NULL)
5. 排序与分页
// 排序:按年龄降序,再按ID升序
queryWrapper.orderByDesc("age").orderByAsc("id");// 分页:配合 Page 类使用(需配置分页插件)
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
IPage<User> userPage = userMapper.selectPage(page, queryWrapper);
6. 其他常用方法
方法名 | 作用 | 示例 |
---|---|---|
isNull(R column) | 字段为 null | queryWrapper.isNull("email") |
isNotNull(R column) | 字段不为 null | queryWrapper.isNotNull("email") |
groupBy(R... columns) | 分组查询 | queryWrapper.groupBy("age", "gender") |
having(String sqlHaving, Object... params) | 分组后条件过滤 | queryWrapper.having("COUNT(*) > 10") |
select(R... columns) | 指定查询字段(默认查询所有字段) | queryWrapper.select("id", "name", "age") |
三、UpdateWrapper
:更新条件构造
UpdateWrapper
继承自 QueryWrapper
,新增 set
方法用于设置更新字段,适合复杂条件的更新操作。
// 示例:将年龄>30且姓名包含"张"的用户,邮箱改为"old@example.com",年龄加1
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// 设置更新字段
updateWrapper.set("email", "old@example.com").setSql("age = age + 1"); // 支持SQL片段(如自增)
// 设置更新条件
updateWrapper.gt("age", 30).like("name", "张");userMapper.update(null, updateWrapper);
// 生成 SQL:UPDATE user SET email = 'old@example.com', age = age + 1 WHERE age > 30 AND name LIKE '%张%'
四、Lambda 条件构造器(推荐)
LambdaQueryWrapper
和 LambdaUpdateWrapper
通过 Lambda 表达式引用实体类字段(如 User::getName
),避免硬编码字段名(减少因字段名修改导致的错误)。
示例:LambdaQueryWrapper
// 示例:查询年龄>20且姓名包含"张"的用户(Lambda方式)
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery.gt(User::getAge, 20) // 引用User类的age字段.like(User::getName, "张"); // 引用User类的name字段List<User> userList = userMapper.selectList(lambdaQuery);
示例:LambdaUpdateWrapper
// 示例:更新姓名为"张三"的用户年龄为25
LambdaUpdateWrapper<User> lambdaUpdate = new LambdaUpdateWrapper<>();
lambdaUpdate.eq(User::getName, "张三") // 条件:name = "张三".set(User::getAge, 25); // 更新字段:age = 25userMapper.update(null, lambdaUpdate);
五、条件判断:condition
参数
所有条件方法都支持 condition
布尔参数,用于动态决定是否添加该条件(避免手动写 if-else
)。
// 示例:若name不为null,则添加姓名模糊查询条件
String name = "张"; // 假设从前端传入,可能为null
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(name != null, "name", name); // 第一个参数为true时才添加条件// Lambda方式
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery.like(name != null, User::getName, name);
六、注意事项
-
字段名与数据库列名映射:
条件构造器中的字段名默认对应实体类的属性名,若数据库列名与属性名不一致(如属性名userName
对应列名user_name
),需通过@TableField(value = "user_name")
注解配置,条件构造器会自动映射为数据库列名。 -
SQL 注入风险:
条件构造器的参数值会被 MyBatis 自动参数化(如like("name", val)
会生成LIKE ?
),避免 SQL 注入。但setSql()
、having()
等方法直接传入 SQL 片段时需谨慎(如setSql("age = " + age)
可能有风险,建议用参数化方式)。 -
避免冗余条件:
若条件为空(如queryWrapper
无任何条件),会生成WHERE 1=1
(不影响查询结果,但可简化逻辑)。
总结
条件构造器是 MyBatis-Plus 简化 SQL 条件构建的核心工具,通过 QueryWrapper
/UpdateWrapper
支持几乎所有 SQL 条件语法,结合 Lambda 表达式可进一步提升代码的安全性和可维护性。实际开发中,推荐优先使用 LambdaQueryWrapper
和 LambdaUpdateWrapper
,减少硬编码字段名的错误,同时利用 condition
参数实现动态条件,让代码更简洁高效。
10.5、MyBatis-Plus的自定义操作
MyBatis-Plus(MP)在提供强大的CRUD自动生成能力的同时,也支持灵活的自定义操作,以满足复杂业务场景需求。自定义操作主要包括自定义SQL语句、扩展BaseMapper、自定义方法等,以下是详细实现方式:
一、自定义SQL(XML方式)
当BaseMapper提供的方法无法满足复杂查询(如多表关联、子查询)时,可通过XML文件编写自定义SQL,并在Mapper接口中声明方法。
步骤示例:
- 在Mapper接口中定义方法
public interface UserMapper extends BaseMapper<User> {// 自定义方法:查询用户及其关联的订单列表List<UserOrderVO> selectUserWithOrders(@Param("userId") Long userId);
}
- 创建对应的XML映射文件(UserMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mapper.UserMapper"><!-- 自定义查询:多表关联 --><select id="selectUserWithOrders" resultType="com.example.vo.UserOrderVO">SELECT u.id AS userId, u.name AS userName, o.id AS orderId, o.order_no AS orderNoFROM user uLEFT JOIN `order` o ON u.id = o.user_idWHERE u.id = #{userId}</select>
</mapper>
- 调用自定义方法
@Autowired
private UserMapper userMapper;List<UserOrderVO> userOrders = userMapper.selectUserWithOrders(1L);
二、自定义SQL(注解方式)
简单的自定义SQL可直接通过注解(如@Select
、@Update
)在Mapper接口中声明,无需XML文件。
public interface UserMapper extends BaseMapper<User> {// 注解方式自定义查询:根据年龄范围查询用户@Select("SELECT * FROM user WHERE age BETWEEN #{minAge} AND #{maxAge}")List<User> selectByAgeRange(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);// 自定义更新:批量更新状态@Update("<script>" +"UPDATE user SET status = #{status} WHERE id IN " +"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +"#{id}" +"</foreach>" +"</script>")int updateStatusBatch(@Param("ids") List<Long> ids, @Param("status") Integer status);
}
- 注解中支持MyBatis的动态SQL标签(如
<foreach>
、<if>
),需用<script>
包裹。
三、扩展BaseMapper(全局自定义方法)
若多个Mapper需要复用同一自定义方法,可通过扩展BaseMapper实现全局方法。
步骤示例:
- 定义扩展接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;// 自定义基础Mapper,添加全局通用方法
public interface MyBaseMapper<T> extends BaseMapper<T> {// 自定义方法:根据字段名和值查询List<T> selectByField(@Param("column") String column, @Param("value") Object value);
}
- 编写对应的XML(MyBaseMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mapper.MyBaseMapper"><!-- 全局通用方法:根据字段查询 --><select id="selectByField" resultType="T">SELECT * FROM ${tableName} WHERE ${column} = #{value}</select>
</mapper>
- 让业务Mapper继承自定义接口
// UserMapper继承自定义的MyBaseMapper,而非直接继承BaseMapper
public interface UserMapper extends MyBaseMapper<User> {// 自动拥有BaseMapper和MyBaseMapper的所有方法
}
- 使用全局方法
// 调用自定义的全局方法
List<User> users = userMapper.selectByField("name", "张三");
四、自定义Service层方法
在Service层,可基于Mapper的自定义方法进一步封装业务逻辑,或直接通过baseMapper
调用原生MyBatis方法。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// 自定义Service方法:批量插入并返回成功数量@Overridepublic int batchInsertWithResult(List<User> userList) {if (CollectionUtils.isEmpty(userList)) {return 0;}// 调用自定义的Mapper方法return baseMapper.batchInsert(userList);}// 结合Lambda条件构造器的复杂查询@Overridepublic List<User> getActiveUsers(Integer minAge) {return lambdaQuery().gt(User::getAge, minAge).eq(User::getStatus, 1) // 状态为激活.list();}
}
五、插件扩展(自定义拦截器)
通过自定义MyBatis拦截器,可对SQL执行过程进行增强(如数据权限控制、SQL日志增强)。
示例:自定义SQL拦截器
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;// 拦截StatementHandler的prepare方法(SQL预编译阶段)
@Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class, Integer.class}
)})
public class DataScopeInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取StatementHandler对象StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = SystemMetaObject.forObject(statementHandler);// 获取原始SQLString originalSql = (String) metaObject.getValue("delegate.boundSql.sql");if (originalSql == null || originalSql.isEmpty()) {return invocation.proceed();}// 自定义逻辑:为查询SQL添加数据范围条件(如仅能查询本部门数据)if (originalSql.trim().toLowerCase().startsWith("select")) {String dataScopeSql = " AND dept_id = 100 "; // 假设当前用户部门ID为100String newSql = originalSql + dataScopeSql;metaObject.setValue("delegate.boundSql.sql", newSql);}// 执行原始操作return invocation.proceed();}@Overridepublic Object plugin(Object target) {// 包装目标对象:仅拦截StatementHandlerreturn Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 接收配置参数(可选)}
}
注册拦截器
@Configuration
public class MyBatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 注册自定义拦截器interceptor.addInnerInterceptor(new DataScopeInterceptor());// 可同时注册分页插件等interceptor.addInnerInterceptor(new PaginationInnerInterceptor());return interceptor;}
}
六、自定义ID生成策略
若内置的ID生成策略(如雪花算法)不满足需求,可自定义ID生成器。
示例:自定义UUID生成器
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.springframework.stereotype.Component;
import java.util.UUID;@Component
public class CustomIdGenerator implements IdentifierGenerator {@Overridepublic Long nextId(Object entity) {// 生成UUID并转为Long(仅示例,实际UUID不适合转为Long)String uuid = UUID.randomUUID().toString().replace("-", "");return Long.parseLong(uuid.substring(0, 16), 16);}
}
使用自定义ID生成器
@Data
public class User {@TableId(type = IdType.ASSIGN_ID) // 自动使用Spring容器中的自定义生成器private Long id;// 其他字段...
}
总结
MyBatis-Plus的自定义操作灵活多样,可根据业务复杂度选择合适的方式:
- 简单SQL:用注解方式在Mapper中直接声明。
- 复杂SQL或多表关联:用XML方式编写。
- 通用方法复用:扩展BaseMapper实现全局方法。
- 业务逻辑封装:在Service层组合基础方法或调用自定义Mapper方法。
- 全局SQL增强:通过自定义拦截器实现(如数据权限、SQL审计)。
这些方式既保留了MP的便捷性,又能满足个性化需求,实现“自动生成+灵活扩展”的平衡。
10.6、MyBatis-Plus的IServce接口
IService
是 MyBatis-Plus(MP)在 Service 层提供的核心接口,它基于 BaseMapper
进一步封装了更多实用的 CRUD 方法,尤其是批量操作、条件查询等增强功能,旨在简化 Service 层的开发。以下是 IService
接口的详细解析:
一、IService
概述
IService
是一个泛型接口,定义了 Service 层的通用操作。通过让自定义 Service 接口继承它,并结合 ServiceImpl<Mapper, Entity>
实现类,可快速获得丰富的业务方法。
// IService 接口定义(简化版)
public interface IService<T> {// 包含 40+ 个通用方法,涵盖 CRUD、批量操作、条件查询等
}// 自定义 Service 接口
public interface UserService extends IService<User> {// 可添加自定义业务方法
}// 实现类(继承 MP 提供的 ServiceImpl)
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// 无需手动实现 IService 方法,直接继承使用
}
- 核心依赖:
ServiceImpl
实现了IService
接口,并持有BaseMapper
实例(通过baseMapper
字段),底层调用 Mapper 方法完成数据库操作。
二、IService
核心方法分类与详解
IService
的方法在 BaseMapper
基础上进行了扩展,增加了批量操作、链式查询、计数查询等功能,主要分为以下几类:
1. 新增操作
方法签名 | 功能描述 | 特点 |
---|---|---|
boolean save(T entity) | 插入单条记录 | 等价于 baseMapper.insert(entity) ,返回是否成功 |
boolean saveBatch(Collection<T> entityList) | 批量插入(默认批次大小 1000) | 内部通过循环调用 insert ,适合小批量数据 |
boolean saveBatch(Collection<T> entityList, int batchSize) | 自定义批次大小的批量插入 | 可控制批次大小(如每 500 条提交一次),优化性能 |
2. 修改操作
方法签名 | 功能描述 | 特点 |
---|---|---|
boolean updateById(T entity) | 根据 ID 更新单条记录 | 等价于 baseMapper.updateById(entity) |
boolean update(T entity, Wrapper<T> updateWrapper) | 条件更新 | 等价于 baseMapper.update(entity, updateWrapper) |
boolean updateBatchById(Collection<T> entityList) | 批量更新(默认批次大小 1000) | 需实体类包含 ID,内部循环调用 updateById |
boolean updateBatchById(Collection<T> entityList, int batchSize) | 自定义批次大小的批量更新 |
3. 新增或更新操作
方法签名 | 功能描述 | 特点 |
---|---|---|
boolean saveOrUpdate(T entity) | 插入或更新(根据 ID 判断:有 ID 则更新,无则插入) | 自动判断操作类型,适合“ Upsert ”场景 |
boolean saveOrUpdateBatch(Collection<T> entityList) | 批量插入或更新 | 批量处理“ Upsert ”,内部循环调用 saveOrUpdate |
4. 删除操作
方法签名 | 功能描述 | 特点 |
---|---|---|
boolean removeById(Serializable id) | 根据 ID 删除 | 等价于 baseMapper.deleteById(id) |
boolean removeByMap(Map<String, Object> columnMap) | 根据字段条件删除 | 等价于 baseMapper.deleteByMap(columnMap) |
boolean remove(Wrapper<T> queryWrapper) | 条件删除 | 等价于 baseMapper.delete(queryWrapper) |
boolean removeByIds(Collection<? extends Serializable> idList) | 批量删除 | 等价于 baseMapper.deleteBatchIds(idList) |
5. 查询操作
方法签名 | 功能描述 | 特点 |
---|---|---|
T getById(Serializable id) | 根据 ID 查询单条记录 | 等价于 baseMapper.selectById(id) |
List<T> list() | 查询所有记录 | 等价于 baseMapper.selectList(null) |
List<T> list(Wrapper<T> queryWrapper) | 条件查询列表 | 等价于 baseMapper.selectList(queryWrapper) |
Page<T> page(Page<T> page) | 无条件分页查询 | 需配合分页插件,等价于 baseMapper.selectPage(page, null) |
Page<T> page(Page<T> page, Wrapper<T> queryWrapper) | 条件分页查询 | 等价于 baseMapper.selectPage(page, queryWrapper) |
long count() | 查询总记录数 | 等价于 baseMapper.selectCount(null) |
long count(Wrapper<T> queryWrapper) | 条件查询总记录数 | 等价于 baseMapper.selectCount(queryWrapper) |
6. 链式查询(Lambda 风格)
IService
提供了 Lambda 链式调用方法,支持更简洁的条件构造:
方法签名 | 功能描述 | 示例 |
---|---|---|
LambdaQueryChainWrapper<T> lambdaQuery() | 开启 Lambda 条件查询链 | lambdaQuery().eq(User::getName, "张三").list() |
LambdaUpdateChainWrapper<T> lambdaUpdate() | 开启 Lambda 条件更新链 | lambdaUpdate().eq(User::getId, 1).set(User::getAge, 20).update() |
三、使用示例
以 UserService
为例,展示 IService
方法的实际用法:
@Autowired
private UserService userService;// 1. 新增
User user = new User();
user.setName("张三");
user.setAge(20);
boolean saveSuccess = userService.save(user); // 插入单条// 2. 批量新增
List<User> userList = Arrays.asList(new User().setName("李四").setAge(22),new User().setName("王五").setAge(25)
);
boolean batchSaveSuccess = userService.saveBatch(userList, 100); // 批次大小100// 3. 条件更新(将年龄>30的用户邮箱改为"old@example.com")
boolean updateSuccess = userService.update(new UpdateWrapper<User>().gt("age", 30).set("email", "old@example.com")
);// 4. 批量更新
List<User> updateList = Arrays.asList(new User().setId(1L).setAge(21),new User().setId(2L).setAge(23)
);
boolean batchUpdateSuccess = userService.updateBatchById(updateList);// 5. 条件查询(年龄在20-30之间的用户)
List<User> userList = userService.list(new QueryWrapper<User>().between("age", 20, 30)
);// 6. 分页查询(第2页,每页10条)
Page<User> page = new Page<>(2, 10);
Page<User> userPage = userService.page(page, new QueryWrapper<User>().like("name", "张"));
List<User> records = userPage.getRecords(); // 当前页数据
long total = userPage.getTotal(); // 总记录数// 7. Lambda链式查询(查询姓名为"张三"的用户)
User zhangsan = userService.lambdaQuery().eq(User::getName, "张三").one(); // 返回单条记录// 8. Lambda链式更新(更新ID=1的用户年龄为20)
boolean update = userService.lambdaUpdate().eq(User::getId, 1).set(User::getAge, 20).update();
四、IService
与 BaseMapper
的区别
维度 | BaseMapper | IService |
---|---|---|
所属层级 | Mapper 层(数据访问层) | Service 层(业务逻辑层) |
方法特点 | 基础 CRUD,返回影响行数(int ) | 增强 CRUD,返回布尔值(boolean ),增加批量操作、链式查询等 |
事务支持 | 无事务管理(需手动在 Service 层控制) | 可结合 @Transactional 注解实现事务管理 |
适用场景 | 直接操作数据库,适合简单查询 | 封装业务逻辑,适合复杂业务场景 |
五、扩展与自定义
IService
支持在继承的基础上添加自定义业务方法,满足个性化需求:
// 自定义 Service 接口
public interface UserService extends IService<User> {// 自定义方法:根据年龄范围查询用户并按姓名排序List<User> getUsersByAgeRange(Integer minAge, Integer maxAge);
}// 实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic List<User> getUsersByAgeRange(Integer minAge, Integer maxAge) {// 调用 IService 的方法return lambdaQuery().between(User::getAge, minAge, maxAge).orderByAsc(User::getName).list();}// 结合自定义 Mapper 方法@Overridepublic List<UserOrderVO> getUserWithOrders(Long userId) {// 通过 baseMapper 调用自定义 Mapper 方法return baseMapper.selectUserWithOrders(userId);}
}
六、最佳实践
-
分层调用:Controller 层调用 Service 层(
IService
方法),Service 层调用 Mapper 层(BaseMapper
方法),避免 Controller 直接操作 Mapper。 -
批量操作优化:大量数据(如万级以上)批量插入/更新时,使用
saveBatch
/updateBatchById
并合理设置batchSize
(建议 500-1000),减少数据库连接次数。 -
事务管理:在 Service 方法上添加
@Transactional
注解,确保批量操作的原子性(要么全成功,要么全失败)。 -
链式查询优先:优先使用
lambdaQuery()
/lambdaUpdate()
链式调用,代码更简洁,且避免硬编码字段名。
总结
IService
是 MyBatis-Plus 在 Service 层的核心封装,它在 BaseMapper
基础上提供了更丰富的功能(批量操作、链式查询等),并返回更直观的布尔值结果。通过继承 IService
和 ServiceImpl
,开发者可快速实现 Service 层逻辑,同时保留自定义业务方法的灵活性。实际开发中,应充分利用其批量操作和链式查询能力,提升代码效率和可读性。
11、详解MyBatis-Plus的注解开发
MyBatis-Plus(MP)的注解开发是在 MyBatis 注解基础上的增强,通过一系列注解简化 SQL 编写、结果映射和实体类配置,无需依赖 XML 文件即可完成大部分数据库操作。以下是 MP 核心注解的详细解析:
一、实体类注解
用于实体类与数据库表、字段的映射配置,是注解开发的基础。
1. @TableName
- 作用:指定实体类对应的数据库表名。
- 场景:当实体类名与表名不一致时使用。
- 属性:
value
:表名(必填)。schema
:数据库 schema(部分数据库支持)。keepGlobalPrefix
:是否保留全局表名前缀(配合全局配置使用)。
@TableName("t_user") // 实体类 User 对应表 t_user
public class User { ... }
2. @TableId
- 作用:指定实体类的主键字段。
- 属性:
value
:主键列名(列名与属性名一致时可省略)。type
:主键生成策略(IdType
枚举),常用值:IdType.AUTO
:数据库自增(需表主键设置自增)。IdType.ASSIGN_ID
:MP 雪花算法生成全局唯一 ID(默认)。IdType.INPUT
:手动输入主键。
public class User {@TableId(type = IdType.AUTO) // 主键自增private Long id;
}
3. @TableField
- 作用:指定非主键字段与数据库列的映射关系。
- 属性:
value
:列名(列名与属性名一致时可省略)。exist
:是否为数据库字段(false
表示该属性不对应表字段,默认true
)。fill
:字段自动填充策略(FieldFill
枚举,如插入/更新时自动填充创建时间)。select
:查询时是否包含该字段(false
表示默认查询不返回该字段)。updateStrategy
:更新时的字段策略(如FieldStrategy.IGNORED
强制更新 null 值)。
public class User {@TableField("user_name") // 属性名 name 对应列 user_nameprivate String name;@TableField(exist = false) // 非数据库字段private String tempData;@TableField(fill = FieldFill.INSERT) // 插入时自动填充private LocalDateTime createTime;
}
4. @Version
- 作用:实现乐观锁(用于解决并发更新冲突)。
- 原理:更新时会自动添加
WHERE version = 旧版本
,并将版本号 +1。
public class User {@Versionprivate Integer version; // 数据库需有 version 列,初始值为 0
}
注意:需配置乐观锁插件才能生效:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;
}
5. @EnumValue
- 作用:指定枚举类中与数据库字段对应的属性。
- 场景:实体类使用枚举类型时,映射枚举的实际存储值。
// 枚举类
public enum GenderEnum {MALE(1, "男"), FEMALE(0, "女");@EnumValue // 数据库存储的是该字段(1或0)private final int code;private final String desc;// 构造器、getter
}// 实体类
public class User {private GenderEnum gender; // 数据库中存储 1 或 0
}
6. @TableLogic
- 作用:标记逻辑删除字段(假删除,而非物理删除)。
- 原理:删除时自动执行更新操作(如
SET deleted = 1
),查询时自动过滤已删除数据(WHERE deleted = 0
)。
public class User {@TableLogicprivate Integer deleted; // 0-未删除,1-已删除
}
配置全局逻辑删除值(可选):
mybatis-plus:global-config:db-config:logic-delete-value: 1 # 逻辑删除值logic-not-delete-value: 0 # 逻辑未删除值
二、SQL 操作注解
用于在 Mapper 接口中直接编写 SQL 语句,替代 XML 映射文件。
1. 基础 CRUD 注解
MP 继承了 MyBatis 的 @Select
、@Insert
、@Update
、@Delete
注解,并增强了动态 SQL 支持:
public interface UserMapper extends BaseMapper<User> {// 自定义查询@Select("SELECT * FROM t_user WHERE name = #{name}")User selectByName(@Param("name") String name);// 动态更新(使用 MP 的条件构造器)@Update("UPDATE t_user SET age = #{age} WHERE id = #{id}")int updateAgeById(@Param("id") Long id, @Param("age") Integer age);// 批量删除(结合 MyBatis 动态 SQL)@Delete("<script>" +"DELETE FROM t_user WHERE id IN " +"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +"#{id}" +"</foreach>" +"</script>")int deleteBatchByIds(@Param("ids") List<Long> ids);
}
2. @Results
与 @Result
- 作用:自定义结果集映射(当字段名与属性名映射复杂时使用)。
@Results(id = "userResultMap", value = {@Result(column = "user_id", property = "id", id = true), // 主键映射@Result(column = "user_name", property = "name"),@Result(column = "create_time", property = "createTime")
})
@Select("SELECT user_id, user_name, create_time FROM t_user WHERE user_id = #{id}")
User selectById(Long id);// 复用结果映射
@ResultMap("userResultMap")
@Select("SELECT user_id, user_name, create_time FROM t_user")
List<User> selectAll();
3. @One
与 @Many
- 作用:实现关联查询(一对一、一对多)。
public interface UserMapper extends BaseMapper<User> {// 一对一关联:用户 -> 身份证@Results({@Result(column = "id", property = "id"),@Result(column = "card_id", property = "card", one = @One(select = "com.example.mapper.CardMapper.selectById"))})@Select("SELECT id, card_id FROM t_user WHERE id = #{id}")User selectUserWithCard(Long id);// 一对多关联:用户 -> 订单@Results({@Result(column = "id", property = "id"),@Result(column = "id", property = "orders",many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId"))})@Select("SELECT id FROM t_user WHERE id = #{id}")User selectUserWithOrders(Long id);
}
4. @Mapper
- 作用:标记接口为 MyBatis 的 Mapper 接口,MP 会自动扫描并生成实现类。
- 替代方案:在启动类用
@MapperScan("com.example.mapper")
批量扫描,无需在每个接口加@Mapper
。
@Mapper
public interface UserMapper extends BaseMapper<User> { ... }
三、高级注解
1. @SqlParser
- 作用:控制 SQL 解析器是否忽略当前方法(用于绕过 MP 的 SQL 拦截器,如分页插件)。
@SqlParser(filter = true) // 忽略 SQL 解析(不分页)
@Select("SELECT * FROM t_user")
List<User> selectAllWithoutPage();
2. @InsertFill
与 @UpdateFill
- 作用:配合字段自动填充策略(
@TableField(fill)
),指定填充处理器。
// 自定义填充处理器
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {// 插入时自动填充 createTimethis.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());}@Overridepublic void updateFill(MetaObject metaObject) {// 更新时自动填充 updateTimethis.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());}
}// 实体类中指定填充时机
public class User {@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.UPDATE)private LocalDateTime updateTime;
}
四、注解开发 vs XML 开发
维度 | 注解开发 | XML 开发 |
---|---|---|
适用场景 | 简单 SQL、单表操作、快速开发 | 复杂 SQL、多表关联、动态 SQL 逻辑复杂 |
可读性 | SQL 与方法在同一文件,简单逻辑直观 | SQL 单独管理,复杂逻辑结构清晰 |
维护性 | 改 SQL 需修改接口代码,需重新编译 | 改 XML 无需编译,热部署友好 |
动态 SQL | 需用 <script> 标签包裹,可读性差 | 支持 <if> 、<foreach> 等标签,灵活 |
五、最佳实践
-
混合使用:简单 CRUD 用 MP 自带的
BaseMapper
方法,无需注解;中等复杂度用注解;复杂逻辑用 XML。 -
字段映射规范:实体类与表名、字段名尽量保持一致,减少
@TableName
、@TableField
的使用。 -
动态 SQL 谨慎使用:注解中的动态 SQL 需用
<script>
包裹,复杂场景(如多层嵌套)建议用 XML。 -
关联查询:简单关联用
@One
/@Many
,复杂多表关联建议用 XML 或 SQL 语句。
总结
MyBatis-Plus 的注解开发通过 @TableName
、@TableId
等实体类注解完成映射配置,结合 @Select
、@Insert
等 SQL 注解实现数据库操作,大幅简化了开发流程。其核心优势是无需 XML 文件,适合简单场景和快速开发。实际项目中,应根据 SQL 复杂度灵活选择注解或 XML 方式,平衡开发效率与可维护性。
12、MyBatis-Plus代码生成器
MyBatis-Plus 代码生成器(AutoGenerator)是一款高效的代码生成工具,能够根据数据库表结构自动生成实体类、Mapper 接口、Service 层、Controller 层等全套代码,极大减少重复性开发工作。以下是其详细使用方法和核心特性解析:
一、代码生成器核心功能
- 自动生成数据库表对应的实体类(支持 Lombok、Swagger 注解)。
- 生成 Mapper 接口及 XML 映射文件。
- 生成 Service 接口及实现类(继承 IService 和 ServiceImpl)。
- 生成 Controller 层代码(包含基本 CRUD 接口)。
- 支持自定义模板,适配不同项目架构(如 RESTful API、分层设计)。
- 可配置生成策略(如是否覆盖已有文件、生成注释等)。
二、环境准备
1. 引入依赖(Maven)
<!-- MyBatis-Plus 核心依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency><!-- 代码生成器依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.3.1</version>
</dependency><!-- 模板引擎(必选,MP 代码生成器基于 Velocity 实现) -->
<dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.3</version>
</dependency><!-- 可选:Swagger 依赖(用于生成 API 文档注解) -->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version>
</dependency><!-- 数据库驱动(以 MySQL 为例) -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version>
</dependency>
三、核心配置与使用示例
以下是完整的代码生成器配置类,包含数据库连接、包路径、生成策略等关键配置:MyBatis-Plus代码生成器配置类:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
import com.baomidou.mybatisplus.generator.injector.methods.Add;
import com.baomidou.mybatisplus.generator.injector.methods.Delete;
import com.baomidou.mybatisplus.generator.injector.methods.Update;import java.util.Collections;public class CodeGenerator {public static void main(String[] args) {// 数据库连接配置String url = "jdbc:mysql://localhost:3306/mybatis_plus_db?serverTimezone=UTC&useSSL=false";String username = "root";String password = "123456";// 代码生成主流程FastAutoGenerator.create(url, username, password)// 1. 全局配置.globalConfig(builder -> {builder.author("yourname") // 作者名.outputDir(System.getProperty("user.dir") + "/src/main/java") // 输出目录(项目的java目录).commentDate("yyyy-MM-dd") // 注释日期格式.disableOpenDir() // 生成后不自动打开文件夹.enableSwagger() // 开启Swagger注解(需引入依赖).fileOverride(); // 覆盖已有文件})// 2. 包配置(指定生成的类存放的包路径).packageConfig(builder -> {builder.parent("com.example") // 父包名.moduleName("system") // 模块名(可选,如多模块项目).entity("entity") // 实体类包名.mapper("mapper") // Mapper接口包名.service("service") // Service接口包名.serviceImpl("service.impl") // Service实现类包名.controller("controller") // Controller包名.xml("mapper.xml") // Mapper XML存放路径(resources目录下).pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); // XML输出路径})// 3. 数据库表配置.dataSourceConfig(builder -> {builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {// 自定义数据库类型转换(如将DECIMAL转为BigDecimal)return typeRegistry.getColumnType(metaInfo);});})// 4. 策略配置.strategyConfig(builder -> {builder// 指定要生成的表(支持正则表达式,如 "user_*" 生成所有以user_开头的表).addInclude("user", "order") // 排除不需要生成的表.addExclude("sys_log")// 实体类策略.entityBuilder().enableLombok() // 开启Lombok注解.enableChainModel() // 开启链式调用(如 user.setName("").setAge(18)).naming(NamingStrategy.underline_to_camel) // 表名转驼峰(如 t_user -> User).columnNaming(NamingStrategy.underline_to_camel) // 列名转驼峰.idType(com.baomidou.mybatisplus.generator.config.rules.IdType.AUTO) // 主键策略.enableTableFieldAnnotation() // 为字段添加@TableField注解// Mapper策略.mapperBuilder().superClass(BaseMapper.class) // 继承BaseMapper.enableMapperAnnotation() // 开启@Mapper注解.enableBaseResultMap() // 生成基本的resultMap// Service策略.serviceBuilder().formatServiceFileName("%sService") // Service接口名格式(如 UserService).formatServiceImplFileName("%sServiceImpl") // Service实现类名格式// Controller策略.controllerBuilder().enableRestStyle() // 开启REST风格(使用@RestController).enableHyphenStyle() // 路径中驼峰转连字符(如 getUserById -> get-user-by-id).formatFileName("%sController"); // Controller类名格式})// 5. 模板引擎配置(默认Velocity).templateEngine(new VelocityTemplateEngine())// 6. 执行生成.execute();}
}
四、核心配置详解
1. 全局配置(globalConfig
)
author
:生成代码的作者名(会添加到类注释中)。outputDir
:代码输出目录(通常为src/main/java
)。fileOverride
:是否覆盖已有文件(true
表示覆盖,适合表结构更新后重新生成)。enableSwagger
:生成 Swagger 注解(如@Api
、@ApiModelProperty
),需引入 Swagger 依赖。disableOpenDir
:生成后不自动打开输出目录(避免频繁弹窗)。
2. 包配置(packageConfig
)
parent
:父包名(如com.example
),所有生成的类都会放在该包下。moduleName
:模块名(如system
、user
),用于多模块项目区分。pathInfo
:指定非 Java 资源(如 Mapper XML)的输出路径(通常放在resources/mapper
下)。
3. 策略配置(strategyConfig
)
策略配置是代码生成的核心,控制生成类的命名规则、注解、继承关系等:
-
表匹配:
addInclude("user", "order")
:指定生成哪些表(必填)。addExclude("sys_log")
:排除不需要生成的表。
-
实体类策略(
entityBuilder
):enableLombok()
:生成 Lombok 注解(@Data
、@Builder
等),避免手动编写 getter/setter。naming(NamingStrategy.underline_to_camel)
:数据库表名(下划线)转实体类名(驼峰),如t_user
→User
。idType(IdType.AUTO)
:主键生成策略(与实体类@TableId
对应)。
-
Mapper策略(
mapperBuilder
):superClass(BaseMapper.class)
:让生成的 Mapper 接口继承BaseMapper
,获得基础 CRUD 方法。enableBaseResultMap()
:生成基本的resultMap
(字段映射),适合字段名与属性名不一致的场景。
-
Service策略(
serviceBuilder
):- 生成的 Service 接口继承
IService
,实现类继承ServiceImpl<Mapper, Entity>
。 formatServiceFileName("%sService")
:指定 Service 接口命名格式(如UserService
)。
- 生成的 Service 接口继承
-
Controller策略(
controllerBuilder
):enableRestStyle()
:生成@RestController
注解(RESTful API 风格)。enableHyphenStyle()
:URL 路径中驼峰转连字符,如getUserById
→/get-user-by-id
。
五、自定义模板(进阶)
如果默认生成的代码格式不符合项目规范,可通过自定义模板修改生成逻辑:
1. 获取默认模板
MP 代码生成器的默认模板位于 mybatis-plus-generator-${version}.jar
的 templates
目录下,包含:
entity.java.vm
(实体类模板)mapper.java.vm
(Mapper 接口模板)service.java.vm
(Service 接口模板)- 等其他模板文件。
2. 自定义模板步骤
- 复制默认模板:将需要修改的模板文件复制到项目的
src/main/resources/templates
目录下。 - 修改模板:根据项目需求修改模板内容(使用 Velocity 语法)。例如,在实体类中添加自定义注解:
// 自定义 entity.java.vm 片段 #if(${entityLombokModel}) @Data @ApiModel("${entity.comment!}") // 自定义 Swagger 注解 #else // ... #end
- 指定自定义模板路径:在生成器配置中添加模板路径:
.templateConfig(builder -> {builder.entity("/templates/entity.java") // 自定义实体类模板.mapper("/templates/mapper.java") // 自定义Mapper模板// 其他模板... })
六、注意事项
- 数据库权限:确保数据库用户有足够权限(如
SELECT
权限),否则无法读取表结构。 - 表注释与字段注释:生成的代码注释会读取数据库表和字段的注释,建议提前完善数据库注释。
- 多次生成:表结构更新后,需设置
fileOverride(true)
覆盖旧文件,但自定义修改过的代码会被覆盖(建议先生成基础代码,再手动调整)。 - 版本兼容:代码生成器版本需与 MyBatis-Plus 核心包版本一致,避免兼容性问题。
- 多数据源支持:如需生成多数据源的代码,可通过
dataSourceConfig
分别配置不同数据源。
七、生成代码结构示例
执行代码生成器后,项目中会生成如下结构的代码:
com.example.system
├── entity
│ └── User.java // 实体类(带Lombok、Swagger注解)
├── mapper
│ ├── UserMapper.java // Mapper接口(继承BaseMapper)
│ └── UserMapper.xml // Mapper XML文件
├── service
│ ├── UserService.java // Service接口(继承IService)
│ └── impl
│ └── UserServiceImpl.java // Service实现类
└── controller└── UserController.java // Controller(REST风格接口)
总结
MyBatis-Plus 代码生成器通过简洁的配置实现了全套代码的自动生成,核心优势在于:
- 减少 80% 以上的重复编码工作,专注业务逻辑。
- 支持高度自定义(包路径、命名规则、模板内容),适配不同项目规范。
- 集成 Lombok、Swagger 等工具,生成的代码符合现代开发习惯。
实际使用中,建议先通过默认配置生成基础代码,再根据项目需求调整模板或手动修改生成的代码,平衡效率与灵活性。
13、MyBatis-Plus的多数据源
MyBatis-Plus 的多数据源功能主要用于解决项目中需要同时操作多个数据库的场景(如读写分离、业务分库、跨库联查等)。其底层依赖第三方库 dynamic-datasource-spring-boot-starter
实现,支持数据源动态切换、负载均衡、事务隔离等特性,使用简单且扩展性强。以下是详细解析:
一、多数据源应用场景
- 读写分离:主库(Master)负责写入操作,从库(Slave)负责查询操作,提升数据库吞吐量。
- 业务分库:不同业务模块使用独立数据库(如用户库、订单库、商品库),降低单库压力。
- 跨库联查:需同时操作多个数据库的数据(如统计报表、数据迁移)。
- 多租户隔离:不同租户数据存储在独立数据库,通过数据源切换实现隔离。
二、实现依赖
MyBatis-Plus 多数据源依赖 dynamic-datasource-spring-boot-starter
(一个轻量级的多数据源管理框架),需在项目中引入以下依赖:
<!-- MyBatis-Plus 核心依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency><!-- 多数据源核心依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.2</version>
</dependency><!-- 数据库驱动(以 MySQL 为例,多库可引入多个驱动) -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version>
</dependency>
三、核心配置(多数据源配置)
通过 application.yml
配置多个数据源,指定主数据源(默认数据源)和其他数据源,并设置 MyBatis-Plus 相关参数。
MyBatis-Plus多数据源配置:
spring:# 多数据源配置datasource:dynamic:primary: master # 默认数据源(必填)strict: false # 是否严格匹配数据源(false则找不到数据源时使用默认数据源)datasource:# 主库(写入)master:url: jdbc:mysql://localhost:3306/master_db?serverTimezone=UTC&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# 从库1(查询)slave1:url: jdbc:mysql://localhost:3306/slave1_db?serverTimezone=UTC&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# 从库2(查询,可选,用于负载均衡)slave2:url: jdbc:mysql://localhost:3306/slave2_db?serverTimezone=UTC&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# 业务库(如订单库)order:url: jdbc:mysql://localhost:3306/order_db?serverTimezone=UTC&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# MyBatis-Plus 配置
mybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML路径type-aliases-package: com.example.entity # 实体类包路径configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志global-config:db-config:id-type: auto # 主键策略
四、数据源切换:@DS
注解
dynamic-datasource
提供 @DS
注解实现数据源动态切换,可标注在类或方法上,优先级:方法注解 > 类注解。
1. 注解参数说明
@DS("master")
:指定使用名为master
的数据源(对应配置中的datasource
节点 key)。@DS("slave1")
:指定使用slave1
数据源。@DS("order")
:指定使用order
业务库数据源。@DS("#{user.dsKey}")
:支持 SpEL 表达式(动态从参数中获取数据源名称)。
2. 使用示例
(1)在 Service 类上标注(统一切换)
// 订单相关操作统一使用 order 数据源
@Service
@DS("order")
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {// 所有方法默认使用 order 数据源public Order getOrderById(Long id) {return getById(id);}
}
(2)在方法上标注(灵活切换)
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// 写入操作:使用主库@Override@DS("master")public boolean saveUser(User user) {return save(user);}// 查询操作:使用从库1@Override@DS("slave1")public User getUserById(Long id) {return getById(id);}// 复杂查询:动态指定数据源(SpEL表达式)@Override@DS("#{query.dsType}") // 从参数 query 的 dsType 字段获取数据源名称public List<User> queryUsers(UserQuery query) {return lambdaQuery().eq(User::getAge, query.getAge()).list();}
}
(3)在 Controller 层标注(不推荐,建议在 Service 层控制)
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;// 查询走从库@GetMapping("/{id}")@DS("slave1")public User getUser(@PathVariable Long id) {return userService.getById(id);}
}
五、高级特性
1. 数据源分组与负载均衡
对同类型数据源(如多个从库)分组,实现负载均衡(默认轮询策略)。
配置示例:
spring:datasource:dynamic:datasource:master: # 主库# ...slave_1: # 从库1(分组名为 slave)url: jdbc:mysql://localhost:3306/slave1_db# ...slave_2: # 从库2(分组名为 slave)url: jdbc:mysql://localhost:3306/slave2_db# ...
使用分组数据源:
@Service
public class UserServiceImpl {// 使用 slave 分组(自动在 slave_1 和 slave_2 之间轮询)@DS("slave")public List<User> listUsers() {return list();}
}
2. 编程式切换数据源
除注解外,可通过 DynamicDataSourceContextHolder
手动切换数据源(适合复杂逻辑):
import com.baomidou.dynamic.datasource.DynamicDataSourceContextHolder;@Service
public class DataSyncService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate OrderMapper orderMapper;// 同步用户数据到订单库public void syncUserToOrderDb(Long userId) {// 1. 从主库查询用户DynamicDataSourceContextHolder.push("master"); // 切换到 masterUser user = userMapper.selectById(userId);// 2. 写入订单库DynamicDataSourceContextHolder.push("order"); // 切换到 orderOrder order = new Order();order.setUserId(user.getId());order.setUserName(user.getName());orderMapper.insert(order);// 3. 清除上下文(避免线程污染)DynamicDataSourceContextHolder.clear();}
}
3. 多数据源事务
多数据源事务默认不支持跨库原子性(单数据源事务正常生效)。如需跨库事务,需结合分布式事务框架(如 Seata):
<!-- 引入 Seata 依赖 -->
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.6.1</version>
</dependency>
使用分布式事务:
@Service
public class MultiDbTransactionService {@Autowiredprivate UserService userService;@Autowiredprivate OrderService orderService;// 跨库事务(需 Seata 支持)@GlobalTransactional // Seata 分布式事务注解public void createUserAndOrder(User user, Order order) {userService.save(user); // 操作 master 库orderService.save(order); // 操作 order 库// 若任意一步失败,两库操作同时回滚}
}
六、注意事项
- 数据源名称匹配:
@DS
注解的参数必须与application.yml
中datasource
节点的 key 一致(如master
、slave1
)。 - 线程安全:
DynamicDataSourceContextHolder
基于ThreadLocal
实现,确保多线程环境下数据源隔离。 - 事务与数据源的关系:单数据源事务(
@Transactional
)正常生效;跨数据源事务需分布式事务支持,否则可能出现数据不一致。 - 避免过度使用:多数据源会增加系统复杂度,非必要场景(如单库可满足)不建议使用。
- 性能优化:对查询密集型业务,建议使用从库分组 + 负载均衡,分散查询压力。
总结
MyBatis-Plus 多数据源通过 dynamic-datasource-spring-boot-starter
实现,核心优势在于:
- 配置简单:通过 YAML 配置多个数据源,无需手动编写数据源切换逻辑。
- 切换灵活:支持
@DS
注解和编程式切换,满足不同场景需求。 - 扩展性强:支持数据源分组、负载均衡、分布式事务等高级特性。
实际使用中,需根据业务场景(如读写分离、业务分库)合理配置数据源,并注意事务一致性和性能优化,避免引入不必要的复杂度。
14.1、MyBatis-Plus分页插件
MyBatis-Plus 的分页插件(PaginationInnerInterceptor)是其核心增强功能之一,能够无需 needing to manually write pagination SQL (such as LIMIT
in MySQL or ROW_NUMBER()
in SQL Server). It integrates seamlessly with MyBatis-Plus’s query methods, enabling pagination with minimal configuration. Below is a detailed explanation of its usage:
一、分页插件的核心作用
- 自动分页:拦截查询 SQL 并自动添加分页条件(如
LIMIT ?,?
或OFFSET ... ROWS FETCH NEXT ... ROWS ONLY
)。 - 支持多种数据库:适配 MySQL、Oracle、SQL Server、PostgreSQL 等主流数据库。
- 与条件构造器结合:可配合
QueryWrapper
或LambdaQueryWrapper
实现带条件的分页查询。 - 返回分页元数据:包含总记录数、总页数、当前页码、每页条数等信息。
二、使用步骤
1. 引入依赖
确保项目已引入 MyBatis-Plus 核心依赖(分页插件包含在 mybatis-plus-boot-starter
中):
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
2. 配置分页插件
需要手动注册分页插件到 Spring 容器中(这是使用分页功能的关键步骤):MyBatis-Plus分页插件配置:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@MapperScan("com.example.mapper") // 扫描Mapper接口包
public class MyBatisPlusConfig {/*** 注册MyBatis-Plus拦截器(包含分页插件)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 设置数据库类型(可选,自动识别时可省略)paginationInnerInterceptor.setDbType(com.baomidou.mybatisplus.annotation.DbType.MYSQL);// 溢出总页数时是否进行处理(true:返回最后一页,false:继续请求会报错)paginationInnerInterceptor.setOverflow(true);interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}
3. 基本分页查询
使用 Page
类指定分页参数(页码、每页条数),结合 BaseMapper
或 IService
的分页方法实现查询。
(1)基于 BaseMapper
的分页
@Autowired
private UserMapper userMapper;// 分页查询:第1页,每页10条记录
public IPage<User> getUserPage(Integer pageNum, Integer pageSize) {// 创建分页对象(pageNum:页码,从1开始;pageSize:每页条数)Page<User> page = new Page<>(pageNum, pageSize);// 调用selectPage方法(第二个参数为查询条件,null表示无条件)IPage<User> userPage = userMapper.selectPage(page, null);// 分页结果解析long total = userPage.getTotal(); // 总记录数long pages = userPage.getPages(); // 总页数List<User> records = userPage.getRecords(); // 当前页数据return userPage;
}
(2)基于 IService
的分页(推荐)
@Autowired
private UserService userService;// 带条件的分页查询:查询年龄>20的用户,第2页,每页5条
public IPage<User> getUserPageWithCondition() {Page<User> page = new Page<>(2, 5); // 第2页,每页5条// 构建查询条件LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.gt(User::getAge, 20); // 年龄>20// 调用page方法IPage<User> userPage = userService.page(page, queryWrapper);return userPage;
}
4. 分页查询结果(IPage
)解析
IPage
接口封装了分页的核心信息,常用方法如下:
方法名 | 说明 |
---|---|
getRecords() | 获取当前页的数据列表 |
getTotal() | 获取总记录数 |
getPages() | 获取总页数(total / pageSize 向上取整) |
getCurrent() | 获取当前页码 |
getSize() | 获取每页条数 |
hasNext() | 是否有下一页(current < pages ) |
hasPrevious() | 是否有上一页(current > 1 ) |
三、高级用法
1. 自定义 SQL 分页
对于自定义 SQL(XML 或注解方式),只需在 Mapper 方法参数中添加 Page
对象,并在 SQL 中正常编写查询逻辑(分页插件会自动拦截并添加分页条件)。
(1)XML 方式
// UserMapper.java
IPage<User> selectUserByAge(Page<User> page, @Param("minAge") Integer minAge);
<!-- UserMapper.xml -->
<select id="selectUserByAge" resultType="com.example.entity.User">SELECT * FROM user WHERE age > #{minAge}
</select>
(2)注解方式
@Select("SELECT * FROM user WHERE email LIKE #{emailPattern}")
IPage<User> selectUserByEmail(Page<User> page, @Param("emailPattern") String emailPattern);
(3)调用自定义分页方法
Page<User> page = new Page<>(1, 10);
IPage<User> result = userMapper.selectUserByAge(page, 18);
// 结果解析同前
2. 分页排序
结合条件构造器的 orderBy
方法实现分页排序:
// 分页查询并按年龄降序、ID升序排序
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(User::getAge).orderByAsc(User::getId);IPage<User> userPage = userService.page(new Page<>(1, 10), queryWrapper);
3. 分页插件参数配置
分页插件支持更多个性化配置,例如:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();PaginationInnerInterceptor pagination = new PaginationInnerInterceptor();// 设置默认每页条数(未指定时生效)pagination.setDefaultLimit(20);// 禁用count查询(仅获取当前页数据,不查询总记录数,提升性能)pagination.setCountSqlParser(new JsqlParserCountOptimize(true)); // 设置最大单页限制(防止恶意请求大量数据)pagination.setMaxLimit(1000L);interceptor.addInnerInterceptor(pagination);return interceptor;
}
四、注意事项
- 分页插件必须配置:未注册
PaginationInnerInterceptor
会导致分页失效(查询所有数据)。 - 页码从1开始:
Page
构造器的pageNum
参数默认从1开始(而非0)。 - 总记录数查询:分页插件会自动执行
COUNT(*)
语句查询总记录数,若无需总记录数,可通过setCountSqlParser
禁用(见高级配置)。 - 多表关联分页:自定义多表关联 SQL 时,分页插件同样生效,但需确保 SQL 语法正确。
- 与其他插件兼容:分页插件可与乐观锁插件、动态数据源插件等同时使用,注意拦截器注册顺序(无特殊顺序要求)。
总结
MyBatis-Plus 分页插件通过简单配置即可实现自动分页,核心步骤是:
- 注册
PaginationInnerInterceptor
拦截器。 - 使用
Page
类指定分页参数。 - 调用
selectPage
(Mapper)或page
(Service)方法执行分页查询。
其优势在于无需编写分页 SQL,自动适配多种数据库,且能与条件构造器无缝结合,极大简化了分页功能的实现。实际开发中,建议根据业务需求合理设置分页参数(如最大单页限制),并优化查询性能。
14.2、MyBatis-Plus乐观锁和悲观锁插件
在并发场景下,数据库数据的一致性至关重要。MyBatis-Plus 提供了乐观锁和悲观锁的解决方案,分别适用于不同的并发场景。以下是两者的详细介绍和使用方法:
一、乐观锁(Optimistic Lock)
乐观锁假设并发操作不会频繁冲突,因此不主动加锁,而是通过版本控制机制实现数据一致性。适用于读多写少的场景(如商品库存、用户积分)。
1. 实现原理
- 在数据库表中添加
version
字段(版本号,初始值为 0)。 - 更新数据时,条件中会自动包含
version = 旧版本号
,并将版本号 +1。 - 若版本号不匹配(表示数据已被其他线程修改),则更新失败(返回影响行数为 0)。
示例 SQL:
-- 更新时自动添加版本条件
UPDATE user SET name = '张三', version = 2 WHERE id = 1 AND version = 1;
2. 使用步骤
(1)数据库表添加版本字段
ALTER TABLE `user` ADD COLUMN `version` INT NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁';
(2)实体类添加 @Version
注解
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;@Data
public class User {private Long id;private String name;private Integer age;@Version // 标记乐观锁版本字段private Integer version;
}
(3)配置乐观锁插件
MyBatis-Plus乐观锁插件配置:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyBatisPlusConfig {/*** 注册乐观锁插件*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加乐观锁拦截器interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}
(4)使用乐观锁更新数据
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic boolean updateUserAge(Long id, Integer newAge) {// 1. 查询数据(获取当前版本号)User user = getById(id);if (user == null) {return false;}// 2. 修改字段user.setAge(newAge);// 3. 更新(MP会自动处理版本号)return updateById(user);}
}
执行流程:
- 查询时获取当前版本号(如
version = 1
)。 - 更新时 SQL 自动添加
WHERE version = 1
,并设置version = 2
。 - 若期间有其他线程修改过数据(版本号已变为 2),则本次更新失败(返回
false
)。
3. 冲突处理
当乐观锁更新失败时,通常需要重试机制:
@Override
@Transactional
public boolean updateWithRetry(Long id, Integer newAge) {int retryCount = 3; // 最多重试3次while (retryCount > 0) {try {User user = getById(id);user.setAge(newAge);boolean success = updateById(user);if (success) {return true;}} catch (Exception e) {// 处理异常}retryCount--;// 可选:重试前休眠一段时间Thread.sleep(100);}return false; // 多次重试失败
}
二、悲观锁(Pessimistic Lock)
悲观锁假设并发操作会频繁冲突,因此在操作数据时主动加锁,阻止其他线程修改。适用于写多读少的场景(如订单创建、库存扣减)。
MyBatis-Plus 本身不提供专门的悲观锁插件,而是通过 SQL 的 FOR UPDATE
语句实现,需结合事务使用。
1. 实现原理
- 查询数据时添加
FOR UPDATE
子句,对查询的行加行级锁(或表锁,取决于数据库)。 - 其他线程尝试修改或查询加锁的数据时会阻塞,直到锁释放。
- 事务提交或回滚后,锁自动释放。
2. 使用步骤
(1)在 Mapper 中定义带悲观锁的查询方法
public interface UserMapper extends BaseMapper<User> {// 悲观锁查询(FOR UPDATE)@Select("SELECT * FROM user WHERE id = #{id} FOR UPDATE")User selectByIdWithLock(@Param("id") Long id);
}
(2)在 Service 中结合事务使用
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// 必须添加事务注解,否则锁会立即释放@Override@Transactionalpublic boolean updateUserWithPessimisticLock(Long id, Integer newAge) {// 1. 加锁查询(其他线程无法修改该记录,直到事务结束)User user = baseMapper.selectByIdWithLock(id);if (user == null) {return false;}// 2. 业务逻辑处理(如复杂计算)user.setAge(newAge);// 3. 更新数据return updateById(user);}
}
注意:
- 必须在事务(
@Transactional
)中使用FOR UPDATE
,否则锁会在查询后立即释放。 - 锁的粒度由数据库决定(MySQL InnoDB 为行级锁,需索引匹配;否则为表锁)。
3. 悲观锁的风险与规避
- 死锁:多个线程相互等待对方释放锁,导致无限阻塞。
规避:控制事务执行时间,按固定顺序操作资源。 - 性能下降:锁会阻塞其他线程,高并发下可能导致响应变慢。
规避:仅在必要场景使用,减少锁持有时间。
三、乐观锁 vs 悲观锁对比
维度 | 乐观锁 | 悲观锁 |
---|---|---|
实现方式 | 版本号(@Version + 插件) | SQL FOR UPDATE + 事务 |
并发策略 | 假设冲突少,不加锁,冲突后重试 | 假设冲突多,主动加锁,阻塞其他线程 |
适用场景 | 读多写少(如商品详情、用户资料) | 写多读少(如订单提交、库存扣减) |
性能 | 高(无锁竞争) | 低(可能阻塞) |
实现复杂度 | 简单(MP 插件支持) | 中等(需手动写 SQL 和控制事务) |
冲突处理 | 需要手动实现重试机制 | 自动阻塞等待,无需重试 |
四、最佳实践
- 优先使用乐观锁:乐观锁性能更高,适合大多数低冲突场景。
- 悲观锁谨慎使用:仅在写操作频繁、冲突概率高的场景使用,且需控制事务范围。
- 结合业务设计:
- 乐观锁:适合库存扣减(允许有限重试)、用户资料更新等。
- 悲观锁:适合订单创建(防止超卖)、资金转账等强一致性场景。
- 避免长时间持有锁:无论是乐观锁的重试还是悲观锁的事务,都应尽量缩短操作时间。
总结
MyBatis-Plus 的乐观锁通过 @Version
注解和插件实现,无需手动编写版本控制逻辑,适合低冲突场景;悲观锁则需通过 FOR UPDATE
语句结合事务实现,适合高冲突场景。实际开发中,应根据业务的并发特点选择合适的锁机制,平衡数据一致性和系统性能。
15、源码
配套源码-MyBatis核心技术全解与项目实战