专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Spring Boot整合MyBatis教程:全栈配置、动态SQL与缓存实战

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 selectByCondition(User user)  灵活  需要处理多个参数 
分页查询  大数据量分页显示  List selectByPage(RowBounds rb)  内存友好  需要额外处理分页逻辑 
注解方式查询  简单SQL直接写在接口上  @Select("SELECT * FROM user")  无需XML文件  复杂SQL可读性差 
结果集映射  处理复杂结果集  使用 @ResultMap 或XML中的   处理复杂关系  配置稍复杂 

示例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  条件判断  AND name = #{name} 
choose/when  多条件选择(类似Java的switch)  AND id = #{id}... 
where  智能处理WHERE关键字和AND/OR前缀  AND name = #{name} 
set  智能处理UPDATE语句中的SET部分  name = #{name}, 
foreach  循环集合,常用于IN条件或批量操作  #{item} 
trim  自定义字符串截取,可以替代where或set  ... 
bind  创建变量并绑定到上下文   

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处理方式
一对一  如用户和身份证  标签或注解 @One 
一对多  如部门和员工  标签或注解 @Many 
多对多  如学生和课程  通过中间表实现,结合一对多和多对一 
嵌套结果  单SQL查询获取所有关联数据  使用嵌套的  
嵌套查询  通过额外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优化技巧

1、 **避免SELECT ***:
2、 只查询需要的字段,减少数据传输量

示例: `SELECT id, username FROM user`  而不是  `SELECT * FROM user`

3、 合理使用索引
4、 为WHERE条件、JOIN条件和ORDER BY字段建立索引

示例: `CREATE INDEX idx_user_username ON user(username);`

5、 批量操作
6、 使用批量插入代替单条插入

示例:
<insert id="batchInsert">
        INSERT INTO user (username, email) VALUES
        <foreach collection="list" item="item" separator=",">
            (#{item.username}, #{item.email})
        </foreach>
    </insert>

7、 分页优化
8、 避免使用LIMIT 100000, 10这样的深分页

使用基于索引的分页:
SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10

9、 避免N+1查询问题
10、 使用嵌套结果代替嵌套查询

或者使用 `@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执行监控
2、 使用前面提到的插件记录SQL执行时间

集成P6Spy等工具监控真实SQL

3、 慢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");
                }
            }
        }
        // 其他方法...
    }

4、 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、 命名规范
2、 查询: selectXxx , getXxx , queryXxx

插入: `insertXxx` ,  `saveXxx`

更新: `updateXxx`

删除: `deleteXxx` ,  `removeXxx`

3、 Mapper接口: XxxMapper

XML文件:与Mapper接口同名

方法名:

4、 事务管理

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

5、 异常处理

@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、 敏感数据保护
4、 使用前面提到的数据脱敏插件

密码等敏感信息加密存储

5、 日志脱敏

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有什么区别?

特性 MyBatis JPA/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

未经允许不得转载:搜云库 » Spring Boot整合MyBatis教程:全栈配置、动态SQL与缓存实战

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们