{T}

缓存与注解开发

MyBatis 缓存

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存中。当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。减少网络连接和数据库查询带来的损耗,从而提高我们的查询效率,减少高并发访问带来的系统性能问题

一句话概括:经常查询一些不经常发生变化的数据,使用缓存来提高查询效率

Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。 Mybatis 中缓存分为一级缓存,二级缓存

一级缓存

一级缓存是 SqlSession 级别的缓存,是默认开启的

所以在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用一个 Mapper 方法,往往只执行一次 SQL,因为使用 SelSession 第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库

验证

java
// 验证mybatis中的一级缓存
@Test
public void testOneCache() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 根据id查询用户信息
    // 第一次查询,查询的数据库
    User user1 = userMapper.findById(1);
    System.out.println(user1);

    // clearCache: 手动清空缓存
    //sqlSession.clearCache();

    // 第二次查询,查询的是一级缓存
    User user2 = userMapper.findById(1);
    System.out.println(user2);

    sqlSession.close();

}

我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是 Mybatis 提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 1 的记录时,并没有发出 sql 语句从数据库中查询数据,而是从一级缓存中查询

分析

一级缓存是 SqlSession 范围的缓存,执行 SqlSession 的 C(增加)U(更新)D(删除)操作,或者调用 clearCache()、commit()、close() 方法,都会清空缓存

  1. 第一次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,如果没有,从数据库查询用户信息
  2. 得到用户信息,将用户信息存储到一级缓存中
  3. 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读
  4. 第二次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,缓存中有,直接从缓 存中获取用户信息

清除

java
@Test 
public void testClearOneCache() throws Exception { 
  SqlSession sqlSession = MybatisUtils.openSession(); 
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  User user1 = userMapper.findById(41); 
  System.out.println("第一次查询的用户:" + user1); 
  
  //调用sqlSession清除缓存的方法 
  sqlSession.clearCache(); 
  
  User user2 = userMapper.findById(41); 
  System.out.println("第二次查询的用户:" + user2); 
}
xml
<!-- 每次查询时,都会清除缓存 --> 
<select id="findById" resultType="com.xiaoye.domain.User" parameterType="int"  flushCache="true">
  SELECT * FROM user WHERE id = #{id}
</select>

二级缓存

  • 二级缓存是 namspace 级别(跨 sqlSession )的缓存,是默认不开启的
  • 二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis 要求返回的 POJO 必须是可序列化的。也就是要求实现 Serializable 接口,配置方法很简单,只需要在映射 XML 文件配置 就可以开启二级缓存了

验证

a)配置核心配置文件

xml
<settings> 
  <!-- 因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置。 
			value 为true代表开启二级缓存;为false代表不开启二级缓存。
  --> 
  <setting name="cacheEnabled" value="true"/> 
</settings>

b)配置 UserMapper.xml 映射

xml
<mapper namespace="com.lagou.dao.UserMapper"> 
  <!--当前映射文件开启二级缓存--> 
  <cache></cache> 

  <!-- 
  	<select>标签中设置useCache=”true”代表当前这个statement要使用二级缓存。 
  	如果不使用二级缓存可以设置为false 注意:针对每次查询都需要最新的数据sql,
  	要设置成useCache="false",禁用二级缓存。
  --> 
  <select id="findById" parameterType="int" resultType="user" useCache="true" > 
    SELECT * FROM user where id = #{id} 
  </select> 
</mapper>

修改user实体实现Serializable

java
public class User implements Serializable { 
  private Integer id; private String username; 
  private Date birthday; 
  private String sex; 
  private String address; 
  private List<Role> roleList; 
  private List<Order> orderList; 
}

c)测试结果

java
/*
   验证mybatis中的二级缓存
*/
@Test
public void testTwoCache() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    SqlSession sqlSession1 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);

    // 第一次查询
    User user = userMapper1.findById(1);
    System.out.println(user);

    // 只有执行sqlSession.commit或者sqlSession.close,那么一级缓存中内容才会刷新到二级缓存
    sqlSession1.close();

    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

	// 从缓存获取的,没有执行sql查询
    User user2 = userMapper2.findById(1);

    System.out.println(user2);

    sqlSession2.close();
}

分析

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的

注意问题(脏读)

mybatis 的二级缓存因为是 namespace 级别,所以在进行多表查询时会产生脏读问题

小结

  1. mybatis 的缓存,都不需要我们手动存储和获取数据。mybatis 自动维护的
  2. mybatis 开启了二级缓存后,那么查询顺序:二级缓存--》一级缓存--》数据库
  3. 注意:mybatis 的二级缓存会存在脏读问题,需要使用第三方的缓存技术解决问题

