Mybatis学习第六弹。

Mybatis-6-延迟加载与缓存

1. 延迟加载

1. 什么是延迟加载

  1. 延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。
  2. 延迟加载(lazy load)是(也称为懒加载)。

2. 为什么要使用延迟加载

  1. 实际开发中一张表往往关联着其他表,但每次查询不一定要用到其他表。
  2. 查询一张表比查询多张表速度要快,可以大大提高查询速度。

3. 延迟加载存在的问题

由于是用到了数据才去加载,所以在用户大量使用查询的时候,会导致查询结果较慢。

4. 延迟加载应用示例

  1. 场景

    1. Account和User表

      表名 列名1 列名2 列名3 列名4 列名5
      Account id uid money
      User id username birthday sex address
      1. User可以有多个Account(即是一对多)。

      2. Account通过uid与唯一的User关联。

    2. 需求

      1. 在通过查询Account时,不直接查询出其中的User。
      2. 在需要使用User信息部分(例如:调用getUser模拟使用User信息)时,才实行加载。
  2. 分析

    1. Account中有User的属性用于关联User表查询的信息。
    2. 不调用其中的getUser方法的时候不查询User的数据(延迟加载)。
    3. 在Account的dao.xml配置返回值的时候将resultMap中的association添加select明确是使用延迟加载来查询User属性。

    延迟加载

  3. 编码

    1. 给Account实体类中添加唯一User属性

      1
      2
      3
      4
      5
      6
      public class Account implements Serializable {
      private int id;
      private int uid;
      private double money;
      private User user; // 唯一user
      }
  1. 在UserDao中添加根据id查询唯一User的抽象方法findById。

    1
    2
    3
    public interface UserDao {
    public User findById(Integer uid);
    }
  1. 在AccountDao中添加findAll查询所有的方法。

    1
    2
    3
    public interface AccountDao {
    public List<Account> findAll();
    }
  1. 在AccountDao.xml中Mapper下添加关联findAll查询方法的配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <mapper namespace="top.tobing.dao.AccountDao">
    <resultMap id="accountMap" type="top.tobing.domain.Account">
    <id column="id" property="id"/>
    <result column="aid" property="id"/>
    <result column="uid" property="uid"/>
    <result column="money" property="money"/>
    <association property="user" javaType="top.tobing.domain.User"
    select="top.tobing.dao.UserDao.findById" column="uid">
    </association>
    </resultMap>

    <select id="findAll" resultMap="accountMap">
    select * from account;
    </select>
    </mapper>

    此步骤在association中包含了select,可以实现按需加载User。

  2. 在UserDao.xml中添加关联findById的方法。

    1
    2
    3
    4
    5
    <mapper namespace="top.tobing.dao.UserDao">
    <select id="findById" resultType="top.tobing.domain.User">
    select * from user where id = #{uid};
    </select>
    </mapper>
  3. 在测试方法中测试是否实现延迟加载

    不调用getUser等,与User信息相关的方法。

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void findAllTest(){
    List<Account> accounts = accountDao.findAll();
    for (Account account : accounts) {
    System.out.println(account.getMoney());
    }
    }

    查询结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    2019-11-28 12:18:36,346 224    [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
    2019-11-28 12:18:36,578 456 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1038677529.
    2019-11-28 12:18:36,578 456 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
    2019-11-28 12:18:36,582 460 [ main] DEBUG .tobing.dao.AccountDao.findAll - ==> Preparing: select * from account;
    2019-11-28 12:18:36,617 495 [ main] DEBUG .tobing.dao.AccountDao.findAll - ==> Parameters:
    2019-11-28 12:18:36,673 551 [ main] DEBUG .tobing.dao.AccountDao.findAll - <== Total: 6
    20000.0
    40000.0
    30000.0
    21000.0
    40000.0
    50000.0
    2019-11-28 12:18:36,675 553 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
    2019-11-28 12:18:36,675 553 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
    2019-11-28 12:18:36,676 554 [ main] DEBUG source.pooled.PooledDataSource - Returned connection 1038677529 to pool.

    只查询了一次。

  > id为42时,调用getUser方法

  
1
2
3
4
5
6
7
8
9
10
@Test
public void findAllTest(){
List<Account> accounts = accountDao.findAll();
for (Account account : accounts) {
System.out.println(account.getMoney());
if(account.getUid()==42){ // 只有id为42才查询。
System.out.println(account.getUser());
}
}
}
> 只有id为42,才去执行查询User表(调用UserDao.xml中的findById)
1
2
3
4
5
6
User{id=42, username='Rongon', birthday=null, sex='男', address='江苏'}
30000.0
21000.0
40000.0
50000.0
User{id=42, username='Rongon', birthday=null, sex='男', address='江苏'}

5. 练习:以第4为例,实现查询用户User表,延迟加载用户的Account信息。

  1. User表中添加关联的Account的字段。

    1
    2
    3
    4
    5
    6
    7
    8
    public class User {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    private List<Account> accounts; // 用户对应的多个账号
    }
  1. AccountDao接口中添加按照uid查询多个Account的方法

    1
    2
    3
    public interface AccountDao {
    List<Account> findAllByUid(Integer uid);
    }
  1. UserDao接口中添加按照查询所有的方法\

    1
    2
    3
    public interface UserDao {
    public List<User> findAllAccount();
    }
  1. AccountDao.xml中添加AccountDao接口对应的方法的配置

    1
    2
    3
    <select id="findAllByUid" resultMap="top.tobing.domain.Account">
    select * from account where uid = #{id};
    </select>
  1. UserDao.xml中添加UserDao接口对应的方法配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <mapper namespace="top.tobing.dao.UserDao">
    <resultMap id="userMap" type="top.tobing.domain.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="birthday" property="birthday"/>
    <result column="address" property="address"/>
    <result column="sex" property="sex"/>
    <association property="accounts" javaType="List"
    select="top.tobing.dao.AccountDao.findAllByUid" column="id">
    </association>
    </resultMap>

    <select id="findAllAccount" resultMap="userMap">
    select * from user;
    </select>

    </mapper>

    注意:

    1. association中的javaType是account对应的数据类型(List
    2. association中的column参数是传给top/tobing/AccountDao.xml中的findAllByUid对应的mapper.
  2. 检验是否实现延迟查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    2019-11-28 16:21:04,172 501    [           main] DEBUG ing.dao.UserDao.findAllAccount  - ==>  Preparing: select * from user; 
    2019-11-28 16:21:04,207 536 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Parameters:
    2019-11-28 16:21:04,261 590 [ main] DEBUG ing.dao.UserDao.findAllAccount - <== Total: 6
    Tobing

    ==============================
    Rongon

    ==============================
    Zenyet

    ==============================
    尼古拉斯

    ==============================
    张无忌

    ==============================
    张三丰

    ==============================
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    2019-11-28 16:21:44,570 217    [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
    2019-11-28 16:21:44,776 423 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1314838582.
    2019-11-28 16:21:44,776 423 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4e5ed836]
    2019-11-28 16:21:44,779 426 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Preparing: select * from user;
    2019-11-28 16:21:44,816 463 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Parameters:
    2019-11-28 16:21:44,868 515 [ main] DEBUG ing.dao.UserDao.findAllAccount - <== Total: 6
    Tobing

    2019-11-28 16:21:44,869 516 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
    2019-11-28 16:21:44,870 517 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 41(Integer)
    2019-11-28 16:21:44,871 518 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 1
    [Account{id=1, uid=41, money=20000.0}]
    ==============================
    Rongon

    2019-11-28 16:21:44,880 527 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
    2019-11-28 16:21:44,881 528 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 42(Integer)
    2019-11-28 16:21:44,882 529 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 2
    [Account{id=2, uid=42, money=40000.0}, Account{id=6, uid=42, money=50000.0}]
    ==============================
    Zenyet

    2019-11-28 16:21:44,882 529 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
    2019-11-28 16:21:44,883 530 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 43(Integer)
    2019-11-28 16:21:44,883 530 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 1
    [Account{id=3, uid=43, money=30000.0}]
    ==============================

6. 总结

  1. 与之前的立即相比。延迟加载在要延迟加载的association中添加了select、column等属性。

    select:用于指定调用那个方法执行查询延迟加载部分内容。

    column:用于指定查询延迟加载部分的参数。

    javaType:要合实体类一一对应。

  2. 所谓延迟加载,在此处的体现是,根据需求,决定是否调用其他的方法(该方法可以实现查询延迟加载的部分数据)。

2.Mybatis中的缓存

1. 缓存

  1. What:存储在内存中临时数据。
  2. Why:减少和数据库交互次数,提高执行效率。
  3. When:
    1. 使用缓存:数据经常查询、数据不经常改变、数据正确性对最终结果影响不大。
    2. 不使用缓存:数据经常改变、数据正确性对最终结果影响很大(例如:商品库存、银行汇率、股市牌价等)

2. Mybatis中缓存机制

  1. 一级缓存:
    • 是指Mybatis中SqlSession对象的缓存
    • 当我们执行查询后,查询的结果会存入到SqlSession对象提供的一块区域中(一个Map)。
    • 当我们查询同样的数据时,Mybatis会先去SqlSession中的区域找,如果存在直接拿来用。
    • 因为一级缓存保存在SqlSession,所以当SqlSession对象消失,一级缓存自动消失。
  2. 二级缓存:
    • 是指存储在SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。
    • Mybatis默认不开启二级缓存
    • SqlSession执行相同Mapper映射下sql,执行commit提交,将会清空Mapper映射下的二级缓存

缓存机制

3. 一级缓存

  • Mybatis中默认开启一级缓存

  • 验证步骤

    1. 创建User、UserDao、UserDao.xml
    1
    2
    3
    4
    5
    6
    7
    public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    }
  1. UserDao中添加一个用于查询的查询方法

    1
    2
    3
    public interface UserDao {
    public User findById(int id);
    }
  1. UserDao.xml配置查询的SQL语句

    1
    2
    3
    4
    5
    <mapper namespace="top.tobing.dao.UserDao">
    <select id="findById" resultType="top.tobing.domain.User">
    select * from user where id = #{id} ;
    </select>
    </mapper>
  2. 验证缓存查询结果

    1
    2
    3
    4
    5
    public void findByIdTest(){
    User user = userDao.findById(44); // 第一次查询
    System.out.println(user);
    User user1 = userDao.findById(44); // 第二次查询
    System.out.println(user1);
    1
    2
    3
    4
    5
    DEBUG op.tobing.dao.UserDao.findById  - ==>  Preparing: select * from user where id = ? ; 
    DEBUG op.tobing.dao.UserDao.findById - ==> Parameters: 44(Integer)
    DEBUG op.tobing.dao.UserDao.findById - <== Total: 1
    top.tobing.domain.User@7fd7a283
    top.tobing.domain.User@7fd7a283

    两次查询出来的对象地址相同

  3. 验证清楚缓存

    调用clearCache、close等方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void findByIdTest(){
    User user = userDao.findById(44); // 第一次查询
    System.out.println(user);
    // 再次获取session
    // session.close();
    // session = factory.openSession();
    // userDao = session.getMapper(UserDao.class);
    // 另外一种方式清空数据
    session.clearCache();

    User user1 = userDao.findById(44); // 第二次查询
    System.out.println(user1);

    结果查询了两次

    调用了commit更新等操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void firstCacheTest(){
    User user = userDao.findById(44); // 第一次查询
    System.out.println(user);
    user.setUsername("CacheTest");
    userDao.updateUser(user);
    user = userDao.findById(44);
    System.out.println(user);
    System.out.println("--------------");
    }

    此处也会跳过缓存,直接查询。

4. 二级缓存

同一个SqlSessionFactory得到的SqlSession可以共享二级缓存数据。

使用步骤:

  1. SqlMapConfig中开启二级缓存
  2. Mapper中开启二级缓存
  3. statement中使用二级缓存
  1. SQLMapConfig开启二级缓存

    1
    2
    3
    4
    <!--开启二级缓存-->
    <settings>
    <setting name="cacheEnable" value="true"/>
    </settings>
  1. Mapper中开启二级缓存、statement使用二级缓存

    1
    2
    3
    4
    5
    6
    7
    8
    <mapper namespace="top.tobing.dao.UserDao">
    <!--Mapper开启缓存-->
    <cache></cache>
    <!--查询时使用缓存-->
    <select id="findById" resultType="top.tobing.domain.User" useCache="true">
    select * from user where id = #{id} ;
    </select>
    </mapper>
  2. 检验结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void secondCacheTest(){
    SqlSession sqlSession1 = factory.openSession();
    UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
    User user1 = userDao1.findById(44);
    System.out.println(user1);
    sqlSession1.close();

    SqlSession sqlSession2 = factory.openSession();
    UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
    User user2 = userDao2.findById(44);
    System.out.println(user2);
    sqlSession2.close();
    }

    结果显示查询了一次。

5. 总结

  • 缓存是可以数据库开销。
  • Mybatis中有两种缓存:一级缓存和二级缓存
  • 一级缓存作用范围在SqlSession中,SqlSession,commit(增删改)会清空缓存。
  • 二级缓存作用范围在SqlSessionFactory,即是一个SqlSessionFactory生产出来的SqlSession共享同一二级缓存。
  • 二级缓存会随着同一mapper任意一个SqlSession进行了commit而清空
  • 一级缓存不需要手动开启。
  • 二级缓存需要设置SqlMapConfig、Mapper、和要查询的方法。

评论