SSM之MyBatis系列(七)---- MyBatis中的延迟加载、缓存和注解开发

MyBatis中的延迟加载

延迟加载和立即加载

假如此时我们有 1 个用户,该用户拥有 100 个账户,那么问题来了:

  • 当我们查询用户的时候,要不要把关联的账户也一起查询出来?
  • 当我们查询账户的时候,要不要把关联的用户一起查询出来?

  答案很简单。在查询用户的时候,用户所拥有的账户信息应该是什么时候使用,什么时候查询,不然每次查询该用户的时候,都要查询他拥有的账户,那么开销无疑是比较大的;
  在查询账户的时候,由于每个账户对应一个用户,所以应该账户的所属用户信息应该是随着账户查询时一起查询出来,否则别人不知道该账户属于谁。

  • 什么是延迟加载:
  • 延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据,延迟加载也称按需加载(懒加载)。
  • 一对多多对多的表关系中,通常情况下我们都是采用延迟加载。
  • 什么是立即加载:
  • 立即加载就是不管是否需要数据,只要一进行查询,就会把相关联的数据一并查询出来
  • 多对一一对一的表关系中,通常情况下我们都是采用立即加载。

一对一实现延迟加载

  • 实现以下需求:
      当查询账户信息时使用延迟加载。也就是说,如果不需要使用用户信息的话,那么只查询账户信息;只有当需要使用用户信息时,才去关联查询

  • 首先在 Mybatis 的配置文件SqlMapConfig中开启延迟加载

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

第二条aggressiveLazyLoading可不写,因为它默认就是false

  • 注意,在编写 Mybatis 的配置文件时,文档结构一定不可以随便写,一定要按照官方文档Mybatis 的 XML 配置 所要求的顺序,比如说:<settings></settings> 标签不可以写在<environments></environments> 下方。具体文档结构见下图:
    在这里插入图片描述

  • 修改上一篇文章MyBatis的多表查询编写好的账户映射文件 AccountDao.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.Dao.AccountDao">
    <!-- 定义可以封装带有User的Account的 resultMap -->
    <resultMap id="accountUserMap" type="account">
        <id property="id" column="id"></id>
        <result property="uid" column="uid"></result>
        <result property="money" column="money"></result>
        <!-- 一对一的关系映射:配置封装user的内容-->
        <association property="user" column="uid" javaType="user" select="com.Dao.UserDao.findById"></association>
    </resultMap>
    <!-- 配置查询所有   -->
    <select id="findAll" resultMap="accountUserMap">
        select * from account
    </select>
</mapper>

  • <association></association>标签中的select属性表示我们要调用的映射语句的 ID,它会从column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
  • column 属性指定的内容:用户根据id查询时,所需要的参数的值。
  • 执行上篇文章的测试代码,运行结果如下:
    在这里插入图片描述

可以发现,现在查询账户的话,并不会和之前一样,把所有的账户信息连同用户信息一并查询出来。而是由于我们在测试代码中打印了 Account 对象,其中调用了 User 对象,才会进行查询。

一对多实现立即加载

- 实现以下需求:
  当查询用户信息时使用延迟加载。也就是说,如果不需要使用账户信息的话,那么只查询用户信息;只有当需要使用账户信息时,才去关联查询

  • 还是一样先修改UserDao.xml映射文件,不同的是现在是使用<collection></collection>进行延迟加载
<?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.Dao.UserDao">
    <!--定义user的resultMap-->
    <resultMap id="userAccountMap" type="user">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="sex" column="sex"/>
        <result property="birthday" column="birthday"/>
        <result property="address" column="address"/>
        <!--配置user对象中accounts集合的映射-->
        <collection property="accounts" ofType="account" select="com.Dao.AccountDao.findAccountByUid" column="id"> </collection>
    </resultMap>
    <!-- 配置查询所有   -->
    <select id="findAll" resultMap="userAccountMap">
        select * from user
    </select>
    <!--  根据ID查询用户  -->
    <select id="findById" parameterType="java.lang.Integer" resultType="user">
        select * from user where id = #{uid}
    </select>
