20201120黄春跃

20201120黄春跃

知识点

Mybatis 延迟加载策略

通过前面的学习,我们已经掌握了 Mybatis 中一对一,一对多,多对多关系的配置及实现,可以实现对
象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的账户信
息。此时就是我们所说的延迟加载。

为什么要延迟加载?

延迟加载:
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张
表速度要快。
坏处:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗
时间,所以可能造成用户等待时间变长,造成用户体验下降。

实现需求

查询账户(Account)信息并且关联查询用户(User)信息。如果先查询账户(Account)信息即可满足要求,
当我们需要查询用户(User)信息时再查询用户(User)信息。把对用户(User)信息的按需去查询就是延迟加
载。
之前我们实现多表操作时,我们使用了resultMap来实现一对一,一对多,多对多关系的操作。主要是
通过 association、collection 实现一对一及一对多映射。association、collection 具备延迟加载功能。

使用 assocation 实现延迟加载

需求:
查询账户信息同时查询用户信息。

账户的持久层 DAO 接口

public interface IAccountDao {
  /**
  * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息
  * @return
  */
  List<Account> findAll();
}

账户的持久层映射文件

<?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.itlaobing.dao.IAccountDao"><!-- 建立对应关系 -->
  <resultMap type="account" id="accountMap">
    <id column="id" property="id"/>
    <result column="uid" property="uid"/>
    <result column="money" property="money"/>
    <!-- 它是用于指定从表方的引用实体属性的 -->
    <association property="user" javaType="user"
          select="com.itlaobing.dao.IUserDao.findById"
          column="uid">
    </association>
  </resultMap>
    <!-- 配置查询所有操作-->
    <select id="findAll" resultMap="accountMap">
     select * from account
    </select>
</mapper>

select: 填写我们要调用的 select 映射的 id
column : 填写我们要传递给 select 映射的参数

开启 Mybatis 的延迟加载策略

Mybatis默认没有打开懒加载配置,我们需要在 Mybatis 的配置文件 SqlMapConfig.xml 文件中添加延
迟加载的配置。

<settings>
    <!-- 开启懒加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
  </settings>

mybatis 3.4.1之后aggressiveLazyLoading 默认值为false所以可以不设置这个

编写测试只查账户信息不查用户信息。

package com.itlaobing.dao;
import com.itlaobing.model.Account;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
package com.itlaobing.dao;
import com.itlaobing.model.Account;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
1
2
3
4
5
6
7
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
import static org.junit.Assert.*;
/**
* @Classname IAccountDaoTest
* @Description TODO()
* @Date 2019/12/11 19:35
* @Author by Administrator
* @Version v1.0
*/
public class IAccountDaoTest {
  private InputStream in ;
  private SqlSessionFactory factory;
  private SqlSession session;
  private IAccountDao accountDao;
  @Before//在测试方法执行之前执行
  public void init()throws Exception {
    //1.读取配置文件
    in = Resources.getResourceAsStream("SqlMapConfig.xml");
    //2.创建构建者对象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    //3.创建 SqlSession 工厂对象
    factory = builder.build(in);
    //4.创建 SqlSession 对象
    session = factory.openSession();
    //5.创建 Dao 的代理对象
    accountDao = session.getMapper(IAccountDao.class);
 }
  @After//在测试方法执行完成之后执行
  public void destroy() throws Exception{
    //7.释放资源
    session.close();
    in.close();
 }
  @Test
  public void findAll() {
    //6.执行操作
    List<Account> accounts = accountDao.findAll();
 }
}

我们发现,因为本次只是将 Account对象查询出来放入 List 集合中,并没有涉及到 User对象,所以就
没有发出 SQL 语句查询账户所关联的 User 对象的查询。

修改测试类代码:

@Test
  public void findAll() {
    //6.执行操作
    List<Account> accounts = accountDao.findAll();
    System.out.println("我要看第一条账户的用户信息");
    System.out.println(accounts.get(0).getUser());
 }

我们发现当我们在调用用户信息的时候才会去调用查询用户信息的sql去数据库查询用户信息。

使用 Collection 实现延迟加载

同样我们也可以在一对多关系配置的结点中配置延迟加载策略。
节点中也有 select 属性,column 属性。
需求:
完成加载用户对象时,查询该用户所拥有的账户信息

在 User 实体类中加入 List属性

@Data
public class User {
  private int id;
  private String name;
  private String password;
  private List<Account> accounts;
}

编写用户和账户持久层接口的方法

IUserDao.java

/**
  * 查询所有用户,同时获取出每个用户下的所有账户信息
  * @return
  */
  List<User> findAll();

IAccountDao

/**
  * 根据用户编号查询账户信息
  * @param uid
  * @return
  */
  List<Account> findByUid(int uid);

编写用户持久层映射配置

IUserDao.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.itlaobing.dao.IUserDao">
  <!-- 建立对应关系 -->
  <resultMap type="user" id="userMap">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="password" property="password"/>
    <!-- collection 是用于建立一对多中集合属性的对应关系
      ofType 用于指定集合元素的数据类型
      select 是用于指定查询账户的唯一标识(账户的 dao 全限定类名加上方法名称)
      column 是用于指定使用哪个字段的值作为条件查询
      -->
    <collection property="accounts" ofType="account"
      select="com.itlaobing.dao.IAccountDao.findByUid" column="id">
    </collection>
  </resultMap>
    <!-- 配置查询所有操作-->
  <select id="findAll" resultMap="userMap">
   SELECT * from user
  </select>
  <!-- 根据 id 查询 -->
  <select id="findById" resultType="user" parameterType="int">
   select
   *
   from user where id = #{value}
  </select>
