一、基础概念与配置
1.1 Spring Boot与MyBatis简介
技术 | 描述 | 优点 |
---|---|---|
Spring Boot | 简化Spring应用开发的框架,提供自动配置、快速启动等特性 | 快速开发、内嵌服务器、自动配置、无需XML配置 |
MyBatis | 持久层框架,将Java对象与SQL语句映射,避免了几乎所有的JDBC代码和参数 | SQL灵活可控、学习成本低、与Spring集成良好、性能接近直接使用JDBC |
通俗理解:Spring Boot像是装修好的房子(提供各种便利设施),MyBatis像是专业的管道工(专门处理数据流动),两者结合让你能快速搭建高效的数据处理系统。
1.2 项目初始化与基础配置
步骤1:创建Spring Boot项目并添加依赖
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</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>
</dependencies>
步骤2:配置数据库连接
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml # mapper文件位置
type-aliases-package: com.example.demo.entity # 实体类包名
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名自动转换
步骤3:创建实体类
// User.java
@Data // Lombok注解,自动生成getter/setter等方法
public class User {
private Long id;
private String username;
private String password;
private String email;
private Date createTime;
private Date updateTime;
}
步骤4:创建Mapper接口
// UserMapper.java
@Mapper // 标识这是一个MyBatis的Mapper接口
public interface UserMapper {
// 根据ID查询用户
User selectById(@Param("id") Long id);
// 查询所有用户
List<User> selectAll();
// 插入用户
int insert(User user);
// 更新用户
int update(User user);
// 删除用户
int deleteById(@Param("id") Long id);
}
步骤5:创建Mapper XML文件
<!-- resources/mapper/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.demo.mapper.UserMapper">
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, username, password, email, create_time, update_time
</sql>
<select id="selectById" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM user
WHERE id = #{id}
</select>
<select id="selectAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM user
</select>
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user
(username, password, email, create_time, update_time)
VALUES
(#{username}, #{password}, #{email}, now(), now())
</insert>
<update id="update" parameterType="User">
UPDATE user
SET
username = #{username},
password = #{password},
email = #{email},
update_time = now()
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
步骤6:创建Service层
// UserService.java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
public List<User> getAllUsers() {
return userMapper.selectAll();
}
public int addUser(User user) {
return userMapper.insert(user);
}
public int updateUser(User user) {
return userMapper.update(user);
}
public int deleteUser(Long id) {
return userMapper.deleteById(id);
}
}
步骤7:创建Controller层
// UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@PostMapping
public ResponseEntity<Void> addUser(@RequestBody User user) {
userService.addUser(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping
public ResponseEntity<Void> updateUser(@RequestBody User user) {
userService.updateUser(user);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
二、基本CRUD操作
2.1 查询操作详解
MyBatis提供了多种查询方式,下面通过表格对比各种查询方法:
方法类型 | 使用场景 | 示例 | 优点 | 缺点 |
---|---|---|---|---|
简单查询 | 根据ID等简单条件查询 | User selectById(Long id) | 简单直接 | 功能有限 |
条件查询 | 多条件组合查询 | List<User> selectByCondition(User user) | 灵活 | 需要处理多个参数 |
分页查询 | 大数据量分页显示 | List<User> selectByPage(RowBounds rb) | 内存友好 | 需要额外处理分页逻辑 |
注解方式查询 | 简单SQL直接写在接口上 | @Select("SELECT * FROM user") | 无需XML文件 | 复杂SQL可读性差 |
结果集映射 | 处理复杂结果集 | 使用@ResultMap 或XML中的<resultMap> | 处理复杂关系 | 配置稍复杂 |
示例1:条件查询
// UserMapper.java
List<User> selectByCondition(@Param("username") String username,
@Param("email") String email);
<!-- UserMapper.xml -->
<select id="selectByCondition" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>
示例2:注解方式查询
// UserMapper.java
@Select("SELECT * FROM user WHERE email = #{email}")
@Results(id = "userResultMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "username", column = "username"),
@Result(property = "email", column = "email")
})
User selectByEmail(String email);
2.2 插入操作详解
插入操作需要考虑主键生成策略、批量插入等问题。
主键生成策略对比:
策略 | 描述 | 适用场景 |
---|---|---|
useGeneratedKeys | 使用数据库自增主键,返回生成的主键 | MySQL、PostgreSQL等支持自增的数据库 |
selectKey | 在执行插入前或后执行SQL获取主键 | Oracle序列、特殊主键生成需求 |
应用层生成 | 在Java代码中生成UUID等主键 | 分布式系统、需要提前知道主键 |
示例1:使用自增主键
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, email) VALUES(#{username}, #{email})
</insert>
示例2:批量插入
// UserMapper.java
int batchInsert(@Param("users") List<User> users);
<!-- UserMapper.xml -->
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
2.3 更新与删除操作
更新和删除操作相对简单,但需要注意乐观锁、逻辑删除等场景。
示例1:带乐观锁的更新
// User.java
@Data
public class User {
// ...其他字段
private Integer version; // 版本号
}
<!-- UserMapper.xml -->
<update id="updateWithVersion" parameterType="User">
UPDATE user
SET
username = #{username},
email = #{email},
version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
示例2:逻辑删除
// User.java
@Data
public class User {
// ...其他字段
private Boolean deleted; // 删除标志
}
<!-- UserMapper.xml -->
<update id="logicalDelete">
UPDATE user SET deleted = 1 WHERE id = #{id}
</update>
<select id="selectAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM user
WHERE deleted = 0
</select>
三、动态SQL
MyBatis提供了强大的动态SQL功能,可以根据不同条件生成不同的SQL语句。
3.1 常用动态SQL元素
元素 | 描述 | 示例 |
---|---|---|
if | 条件判断 | <if test="name != null">AND name = #{name}</if> |
choose/when | 多条件选择(类似Java的switch) | <choose><when test="id != null">AND id = #{id}</when>...</choose> |
where | 智能处理WHERE关键字和AND/OR前缀 | <where><if test="name != null">AND name = #{name}</if></where> |
set | 智能处理UPDATE语句中的SET部分 | <set><if test="name != null">name = #{name},</if></set> |
foreach | 循环集合,常用于IN条件或批量操作 | <foreach item="item" collection="list" open="(" separator="," close=")">#{item}</foreach> |
trim | 自定义字符串截取,可以替代where或set | <trim prefix="WHERE" prefixOverrides="AND">...</trim> |
bind | 创建变量并绑定到上下文 | <bind name="pattern" value="'%' + name + '%'" /> |
3.2 动态SQL示例
示例1:复杂条件查询
<select id="selectByComplexCondition" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM user
<where>
<if test="user.username != null and user.username != ''">
AND username LIKE CONCAT('%', #{user.username}, '%')
</if>
<if test="user.email != null and user.email != ''">
AND email = #{user.email}
</if>
<if test="createTimeStart != null">
AND create_time >= #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND create_time <= #{createTimeEnd}
</if>
<choose>
<when test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</when>
<otherwise>
AND status = 1
</otherwise>
</choose>
</where>
ORDER BY
<choose>
<when test="orderBy != null and orderBy != ''">
${orderBy}
</when>
<otherwise>
id DESC
</otherwise>
</choose>
</select>
示例2:动态更新
<update id="updateSelective" parameterType="User">
UPDATE user
<set>
<if test="username != null">
username = #{username},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="password != null">
password = #{password},
</if>
update_time = now()
</set>
WHERE id = #{id}
</update>
示例3:批量插入或更新
<insert id="upsertUsers">
INSERT INTO user (id, username, email)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.id}, #{user.username}, #{user.email})
</foreach>
ON DUPLICATE KEY UPDATE
username = VALUES(username),
email = VALUES(email),
update_time = now()
</insert>
四、关联关系映射
处理数据库表之间的关联关系是ORM框架的重要功能,MyBatis提供了多种方式来处理关联关系。
4.1 关联关系类型
关系类型 | 描述 | MyBatis处理方式 |
---|---|---|
一对一 | 如用户和身份证 | <association> 标签或注解@One |
一对多 | 如部门和员工 | <collection> 标签或注解@Many |
多对多 | 如学生和课程 | 通过中间表实现,结合一对多和多对一 |
嵌套结果 | 单SQL查询获取所有关联数据 | 使用嵌套的<association> 和<collection> |
嵌套查询 | 通过额外SQL查询获取关联数据 | 使用select 属性指定额外查询 |
4.2 关联关系示例
场景:博客系统,包含用户(User)、文章(Article)和评论(Comment)三个实体。
实体类:
// User.java
@Data
public class User {
private Long id;
private String username;
private String email;
private List<Article> articles; // 用户写的文章(一对多)
}
// Article.java
@Data
public class Article {
private Long id;
private String title;
private String content;
private Long userId;
private User author; // 文章作者(多对一)
private List<Comment> comments; // 文章的评论(一对多)
private Date createTime;
}
// Comment.java
@Data
public class Comment {
private Long id;
private String content;
private Long articleId;
private Long userId;
private User commenter; // 评论者(多对一)
private Date createTime;
}
方式1:XML配置关联关系
<!-- UserMapper.xml -->
<resultMap id="UserWithArticlesMap" type="User" extends="BaseResultMap">
<collection property="articles" ofType="Article" column="id"
select="com.example.demo.mapper.ArticleMapper.selectByUserId"/>
</resultMap>
<select id="selectUserWithArticles" resultMap="UserWithArticlesMap">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- ArticleMapper.xml -->
<resultMap id="ArticleWithCommentsMap" type="Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<association property="author" javaType="User" column="user_id"
select="com.example.demo.mapper.UserMapper.selectById"/>
<collection property="comments" ofType="Comment" column="id"
select="com.example.demo.mapper.CommentMapper.selectByArticleId"/>
</resultMap>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM article WHERE user_id = #{userId}
</select>
<select id="selectArticleWithComments" resultMap="ArticleWithCommentsMap">
SELECT * FROM article WHERE id = #{id}
</select>
<!-- CommentMapper.xml -->
<resultMap id="CommentWithUserMap" type="Comment">
<id property="id" column="id"/>
<result property="content" column="content"/>
<association property="commenter" javaType="User" column="user_id"
select="com.example.demo.mapper.UserMapper.selectById"/>
</resultMap>
<select id="selectByArticleId" resultMap="BaseResultMap">
SELECT * FROM comment WHERE article_id = #{articleId}
</select>
方式2:注解配置关联关系
// UserMapper.java
@Select("SELECT * FROM user WHERE id = #{id}")
@Results(id = "userWithArticles", value = {
@Result(property = "id", column = "id"),
@Result(property = "articles", column = "id",
many = @Many(select = "com.example.demo.mapper.ArticleMapper.selectByUserId"))
})
User selectUserWithArticles(Long id);
// ArticleMapper.java
@Select("SELECT * FROM article WHERE id = #{id}")
@Results(id = "articleWithComments", value = {
@Result(property = "id", column = "id"),
@Result(property = "author", column = "user_id",
one = @One(select = "com.example.demo.mapper.UserMapper.selectById")),
@Result(property = "comments", column = "id",
many = @Many(select = "com.example.demo.mapper.CommentMapper.selectByArticleId"))
})
Article selectArticleWithComments(Long id);
方式3:单SQL查询嵌套结果
<!-- UserMapper.xml -->
<resultMap id="UserWithArticlesNestedMap" type="User">
<id property="id" column="u_id"/>
<result property="username" column="u_username"/>
<result property="email" column="u_email"/>
<collection property="articles" ofType="Article" resultMap="articleNestedMap"/>
</resultMap>
<resultMap id="articleNestedMap" type="Article">
<id property="id" column="a_id"/>
<result property="title" column="a_title"/>
<result property="content" column="a_content"/>
<collection property="comments" ofType="Comment" resultMap="commentNestedMap"/>
</resultMap>
<resultMap id="commentNestedMap" type="Comment">
<id property="id" column="c_id"/>
<result property="content" column="c_content"/>
</resultMap>
<select id="selectUserWithArticlesNested" resultMap="UserWithArticlesNestedMap">
SELECT
u.id as u_id, u.username as u_username, u.email as u_email,
a.id as a_id, a.title as a_title, a.content as a_content,
c.id as c_id, c.content as c_content
FROM user u
LEFT JOIN article a ON u.id = a.user_id
LEFT JOIN comment c ON a.id = c.article_id
WHERE u.id = #{id}
</select>
4.3 关联关系加载策略
加载策略 | 描述 | 优点 | 缺点 |
---|---|---|---|
嵌套查询 | 执行主查询后,对每个关联对象执行额外查询 | SQL简单清晰 | N+1查询问题,性能可能较差 |
嵌套结果 | 单SQL查询,通过表连接获取所有数据,MyBatis负责结果集映射 | 减少数据库交互,性能好 | SQL可能复杂,结果集可能冗余 |
延迟加载 | 只有访问关联对象时才加载数据 | 初始加载快,节省资源 | 可能导致后续延迟,需要配置 |
批量加载 | 对嵌套查询进行优化,将多个单独查询合并为批量查询 | 减少数据库交互次数 | 配置稍复杂 |
配置延迟加载:
# application.yml
mybatis:
configuration:
lazy-loading-enabled: true # 开启延迟加载
aggressive-lazy-loading: false # 禁用激进延迟加载
五、缓存机制
MyBatis提供了一级缓存和二级缓存机制,合理使用可以显著提高性能。
5.1 一级缓存与二级缓存对比
特性 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession级别 | Mapper(Namespace)级别 |
默认状态 | 开启 | 关闭 |
生命周期 | SqlSession结束即清除 | 应用程序生命周期,可配置清除策略 |
如何开启 | 默认开启 | 需要在配置文件和Mapper中显式开启 |
共享性 | 不能共享 | 可以被多个SqlSession共享 |
适用场景 | 短时间内相同查询 | 读多写少、数据不经常变化 |
数据一致性 | 较高(Session内) | 较低(需要配置刷新策略) |
5.2 一级缓存示例
一级缓存默认开启,无需特殊配置:
// 测试一级缓存
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,会访问数据库
User user1 = mapper.selectById(1L);
System.out.println(user1);
// 第二次查询相同数据,直接从一级缓存获取
User user2 = mapper.selectById(1L);
System.out.println(user2);
// 修改操作会清空一级缓存
mapper.updateUsername(1L, "newName");
// 第三次查询,因为缓存被清空,会再次访问数据库
User user3 = mapper.selectById(1L);
System.out.println(user3);
sqlSession.close();
5.3 二级缓存配置与使用
步骤1:开启二级缓存
# application.yml
mybatis:
configuration:
cache-enabled: true # 开启二级缓存
步骤2:在Mapper接口上添加缓存注解
// UserMapper.java
@CacheNamespace // 开启二级缓存
public interface UserMapper {
// ...
}
步骤3:配置缓存实现(可选,默认使用PerpetualCache)
// 自定义缓存实现
@CacheNamespace(implementation = MyCustomCache.class, eviction = MyCustomCache.class)
public interface UserMapper {
// ...
}
步骤4:实体类实现Serializable接口
public class User implements Serializable {
// ...
}
二级缓存测试示例:
// 测试二级缓存
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L); // 查询数据库
System.out.println(user1);
sqlSession1.close(); // 关闭session,一级缓存内容写入二级缓存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); // 从二级缓存获取
System.out.println(user2);
sqlSession2.close();
5.4 缓存相关配置与策略
缓存刷新策略:
// 在Mapper方法上使用@Options刷新缓存
@Options(flushCache = Options.FlushCachePolicy.TRUE) // 执行前清空缓存
@Update("UPDATE user SET username=#{name} WHERE id=#{id}")
int updateUsername(@Param("id") Long id, @Param("name") String name);
缓存引用:
多个Mapper共享同一个缓存:
// 共享缓存配置
@CacheNamespaceRef(value = UserMapper.class) // 引用UserMapper的缓存
public interface UserDetailMapper {
// ...
}
自定义缓存:
实现MyBatis的Cache接口:
public class RedisCache implements Cache {
private final String id;
private final RedisTemplate<String, Object> redisTemplate;
public RedisCache(String id) {
this.id = id;
// 初始化RedisTemplate
this.redisTemplate = (RedisTemplate<String, Object>)
SpringContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForHash().put(id, key.toString(), value);
}
@Override
public Object getObject(Object key) {
return redisTemplate.opsForHash().get(id, key.toString());
}
// 实现其他方法...
}
六、插件开发
MyBatis允许开发插件来拦截核心组件的执行,实现自定义功能。
6.1 MyBatis四大组件与拦截点
组件 | 拦截点 | 典型应用场景 |
---|---|---|
Executor | update, query, commit, rollback等 | 分页、缓存、性能监控、读写分离 |
ParameterHandler | getParameterObject, setParameters | SQL参数处理、加解密 |
ResultSetHandler | handleResultSets, handleOutputParameters | 结果集处理、数据脱敏 |
StatementHandler | prepare, parameterize, batch, update, query等 | SQL改写、性能监控、SQL执行时间统计 |
6.2 插件开发步骤
步骤1:实现Interceptor接口
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})
})
public class MybatisQueryInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取拦截方法的参数
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
ResultHandler resultHandler = (ResultHandler) invocation.getArgs()[3];
// 执行前的逻辑处理
long startTime = System.currentTimeMillis();
System.out.println("开始执行查询: " + mappedStatement.getId());
// 执行原方法
Object result = invocation.proceed();
// 执行后的逻辑处理
long endTime = System.currentTimeMillis();
System.out.println("查询执行完成,耗时: " + (endTime - startTime) + "ms");
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以接收配置文件中的参数
}
}
步骤2:注册插件
// 通过配置类注册
@Configuration
public class MyBatisConfig {
@Bean
public MybatisQueryInterceptor mybatisQueryInterceptor() {
return new MybatisQueryInterceptor();
}
}
或者通过XML配置:
<plugins>
<plugin interceptor="com.example.demo.plugin.MybatisQueryInterceptor">
<property name="someProperty" value="someValue"/>
</plugin>
</plugins>
6.3 实用插件示例
示例1:分页插件
// @Intercepts 注解用于指定该拦截器要拦截的方法签名。
// @Signature 注解详细描述了要拦截的方法信息,这里表示拦截 Executor 类的 query 方法,
// 该方法的参数为 MappedStatement、Object、RowBounds 和 ResultHandler 类型。
@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
// 实现 Interceptor 接口,表明这是一个 MyBatis 的拦截器类,用于在 MyBatis 执行查询操作时进行拦截处理。
public class PaginationInterceptor implements Interceptor {
/**
* 拦截方法,当 MyBatis 执行拦截的 query 方法时会调用此方法。
*
* @param invocation 包含被拦截方法的调用信息,如目标对象、方法参数等。
* @return 返回被拦截方法的执行结果。
* @throws Throwable 可能抛出的异常。
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取被拦截方法的参数数组
Object[] args = invocation.getArgs();
// 从参数数组中获取 MappedStatement 对象,它包含了 SQL 语句的映射信息
MappedStatement ms = (MappedStatement) args[0];
// 获取查询的参数对象
Object parameter = args[1];
// 获取分页信息对象
RowBounds rowBounds = (RowBounds) args[2];
// 判断是否需要进行分页操作,如果 rowBounds 不是默认的 RowBounds 对象,则需要分页
if (rowBounds != RowBounds.DEFAULT) {
// 获取执行器对象,用于执行 SQL 语句
Executor executor = (Executor) invocation.getTarget();
// 获取原始 SQL 语句的 BoundSql 对象,它包含了 SQL 语句和参数映射信息
BoundSql boundSql = ms.getBoundSql(parameter);
// 从 BoundSql 对象中获取原始的 SQL 语句,并去除前后的空白字符
String sql = boundSql.getSql().trim();
// 调用 buildPageSql 方法,根据原始 SQL 语句和分页信息构造分页 SQL 语句
String pageSql = buildPageSql(sql, rowBounds);
// 创建一个新的 BoundSql 对象,使用构造好的分页 SQL 语句和原始的参数映射信息
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql,
boundSql.getParameterMappings(), boundSql.getParameterObject());
// 调用 buildMappedStatement 方法,根据原始的 MappedStatement 和新的 BoundSql 创建一个新的 MappedStatement 对象
MappedStatement pageMs = buildMappedStatement(ms, pageBoundSql);
// 修改参数数组,将新的 MappedStatement 对象替换原有的 MappedStatement 对象
args[0] = pageMs;
// 将分页信息重置为默认值,因为已经在 SQL 语句中添加了分页逻辑
args[2] = RowBounds.DEFAULT;
}
// 继续执行被拦截的方法,并返回执行结果
return invocation.proceed();
}
/**
* 根据原始 SQL 语句和分页信息构造分页 SQL 语句。
*
* @param sql 原始的 SQL 语句。
* @param rowBounds 分页信息对象。
* @return 构造好的分页 SQL 语句。
*/
private String buildPageSql(String sql, RowBounds rowBounds) {
// 使用 StringBuilder 来拼接 SQL 语句,提高性能
StringBuilder pageSql = new StringBuilder(sql);
// 在原始 SQL 语句后面添加 LIMIT 子句,实现分页功能
pageSql.append(" LIMIT ").append(rowBounds.getOffset()).append(",").append(rowBounds.getLimit());
// 将 StringBuilder 对象转换为字符串并返回
return pageSql.toString();
}
/**
* 根据原始的 MappedStatement 和新的 BoundSql 创建一个新的 MappedStatement 对象。
*
* @param ms 原始的 MappedStatement 对象。
* @param boundSql 新的 BoundSql 对象。
* @return 新的 MappedStatement 对象。
*/
private MappedStatement buildMappedStatement(MappedStatement ms, BoundSql boundSql) {
// 创建一个新的 MappedStatement.Builder 对象,用于构建新的 MappedStatement 对象
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId() + "_PAGE",
// 定义一个新的 SqlSource 对象,用于返回新的 BoundSql 对象
new SqlSource() {
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
},
// 设置 SQL 命令类型,与原始的 MappedStatement 保持一致
ms.getSqlCommandType());
// 复制原始 MappedStatement 的各种属性到新的 MappedStatement 中
builder.resource(ms.getResource())
.fetchSize(ms.getFetchSize())
.statementType(ms.getStatementType())
.keyGenerator(ms.getKeyGenerator())
.timeout(ms.getTimeout())
.parameterMap(ms.getParameterMap())
.resultMaps(ms.getResultMaps())
.cache(ms.getCache())
.flushCacheRequired(ms.isFlushCacheRequired())
.useCache(ms.isUseCache());
// 构建并返回新的 MappedStatement 对象
return builder.build();
}
// 这里表示可以实现 Interceptor 接口的其他方法,但在当前代码中省略了具体实现
// 实现其他方法...
}
示例2:数据脱敏插件
// @Intercepts 注解用于指定该拦截器要拦截的方法签名。
// @Signature 注解详细描述了要拦截的方法信息,这里表示拦截 ResultSetHandler 类的 handleResultSets 方法,
// 该方法的参数为 Statement 类型。
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
// 实现 Interceptor 接口,表明这是一个 MyBatis 的拦截器类,用于在 MyBatis 处理查询结果集时进行拦截处理。
public class DataMaskingInterceptor implements Interceptor {
/**
* 拦截方法,当 MyBatis 执行拦截的 handleResultSets 方法时会调用此方法。
*
* @param invocation 包含被拦截方法的调用信息,如目标对象、方法参数等。
* @return 返回处理后的结果集。
* @throws Throwable 可能抛出的异常。
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 调用被拦截方法并获取原始查询结果集,将其转换为 List<Object> 类型
List<Object> results = (List<Object>) invocation.proceed();
// 检查结果集是否为空,如果不为空则进行脱敏处理
if (!CollectionUtils.isEmpty(results)) {
// 遍历结果集中的每个对象
for (Object result : results) {
// 检查对象是否为 null,如果不为 null 则调用 maskSensitiveData 方法进行脱敏处理
if (result != null) {
maskSensitiveData(result);
}
}
}
// 返回处理后的结果集
return results;
}
/**
* 对对象中的敏感数据进行脱敏处理。
*
* @param obj 要进行脱敏处理的对象。
*/
private void maskSensitiveData(Object obj) {
// 获取对象的类信息
Class<?> clazz = obj.getClass();
// 遍历对象类的所有声明字段
for (Field field : clazz.getDeclaredFields()) {
// 检查字段是否带有 @Mask 注解
if (field.isAnnotationPresent(Mask.class)) {
// 获取字段上的 @Mask 注解实例
Mask mask = field.getAnnotation(Mask.class);
try {
// 设置字段可访问,以便可以修改其值
field.setAccessible(true);
// 获取字段的值
Object value = field.get(obj);
// 检查字段的值是否为 String 类型
if (value instanceof String) {
String strValue = (String) value;
// 调用 maskValue 方法根据注解配置对字符串值进行脱敏处理,并将处理后的值设置回字段
field.set(obj, maskValue(strValue, mask));
}
} catch (IllegalAccessException e) {
// 处理访问字段值时可能抛出的异常,这里可以根据实际需求添加更详细的异常处理逻辑
}
}
}
}
/**
* 根据 @Mask 注解的配置对字符串值进行脱敏处理。
*
* @param value 要进行脱敏处理的字符串值。
* @param mask @Mask 注解实例,包含脱敏类型信息。
* @return 脱敏后的字符串值。
*/
private String maskValue(String value, Mask mask) {
// 根据 @Mask 注解的 type 属性进行不同类型的脱敏处理
switch (mask.type()) {
case NAME:
// 对姓名进行脱敏处理,如果姓名长度大于 1,则保留首字符和尾字符(如果长度大于 2),中间用 * 替代
return value.length() > 1 ? value.charAt(0) + "*" + (value.length() > 2 ? value.charAt(value.length() - 1) : "") : value;
case PHONE:
// 对手机号码进行脱敏处理,保留前三位和后四位,中间四位用 **** 替代
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
case ID_CARD:
// 对身份证号码进行脱敏处理,保留前四位和后四位,中间十位用 ********** 替代
return value.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
case EMAIL:
// 对邮箱地址进行脱敏处理,保留邮箱名的前三位,中间部分用 **** 替代
return value.replaceAll("(\\w{3})[^@]*(@.*)", "$1****$2");
default:
// 如果没有匹配的脱敏类型,则返回原始值
return value;
}
}
// 这里表示可以实现 Interceptor 接口的其他方法,但在当前代码中省略了具体实现
// 实现其他方法...
}
// 定义一个名为 @Mask 的注解,用于标记需要进行脱敏处理的字段
// @Retention(RetentionPolicy.RUNTIME) 表示该注解在运行时可见,以便在程序运行时可以通过反射获取注解信息
// @Target(ElementType.FIELD) 表示该注解只能应用于字段上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Mask {
// 定义一个属性 type,用于指定脱敏类型,默认值为 MaskType.NAME
MaskType type() default MaskType.NAME;
// 定义一个枚举类型 MaskType,包含了支持的脱敏类型
enum MaskType {
NAME, PHONE, ID_CARD, EMAIL
}
}
七、性能优化
7.1 SQL优化技巧
-
**避免SELECT ***:
- 只查询需要的字段,减少数据传输量
- 示例:
SELECT id, username FROM user
而不是SELECT * FROM user
-
合理使用索引:
- 为WHERE条件、JOIN条件和ORDER BY字段建立索引
- 示例:
CREATE INDEX idx_user_username ON user(username);
-
批量操作:
- 使用批量插入代替单条插入
- 示例:
<insert id="batchInsert"> INSERT INTO user (username, email) VALUES <foreach collection="list" item="item" separator=","> (#{item.username}, #{item.email}) </foreach> </insert>
-
分页优化:
- 避免使用
LIMIT 100000, 10
这样的深分页 - 使用基于索引的分页:
SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10
- 避免使用
-
避免N+1查询问题:
- 使用嵌套结果代替嵌套查询
- 或者使用
@FetchType.SUBSELECT
7.2 MyBatis配置优化
-
配置本地缓存:
mybatis: configuration: local-cache-scope: statement # 默认为session,可设置为statement减少内存占用
-
合理设置JDBC参数:
spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000
-
使用延迟加载:
mybatis: configuration: lazy-loading-enabled: true aggressive-lazy-loading: false
7.3 监控与诊断
-
SQL执行监控:
- 使用前面提到的插件记录SQL执行时间
- 集成P6Spy等工具监控真实SQL
-
慢SQL日志:
@Intercepts(@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})) public class SlowSqlInterceptor implements Interceptor { private static final long SLOW_SQL_THRESHOLD = 1000; // 1秒 @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long duration = System.currentTimeMillis() - start; if (duration > SLOW_SQL_THRESHOLD) { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); System.err.println("慢SQL警告: " + boundSql.getSql() + " 执行时间: " + duration + "ms"); } } } // 其他方法... }
-
Explain分析:
public interface UserMapper { @Select("EXPLAIN ${sql}") List<Map<String, Object>> explain(@Param("sql") String sql); }
八、最佳实践
8.1 项目结构规范
推荐的项目结构:
src/main/java
└── com
└── example
└── demo
├── config # 配置类
├── controller # 控制器
├── service # 服务层
│ ├── impl # 服务实现
├── mapper # Mapper接口
├── entity # 实体类
├── dto # 数据传输对象
├── vo # 视图对象
├── util # 工具类
└── exception # 异常处理
src/main/resources
├── mapper # XML映射文件
├── application.yml # 应用配置
8.2 代码规范
-
命名规范:
- Mapper接口:
XxxMapper
- XML文件:与Mapper接口同名
- 方法名:
- 查询:
selectXxx
,getXxx
,queryXxx
- 插入:
insertXxx
,saveXxx
- 更新:
updateXxx
- 删除:
deleteXxx
,removeXxx
- 查询:
- Mapper接口:
-
事务管理:
@Service public class UserService { @Transactional(rollbackFor = Exception.class) public void createUser(User user) { // 业务操作 userMapper.insert(user); // 其他操作... } }
-
异常处理:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResult> handleBusinessException(BusinessException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ErrorResult(e.getCode(), e.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResult> handleException(Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResult(500, "系统繁忙,请稍后再试")); } }
8.3 安全考虑
-
SQL注入防护:
- 永远使用
#{}
而不是${}
,除非必要 - 如果必须使用
${}
,确保参数已经过验证和转义
- 永远使用
-
敏感数据保护:
- 使用前面提到的数据脱敏插件
- 密码等敏感信息加密存储
-
日志脱敏:
public class SensitiveDataAspect { @Around("execution(* com.example.demo.mapper.*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); // 对参数进行脱敏处理 desensitizeArgs(args); Object result = joinPoint.proceed(args); // 对结果进行脱敏处理 return desensitizeResult(result); } // 脱敏方法实现... }
九、总结
本文全面介绍了Spring Boot整合MyBatis的各个方面,从基础配置到高级特性,包括:
- 基础配置与CRUD操作
- 动态SQL的灵活运用
- 关联关系的多种处理方式
- 缓存机制的原理与配置
- 插件开发的实战示例
- 性能优化的多种技巧
- 项目最佳实践
通过合理的配置和使用MyBatis的高级特性,可以构建出高效、灵活的数据访问层。希望这篇指南能帮助你在实际项目中更好地使用Spring Boot和MyBatis。
十、常见问题解答
Q1:MyBatis和JPA/Hibernate有什么区别?
特性 | MyBatis | JPA/Hibernate |
---|---|---|
SQL控制 | 完全控制SQL | 自动生成SQL |
学习曲线 | 较平缓 | 较陡峭 |
灵活性 | 高 | 中等 |
性能 | 接近JDBC | 可能有额外开销 |
适用场景 | 复杂SQL、需要精细控制 | 快速开发、标准CRUD |
Q2:如何选择XML配置还是注解配置?
-
XML配置适合:
- 复杂SQL
- 动态SQL
- 需要重用SQL片段
- 团队统一管理SQL
-
注解配置适合:
- 简单CRUD
- 快速原型开发
- 小型项目
Q3:如何解决N+1查询问题?
- 使用嵌套结果(单SQL连接查询)
- 开启批量加载
- 使用
@Fetch(FetchMode.SUBSELECT)
- 手动编写优化SQL
好了,就这样吧,点赞随缘(但你不点我会哭)。
喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!