</mapper>
  • 在账户实体类对应的接口层及配置文件中添加对应的方法
    /**
     * 根据用户id查询账户信息
     * @param uid
     * @return
     */
    List<Account> findAccountByUid(Integer uid);
    <!-- 根据用户id查询账户信息,延迟加载账户信息   -->
    <select id="findAccountByUid" resultType="account">
        select * from account where uid = #{uid}
    </select>
  • 运行结果
    在这里插入图片描述

Mybatis 中的缓存

  • 什么是缓存?

缓存就是存在于内存中的临时数据

  • 为什么要使用缓存?

为了减少和数据库交互的次数,提高执行效率

  • 适用于缓存的数据

经常查询并且不经常改变的数据。
数据的正确与否对最终结果影响不大的

  • 不适用于缓存的数据

经常改变的数据。 数据的正确与否对最终结果影响很大的。例如:商品的库存、银行的汇率、股市的牌价等。

Mybatis 的一级缓存

  一级缓存指的是MyBatis中SqlSession对象的缓存
  当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供的一块区域中。
  该区域的结果是一个Map。当我们再次查询同样的数据,mybatis会先去sqlsession中查询是否有,有的话就直接拿。
  当Sqlsession对象消失时,mybatis的一级缓存也就消失了。

下面我们用一段代码来测试一下:
在测试类中添加:

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

测试结果:
在这里插入图片描述

  我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是 Mybatis 提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 41 的记录时,并没有发出 sql 语句从数据库中查询数据,而是从一级缓存中查询。
  当我们调用sqlSession.clearCache();方法时,就可以清空缓存,这时在运行的话就会有两次查询

  • 一级缓存是 SqlSession级别的缓存,只要SqlSession没有flushclose,它就会存在。当调用 SqlSession修改、添加、删除、commit()、close()、clearCache()等方法时,就会清空一级缓存。

  • 一级缓存流程如下图

在这里插入图片描述

  • 第一次发起查询用户 id 为 1 的用户信息,Mybatis 会先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
  • 得到用户信息,将用户信息存储到一级缓存中。
  • 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),那么 Mybatis 就会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  • 第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
  • Mybatis 默认就是使用一次缓存的,不需要配置。
  • 一级缓存中存放的是对象。(一级缓存其实就是 Map 结构,直接存放对象)

Mybatis 的二级缓存

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

  • 二级缓存流程如下图

在这里插入图片描述

  • sqlSession1 去查询用户信息的时候,Mybatis 会将查询数据存储到二级缓存中。
  • 如果 sqlSession3去执行相同 Mapper 映射下的 SQL 语句,并且执行 commit 提交,那么 Mybatis 将会清空该 Mapper 映射下的二级缓存区域的数据。
  • sqlSession2 去查询与sqlSession1 相同的用户信息,Mybatis 首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。

如果想使用 Mybatis 的二级缓存,那么应该做以下配置:

  • 首先在 Mybatis 配置文件SqlMapConfig.xml中添加配置 (这一步其实可以忽略,因为默认值为 true)
<settings>
    <!-- 开启缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings> 
  • 接着在映射文件UseDao.xml中配置
<mapper namespace="cn.ykf.mapper.UserMapper">
    <!-- 使用缓存 -->
    <cache/>
</mapper>

    <!--  根据ID查询用户  -->
    <select id="findById" parameterType="java.lang.Integer" resultType="user" useCache="true">
        select * from user where id = #{uid}
    </select>
  • 针对每次查询都需要最新数据的操作,要设置成useCache="false",禁用二级缓存
  • 当我们使用二级缓存的时候,所缓存的类一定要实现java.io.Serializable接口,这样才可以使用序列化的方式来保存对象。
  • 由于是序列化保存对象,所以二级缓存中存放的是数据,而不是整个对象。
  • 编写二级缓存测试类试试效果:
package com.test;

import com.Dao.UserDao;
import com.domain.User;
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.InputStream;

public class SecondLevelCacheTest {
    
    
    private InputStream in;
    private SqlSessionFactory factory;