</mapper>

标签:主要用于加载关联的集合对象
select 属性:用于指定查询 account 列表的 sql 语句,所以填写的是该 sql 映射的 id
column 属性:用于指定 select 属性的 sql 语句的参数来源,上面的参数来自于 user 的 id 列,所
以就写成 id 这一个字段名了

  1. select 属性中不一定是其他xml(DAO/Mapper)文件中的方法,也可以是当前xml中对应
    的方法
  2. 如果column属性需要多个参数时,使用 {key1=value1,key2=value2} 这种方式传参
    例如:{uid=id}

编写账户持久层映射配置

IAccountDao.xml

<!-- 根据用户 id 查询账户信息 -->
  <select id="findByUid" resultType="account" parameterType="int">
   select * from account where uid = #{uid}
  </select>

Mybatis 缓存

为什么使用缓存

缓存(也称作cache)的作用是为了减去数据库的压力,提高数据库的性能。缓存实现的原理是从数据
库中查询出来的对象在使用完后不要销毁,而是存储在内存(缓存)中,当再次需要获取该对象时,直
接从内存(缓存)中直接获取,不再向数据库执行select语句,从而减少了对数据库的查询次数,因此
提高了数据库的性能。缓存是使用Map集合缓存数据的。(非关系型数据库 Redis key-value)
Mybatis有一级缓存和二级缓存。一级缓存的作用域是同一个SqlSession,在同一个sqlSession中
两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓
存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一
级缓存也就不存在了。Mybatis默认开启一级缓存(本地的会话缓存)。
二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次
执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完
毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而
提高查询效率。
总结:Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。
Mybatis 中缓存分为一级缓存,二级缓存。

Mybatis 一级缓存

证明一级缓存的存在

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush、close或提交事务,它就存在

编写测试方法

@Test
  public void testFindById(){
    User user = userDao.findById(2);
    System.out.println("第一次查询的用户:"+user);
    User user2 = userDao.findById(2);
    System.out.println("第二次查询用户:"+user2);
    System.out.println(user == user2);
 }

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

一级缓存的分析

一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方
法时,就会清空一级缓存。防止后续查询发生脏读(脏读:查询到过期的数据)

第一次发起查询用户 id 为 2 的用户信息,先去找缓存中是否有 id 为 2 的用户信息,如果没有,从数据
库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。第二次发起查询用户 id 为 2的用户信
息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),会清空 SqlSession 中的一级缓存,
这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
Mybatis内部存储缓存使用一个HashMap缓存数据,key为hashCode+sqlId+Sql语句。value为从查询
出来映射生成的java对象。
一级缓存的范围有SESSION和STATEMENT两种,默认是SESSION,如果不想使用一级缓存,可以把一
级缓存的范围指定为STATEMENT,这样每次执行完一个Mapper中的语句后都会将一级缓存清除。

<setting name="localCacheScope" value="STATEMENT"/> 

需要注意的是
当Mybatis整合Spring后,直接通过Spring注入Mapper(Dao)的形式,如果不是在同一个事务中每个
Mapper的每次查询操作都对应一个全新的SqlSession实例,这个时候就不会有一级缓存的命中,但是
在同一个事务中时共用的是同一个SqlSession。如有需要可以启用二级缓存。

测试一级缓存的清空

@Test
  public void testFindById(){
    User user = userDao.findById(2);
    System.out.println("第一次查询的用户:"+user);
    //此方法也可以清空缓存
    session.clearCache();
    User user2 = userDao.findById(2);
    System.out.println("第二次查询用户:"+user2);
    System.out.println(user == user2);
 }

Mybatis 二级缓存

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个
SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询的数据缓存在同一
个区域,如果使用mapper代理方法每个mapper的namespace都不同,此时可以理解为二级缓存区域
是根据mapper划分。
每次查询会先从缓存区域查找,如果找不到则从数据库查询,并将查询到数据写入缓存。Mybatis内部
存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对
象。
sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域,防止脏读。

二级缓存结构图

首先开启 mybatis 的二级缓存。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接
从缓存中取出数据。
如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 commit 提交,将会清空该 mapper 映射下的
二级缓存区域的数据。

二级缓存的开启与关闭

第一步:在 SqlMapConfig.xml 文件开启二级缓存

 <settings>
    <!-- 开启二级缓存的支持 -->
    <setting name="cacheEnabled" value="true"/>
  </settings>

因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。为 true 代表开启二级缓存;
为false 代表不开启二级缓存。

第二步:配置相关的 Mapper 映射文件

标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace
值。

<mapper namespace="com.itlaobing.dao.IUserDao">
  <!-- 开启二级缓存-->
  <cache />
 //或者
  <cache></cache>
 
 //省略其他节点信息
</mapper>

将 UserDao.xml 映射文件中的标签中设置 useCache=”true”代表当前这个 statement 要使用二
级缓存,如果不使用二级缓存可以设置为 false。
注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存。

第三步:POJO序列化

将所有的POJO类实现序列化接口Java.io. Serializable

缓存总结

对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据
库访问量,提高访问速度,例如:耗时较高的统计分析sql。通过设置刷新间隔时间,由mybatis每隔一
段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为60分钟、24小时
等。
对于实时性要求较高的查询不能使用缓存,例如股票行情。

总结

晚自习在牛客网刷了30道Java基础题。

标签

评论

this is is footer