MyBatis 注解

这几年来注解开发越来越流行,Mybatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。我们先围绕一些基本的 CRUD 来学习,再学习复杂映射多表操作

sql
@Insert:实现新增,代替了<insert></insert>

@Delete:实现删除,代替了<delete></delete> 

@Update:实现更新,代替了<update></update> 

@Select:实现查询,代替了<select></select> 

@Result:实现结果集封装,代替了<result></result> 

@Results:可以与@Result 一起使用,封装多个结果集,代替了<resultMap></resultMap> 

@One:实现一对一结果集封装,代替了<association></association> 

@Many:实现一对多结果集封装,代替了<collection></collection>

MyBatis 注解的增删改查【重点】

创建 UserMapper 接口

java
public interface UserMapper { 
  
  @Select("SELECT * FROM user") 
  public List<User> findAll(); 
  
  @Insert("INSERT INTO user(username,birthday,sex,address) VALUES(# {username},#{birthday},#{sex},#{address})") 
  public void save(User user); 
  
  @Update("UPDATE user SET username = #{username},birthday = #{birthday},sex = #{sex},address = #{address} 
          WHERE id = #{id}") 
  public void update(User user); 
  
  @Delete("DELETE FROM user where id = #{id}") 
  public void delete(Integer id);

}

编写核心配置文件

xml
<!--我们使用了注解替代的映射文件,所以我们只需要加载使用了注解的Mapper接口即可--> 
<mappers> 
  <!--扫描使用注解的Mapper类--> 
	<!--   <mapper class="com.lagou.mapper.UserMapper"></mapper>  -->

  <!--扫描使用注解的 Mapper 类所在的包--> 
  <package name="com.lagou.mapper"></package> 
</mappers>

测试代码

java
import com.lagou.domain.Orders;
import com.lagou.domain.User;
import com.lagou.mapper.OrderMapper;
import com.lagou.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class MybatisTest {

  private SqlSessionFactory sqlSessionFactory;
  private SqlSession sqlSession;

  // 在 @Test方法标注的方法执行之前来执行
  @Before
  public void before() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    sqlSession = sqlSessionFactory.openSession();
  }

  @After
  public void after(){

    sqlSession.commit();
    sqlSession.close();

  }

  // 测试查询方法
  @Test
  public void testSelect() throws IOException {

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<User> all = mapper.findAll();

    for (User user : all) {
      System.out.println(user);
    }

  }

  // 测试添加方法
  @Test
  public void testInsert(){

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = new User();
    user.setUsername("汤唯");
    user.setSex("女");
    user.setBirthday(new Date());
    user.setAddress("北京");

    mapper.save(user);

  }

  // 测试更新方法
  @Test
  public void testUpdate(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = new User();
    user.setUsername("柳岩真美");
    user.setBirthday(new Date());
    user.setId(9);

    mapper.update(user);

  }

  // 测试删除方法
  @Test
  public void testDelete(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    mapper.delete(9);
  }
}

使用注解实现复杂映射开发

之前我们在映射文件中通过配置来实现复杂关系映射

使用注解开发后,我们可以使用 @Results、@Result、@One、@Many 注解组合完成复杂关系的配置

一对一查询

需求:查询所有订单,与此同时查询出该订单所属的用户

一对一查询语句

java
SELECT * FROM orders; 

SELECT * FROM `user` WHERE id = #{订单的uid};

a)OrderMapper接口

java
public interface OrderMapper {

    // 查询所有订单,同时查询订单所属的用户信息
    @Select("select * from orders")
    @Results({
        @Result(property = "id", column = "id", id = true),
        @Result(property = "ordertime", column = "ordertime"),
        @Result(property = "total", column = "total"),
        @Result(property = "uid", column = "uid"),
        @Result(property = "user", javaType = User.class, column = "uid", fetchType = FetchType.EAGER,
                one = @One(select = "com.lagou.mapper.UserMapper.findById", ))
    })
    public List<Orders> findAllWithUser();

}

b)UserMapper接口

java
public interface UserMapper { 
  @Select("SELECT * FROM `user` WHERE id = #{id}") 
  public User findById(Integer id);
}

c)测试代码

java
@Test 
public void testOrderWithUser() throws Exception { 
  OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class); 
  List<Order> list = orderMapper.findAllWithUser(); 
  for (Order order : list) { 
    System.out.println(order); 
  } 
}