    /**
     * 测试之前执行,用于初始化
     */
    @Before
    public void init() throws Exception {
    
    
        //1. 读取配置文件
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2. 创建SqlSessionFactory工厂
        factory = new SqlSessionFactoryBuilder().build(in);

    }
    /**
     * 测试结束执行,用于提交事务和释放资源
     */
    @After
    public void destroy() throws Exception{
    
    
        in.close();
    }


    /**
     * 测试缓存
     */
    @Test
    public void testOne(){
    
    
        SqlSession sqlSession1 = factory.openSession();
        UserDao dao1 = sqlSession1.getMapper(UserDao.class);
        User user1 = dao1.findById(41);
        System.out.println("第一次查询的用户:"+user1);
        sqlSession1.close();//一级缓存消失

        SqlSession sqlSession2 = factory.openSession();
        UserDao dao2 = sqlSession2.getMapper(UserDao.class);
        User user2 = dao2.findById(41);
        System.out.println("第二次查询的用户:"+user2);
        sqlSession2.close();

        System.out.println(user1 == user2);
    }
}

在这里插入图片描述

  经过上面的测试,我们发现执行了两次查询,并且在执行第一次查询后,我们关闭了一级缓存,再去执行第二次查询时,我们发现并没有对数据库发出 sql 语句,所以此时的数据就只能是来自于我们所说的二级缓存。
  上面user1与user2的比较为什么是false呢?这是因为二级缓存存放的内容是数据而不是对象

MyBatis的注解开发

大家最好新建一个工程

  • 在 Mybatis 的注解开发中,常用的注解如下表所示:
注解 作用
@Intsert 实现新增
@Update 实现更新
@Delete 实现删除
@Select 实现查询
@Results 实现结果集封装
@ResultMap 实现引用 @Results 定义的封装
@One 实现一对一结果集封装
@Many 实现一对多结果集封装
@SelectProvider 实现动态 SQL 映射
@CacheNamespace 实现注解二级缓存的使用**

Mybatis 使用注解实现单表 CURD

  • 用户实体类接口层 UserDao 代码如下:
package com.Dao;

import com.domain.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;


import java.util.List;

/**
 * @author zhang
 * 用户的持久层接口
 */
public interface UserDao {
    
    
    /**
     * 查询所有用户
     * 在mybatis中针对CRUD一共有四个注解
     * @Select @Insert @Update @Delete
     * @return
     */
    @Select("select * from user")
    List<User> findAll();

    /**
     * 添加用户
     * @param user
     */
    @Insert("INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})")
    void saveUser(User user);

    /**
     * 修改用户
     * @param user
     */
    @Update("UPDATE user SET username = #{username}, birthday = #{birthday}, sex = #{sex}, address = #{address} WHERE id = #{id}")
    void updateUser(User user);

    /**
     * 根据id删除用户
     */
    @Delete("DELETE FROM user WHERE id = #{id}")
    void deleteUser(Integer userId);

    /**
     * 根据id查询单个用户
     *
     * @param userId
     * @return
     */
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Integer userId);

    /**
     * 根据姓名模糊查询多个用户
     * @param username
     * @return
     */
    @Select("SELECT * FROM user WHERE username LIKE #{username}")
    List<User> findUserByName(String username);

    /**
     * 查询用户总数
     *
     * @return
     */
    @Select("SELECT COUNT(*) FROM user")
    int findTotalUser();
}
  • 注意,如果此时实体类的属性与数据库表列名不一致,那么我们应该使用@Results、@Result、@ResultMap 等注解,如下
public interface UserMapper {
    
    
    /**
     * 查询所有用户
     *
     * @return
     */
    @Select("SELECT * FROM user")
    @Results(id = "UserMap",value = {
    
    
            @Result(id = true,property = "userId",column = "id"),
            @Result(property = "userName",column = "username"),
            @Result(property = "userBirthday",column = "birthday"),
            @Result(property = "userSex",column = "sex"),
            @Result(property = "userAddress",column = "address"),
    })
    List<User> listAllUsers();

    /**
     * 添加用户
     *
     * @param user
     * @return 成功返回1,失败返回0
     */
    @Insert("INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})")
    @ResultMap("UserMap")
    int saveUser(User user);

