Spring Boot整合MyBatis全面指南:从基础到高级应用

一、基础概念与配置

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四大组件与拦截点

组件拦截点典型应用场景
Executorupdate, query, commit, rollback等分页、缓存、性能监控、读写分离
ParameterHandlergetParameterObject, setParametersSQL参数处理、加解密
ResultSetHandlerhandleResultSets, handleOutputParameters结果集处理、数据脱敏
StatementHandlerprepare, 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优化技巧

  1. **避免SELECT ***:

    • 只查询需要的字段,减少数据传输量
    • 示例:SELECT id, username FROM user 而不是 SELECT * FROM user
  2. 合理使用索引

    • 为WHERE条件、JOIN条件和ORDER BY字段建立索引
    • 示例:CREATE INDEX idx_user_username ON user(username);
  3. 批量操作

    • 使用批量插入代替单条插入
    • 示例:
      <insert id="batchInsert">
          INSERT INTO user (username, email) VALUES
          <foreach collection="list" item="item" separator=",">
              (#{item.username}, #{item.email})
          </foreach>
      </insert>
      
  4. 分页优化

    • 避免使用LIMIT 100000, 10这样的深分页
    • 使用基于索引的分页:
      SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10
      
  5. 避免N+1查询问题

    • 使用嵌套结果代替嵌套查询
    • 或者使用@FetchType.SUBSELECT

7.2 MyBatis配置优化

  1. 配置本地缓存

    mybatis:
      configuration:
        local-cache-scope: statement  # 默认为session,可设置为statement减少内存占用
    
  2. 合理设置JDBC参数

    spring:
      datasource:
        hikari:
          maximum-pool-size: 20
          connection-timeout: 30000
          idle-timeout: 600000
          max-lifetime: 1800000
    
  3. 使用延迟加载

    mybatis:
      configuration:
        lazy-loading-enabled: true
        aggressive-lazy-loading: false
    

7.3 监控与诊断

  1. SQL执行监控

    • 使用前面提到的插件记录SQL执行时间
    • 集成P6Spy等工具监控真实SQL
  2. 慢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");
                }
            }
        }
        // 其他方法...
    }
    
  3. 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 代码规范

  1. 命名规范

    • Mapper接口:XxxMapper
    • XML文件:与Mapper接口同名
    • 方法名:
      • 查询:selectXxx, getXxx, queryXxx
      • 插入:insertXxx, saveXxx
      • 更新:updateXxx
      • 删除:deleteXxx, removeXxx
  2. 事务管理

    @Service
    public class UserService {
        @Transactional(rollbackFor = Exception.class)
        public void createUser(User user) {
            // 业务操作
            userMapper.insert(user);
            // 其他操作...
        }
    }
    
  3. 异常处理

    @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 安全考虑

  1. SQL注入防护

    • 永远使用#{}而不是${},除非必要
    • 如果必须使用${},确保参数已经过验证和转义
  2. 敏感数据保护

    • 使用前面提到的数据脱敏插件
    • 密码等敏感信息加密存储
  3. 日志脱敏

    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的各个方面,从基础配置到高级特性,包括:

  1. 基础配置与CRUD操作
  2. 动态SQL的灵活运用
  3. 关联关系的多种处理方式
  4. 缓存机制的原理与配置
  5. 插件开发的实战示例
  6. 性能优化的多种技巧
  7. 项目最佳实践

通过合理的配置和使用MyBatis的高级特性,可以构建出高效、灵活的数据访问层。希望这篇指南能帮助你在实际项目中更好地使用Spring Boot和MyBatis。

十、常见问题解答

Q1:MyBatis和JPA/Hibernate有什么区别?

特性MyBatisJPA/Hibernate
SQL控制完全控制SQL自动生成SQL
学习曲线较平缓较陡峭
灵活性中等
性能接近JDBC可能有额外开销
适用场景复杂SQL、需要精细控制快速开发、标准CRUD

Q2:如何选择XML配置还是注解配置?

  • XML配置适合:

    • 复杂SQL
    • 动态SQL
    • 需要重用SQL片段
    • 团队统一管理SQL
  • 注解配置适合:

    • 简单CRUD
    • 快速原型开发
    • 小型项目

Q3:如何解决N+1查询问题?

  1. 使用嵌套结果(单SQL连接查询)
  2. 开启批量加载
  3. 使用@Fetch(FetchMode.SUBSELECT)
  4. 手动编写优化SQL

好了,就这样吧,点赞随缘(但你不点我会哭)。

喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clf丶忆笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值