一对多查询

需求:查询一个用户,与此同时查询出该用户具有的订单

一对多查询语句

sql
SELECT * FROM `user`; 

SELECT * FROM orders where uid = #{用户id};

a)UserMapper 接口

java
// 查询所有用户,及关联的订单信息
@Select("select * from user")
@Results({
      @Result(property = "id", column = "id", id = true),
      @Result(property = "username", column = "username"),
      @Result(property = "birthday", column = "birthday"),
      @Result(property = "sex", column = "sex"),
      @Result(property = "address", column = "address"),
      @Result(property = "ordersList", javaType = List.class, column = "id", fetchType = FetchType.LAZY,
              many = @Many(select = "com.lagou.mapper.OrderMapper.findOrderByUid" ))
    })
public List<User> findAllWithOrder();

b)OrderMapper 接口

java
public interface OrderMapper { 
  @Select("SELECT * FROM orders WHERE uid = #{uid}") 
  public List<Order> findByUid(Integer uid); 
}

c)测试代码

java
@Test 
public void testUserWithOrder() throws Exception { 
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  List<User> list = userMapper.findAllWithOrder(); 
  for (User user : list) { 
    System.out.println(user); 
  } 
}

多对多查询

需求:查询所有用户,同时查询出该用户的所有角色

多对多查询语句

sql
SELECT * FROM `user`; 

SELECT * FROM role r INNER JOIN user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{用户id};

a)UserMapper接口

java
public interface UserMapper { 
    @Select("SELECT * FROM `user`") 
    @Results({ 
        @Result(id = true, column = "id", property = "id"), 
        @Result(column = "brithday", property = "brithday"), 
        @Result(column = "sex", property = "sex"), 
        @Result(column = "address", property = "address"), 
        @Result(property = "roleList", javaType = List.class, column = "id" , 
                many = @Many(select = "com.lagou.mapper.RoleMapper.findByUid")) })
    public List<User> findAllWithRole(); 
}

b)RoleMapper接口

java
public interface RoleMapper { 
  @Select("SELECT * FROM role r INNER JOIN user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{uid}") 
  public List<Role> findByUid(Integer uid); 
}

c)测试代码

java
@Test 
public void testUserWithRole() throws Exception { 
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  List<User> list = userMapper.findAllWithRole(); 
  for (User user : list) { 
    System.out.println(user); 
  } 
}

基于注解的二级缓存

配置 SqlMapConfig.xml 文件开启二级缓存的支持

xml
<settings> 
  <!--因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置。 
	为true代表开启二级缓存;为false代表不开启二级缓存。 --> 	 
  <setting name="cacheEnabled" value="true"/> 
</settings>

在Mapper接口中使用注解配置二级缓存

java
@CacheNamespace 
public interface UserMapper {...}

注解延迟加载

不管是一对一还是一对多 ,在注解配置中都有 fetchType 的属性

  • fetchType = FetchType.LAZY 表示懒加载
  • fetchType = FetchType.EAGER 表示立即加载
  • fetchType = FetchType.DEFAULT 表示使用全局配置

小结

注解开发和 xml 配置优劣分析

  • 注解开发和 xml 配置相比,从开发效率来说,注解编写更简单,效率更高。
  • 从可维护性来说,注解如果要修改,必须修改源码,会导致维护成本增加。xml 维护性更强

版本差异(旧版 → MyBatis 3.5.x)

特性旧版(MyBatis 3.x 早期)MyBatis 3.5.x
一级/二级缓存PerpetualCache + 装饰器链机制不变,完全兼容
注解开发@Select/@Insert/@Update/@Delete、@Results用法不变;3.5.x 持续增强注解场景
@Mapper 扫描需手动配置 mapper 扫描不变;Boot 3 推荐 @MapperScan 或 @Mapper
缓存刷新flushCache / useCache用法不变
record 支持3.5.13+ 支持 record 构造器自动映射

缓存与注解开发机制在 3.5.x 中保持稳定,本文示例无需修改即可运行;升级到 Spring Boot 3.5.x 时注意依赖坐标与 jakarta 包名迁移。

源码深挖:本文讲解 MyBatis 缓存与注解开发的使用层面。若要深入理解一级/二级缓存的底层实现(装饰器模式、CacheKey、缓存淘汰)、Mapper 接口代理机制与注解映射的解析原理,见「MyBatis 核心原理」专栏的《基于 MyBatis 缓存分析装饰器模式》《Mapper 接口映射》《初始化流程》等篇。