  • @Results 注解用于定义映射结果集,相当于标签 <resultMap></resultMap>
    • 其中,id 属性为唯一标识。
  • value 属性用于接收@Result[]注解类型的数组。
  • @Result注解用于定义映射关系,相当于标签 <id /><result />
    • 其中,id 属性指定主键。
    • property 属性指定实体类的属性名,column 属性指定数据库表中对应的列。
  • @ResultMap 注解用于引用 @Results定义的映射结果集,避免了重复定义映射结果集。
  • 测试类跟之前的基于代理Dao实现CRUD传送门一样,下面也是一样

Mybatis 使用注解实现多对一(一对一)

  • 还是以上一篇文章的用户和账户为例子。一个用户可以有多个账户,而一个账户只能对应一个用户。
  • 在 Mybatis 中,多对一是作为一对一来进行处理的。也就是说,虽然多个账户可以属于同一个用户(多对一),但是在实体类中,我们是在账户类添加一个用户类的对象引用,以此来表明所属用户。(此时就相当于一对一)
  • 账户实体类和用户实体类见上篇文章,接口层代码如下:
package com.Dao;

import com.domain.Account;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;

import javax.annotation.Resources;
import java.util.List;

public interface AccountDao {
    
    

    /**
     * 查询所有账户,采用立即加载
     * @return
     */
    @Select("SELECT * FROM account")
    @Results(id = "accountMap",value = {
    
    
            @Result(id = true,column = "id",property = "id"),
            @Result(column = "uid",property = "uid"),
            @Result(column = "money",property = "money"),
            @Result(property = "user", column = "uid",
                    one = @One(select = "com.Dao.UserDao.findById", fetchType = FetchType.EAGER))
    })
    List<Account> findAll();


}

  • @One注解相当于标签 <association></association>,是多表查询的关键,在注解中用来指定子查询返回单一对象
    • 其中,select属性指定用于查询的接口方法,fetchType属性用于指定立
    • 加载或延迟加载,分别对应 FetchType.EAGERFetchType.LAZY
  • 在包含 @one 注解的 @Result 中,column 属性用于指定将要作为参数进行查询的数据库表列。

Mybatis 使用注解实现一对多

  • 为用户实体类添加包含账户实体类的集合引用,具体代码见上篇文章。接口层代码如下
package com.Dao;

import com.domain.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;


import java.util.List;

/**
 * @author zhang
 * 用户的持久层接口
 */
public interface UserDao {
    
    
    /**
     * 查询所有用户
     * 在mybatis中针对CRUD一共有四个注解
     * @Select @Insert @Update @Delete
     * @return
     */
    @Select("select * from user")

    @Results(id = "userMap",value = {
    
    
            @Result(id = true,column = "id",property = "id"),
            @Result(column = "username",property = "username"),
            @Result(column = "address",property = "address"),
            @Result(column = "sex",property = "sex"),
            @Result(column = "birthday",property = "birthday"),
            @Result(property = "accounts", column = "id",
                    many = @Many(select = "com.Dao.AccountDao.findAccountByUid", fetchType = FetchType.LAZY))
    })
    List<User> findAll();

    /**
     * 根据用户id,查询账户信息
     * @param userId
     * @return
     */
    @Select("SELECT * FROM account WHERE uid = #{userId}")
    List<Account> findAccountByUid(Integer userId);
  • @Many 注解相当于标签 <collection></collection>,是多表查询的关键,在注解中用来指定子查询返回对象集合
    • 其中,select 属性指定用于查询的接口方法,fetchType属性用于指定立即加载或延迟加载,分别对应FetchType.EAGERFetchType.LAZY
  • 在包含 @Many 注解的 @Result 中,column属性用于指定将要作为参数进行查询的数据库表列。

Mybatis 使用注解实现二级缓存

  • 如果使用注解时想开启二级缓存,那么首先应该在 Mybatis 配置文件 SqlMapConfig.xml中开启全局配置
<settings>
    <!-- 开启缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

  • 接着在持久层接口中使用注解配置二级缓存
@CacheNamespace(blocking = true)//mybatis 基于注解方式实现配置二级缓存
public interface UserDao {
    
    
	// .....
}

猜你喜欢

转载自blog.csdn.net/weixin_43844418/article/details/113598228