2020:0606 --- mybatis(四)

221 阅读13分钟

今日内容

1. Mybatis中的延迟加载
        什么是延迟加载
        什么是立即加载
2. Mybatis中的缓存
        什么是缓存
        为什么使用缓存
        什么样的数据能使用缓存,什么样的数据不能使用缓存
        mybatis中的一级缓存和二级缓存
3. Mybatis中的注解开发
        环境搭建
        多表CRUD操作(代理Dao方式)
        多表查询操作
        缓存的配置

1 Mybatis中的延迟加载

1.1 延迟加载概述

    问题:在一对多情况中,当我们有一个用户,他有100个账户。
          在查询用户:的时候,要不要把关联的账户查出老?
          在查询账户的时候,要不要把关联的用户查出来?
          
    1.查询用户:要不要把关联的账户查出老?
        * 我们要做的功能是查询用户(user),但是由于我们用户里面关联了一个账户集合(accounts),
    且这个用户有100个账户,我们一查用户就会把这100个账户也查出来。显然这对内存是一个非常大的
    开销。
        所以我们如果用不到这100个账户信息时,是完全不应该将它查询出来的。
        既然没有查出来,拿到用的时候又该怎么办呢?
        
        * 最好的解决办法:延迟加载
        在查询用户时,用户下的账户信息应该是,什么时候使用,什么时候查询。
        ---------------------延迟加载
        
    2.查询账户:要不要把关联的用户查出来?
        通常情况下,在查询账户时,账户的所属用户信息,应该是随着账户查询时一起查询出来。
        ---------------------立即加载
    
    3.延迟加载    
        在真正使用数据时才发起查询,不用的时候不查询。按需加载(懒加载)
    4.立即加载
        不管用不用,只要一调用方法,马上发起查询加载。
        
    5.在对应的四种表关系中:
        一对多,多对多
            关联的对象是多:延迟加载
        多对一,一对一
            关联的对象是一:立即加载

1.2 延迟加载案例需求

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

注意:
    既然是延迟加载,那么我们查询出来的结果就应该是查询账户(Account)的信息,再按需查询
用户(User)的信息。
    sql:select * from account
    而不是:select u.*, a.id as aid, a.uid, a.money from account a, user u where u.id = a.uid;

1.3 使用association实现延迟加载

1.3.1 账户的持久层DAO接口
```
public interface IAccountDao {

    /**
     * 查询所有账户
     * @return
     */
    List<Account> findAll();
}
```
1.3.2 账户的持久层映射文件(一)
```
<mapper namespace="com.itheima.Dao.IAccountDao">
    <!--定义封装account和user的resultMap-->
    <!--配置查询结果的列名和实体类的属性名的对应关系-->
    <resultMap id="accountUserMap" type="com.itheima.domain.Account">
        <id property="id" column="id"></id>
        <result property="uid" column="uid"></result>
        <result property="money" column="money"></result>
        
        <association property="user" column="uid" javaType="User" 
                        select="com.itheima.Dao.IUserDao.findById">

        </association>
    </resultMap>
    
</mapper>
```
1.3.3 association 映射分析
    ```
    <association property="user" column="uid" javaType="User" select="com.itheima.Dao.IUserDao.findById">
    ```
    
    * 1首先association是用来映射Account实体类中的引用实体属性:private User user;
    ```
        public class Account implements Serializable {

        private Integer id;
        private Integer uid;
        private Double money;
    
        //从表实体应该包含一个主表实体的对象引用
        private User user;
        }
    ```
    * 2 asscociation的各个属性分析

    * 3select属性分析:
        select="com.itheima.Dao.IUserDao.findById"
        
            填写我们要调用的select映射(标签)的id。其实是namespace+id。根据这个我们能在所有的映射文件
        XxxDao.xml找到这个唯一的select,进行调用。
        
            此处是IUserDao.xml中的select
            
            ```
            <select id="findById" parameterType="INT" resultType="User">
                select * from user where id = #{id};
            </select>
            ```
        即:com.itheima.Dao.IUserDao.findById(column : id) column就是association的column属性
    
    
    * 4很重要:配置好了之后,到底是怎么延迟加载呢?
        
        lazyLoadingEnabled: 注意这是特别针对关联对象的延迟加载
            延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 
            特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。
        aggressiveLazyLoading:
            我们要关闭这个积极加载关联对象属性,关掉后就成了按需加载。
       
            
##### 1.3.4 账户的持久层映射文件(二)
  
    ```
    <resultMap id="accountUserMap" type="com.itheima.domain.Account">
        <id property="id" column="id"></id>
        <result property="uid" column="uid"></result>
        <result property="money" column="money"></result>
        
        <association property="user" column="uid" javaType="User" 
                        select="com.itheima.Dao.IUserDao.findById">
        </association>
    </resultMap>
    ```  
      
    使用配置好的<resultMap> <association>...</association>  </resultMap> 
    resultMap="accountUserMap":
    ```
    <!--查询所有方法-->
    <select id="findAll" resultMap="accountUserMap">
        select * from account
    </select>
    ```
    
    注意:此时如果没有开启lazyLoadingEnabled和aggressiveLazyLoading属性时,会将
          select * from account
          select * from user where id = #{id};
          都执行了。
          
        因为我们把resultMap="accountUserMap":加到了<select id="findAll" resultMap="accountUserMap">
    里面,显然是要调用这个映射且执行的。
1.3.5 最后配置lazyLoadingEnabled和aggressiveLazyLoading属性
* 主配置文件:SqlMapConfig.xml
    ```
    <settings>
        <!--打开延迟加载的开关-->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!--将积极加载改为按需加载-->
        <!--在 3.4.1 及之前的版本中默认为 true,我们的mybatis的依赖是3.4.5,所以这个不为之也可以,此处为了演示-->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    ```

* 将Test中的打印User删掉(不然会被认为是按需)
    ```
    for (Account account : accounts) {
        System.out.println("----------------------");
        System.out.println(account);
        System.out.println(account.getUser());
    }
    ```
控制台结果:

1.4 使用collection实现延迟加载

1.4.1 在User实体类中加入List属性
```
public class User implements Serializable {

    private Integer id;
    private String username;
    private String address;
    private String sex;
    private Date birthday;

    //一对多关系映射:每个用户可能有多个账户
    //主表实体应该包含从表实体的集合引用(因为是一对多)
    private List<Account> accounts;

    public List<Account> getAccounts() {
        return accounts;
    }

    public void setAccounts(List<Account> accounts) {
        this.accounts = accounts;
    }
}
```
1.4.2 编写用户和账户持久层接口的方法
```
public interface IUserDao {

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

public interface IAccountDao {
    /**
     * 根据用户id查询账户信息
     * @param uid
     * @return
     */
    List<Account> findAccountByUid(Integer uid);
}

```
1.4.3 编写用户持久层映射配置
```
    <!--定义User的resultMap-->
    <resultMap id="userAccountMap" type="com.itheima.domain.User">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="sex" column="sex"></result>
        <result property="address" column="address"></result>
        <result property="birthday" column="birthday"></result>
        <!--配置 user对象中account集合的映射-->
        <!--注意:findAccountByUid需要的参数是user中的id-->
        <collection property="accounts" ofType="account" select="com.itheima.Dao.IAccountDao.findAccountByUid" column="id">

        </collection>
    </resultMap>
    
    <!--查询所有方法-->
    <select id="findAll" resultMap="userAccountMap">
        SELECT * FROM USER
    </select>
```

    <collection>:用于加载关联的集合对象
    select属性:
        用于指定查询account列表的sql语句,所以填的是该sql的映射id
    column属性
        用于指定select属性的sql语句的参数来源,该例是来自于user的id列
1.4.4 编写账户持久层映射配置
```
    <!--根据用户id查询账户列表-->
    <select id="findAccountByUid" resultType="account">
        <!--
            注意这里我们没有parameterType但是仍然写了#{uid}
            所以这个uid不是来自parameterType而是来自IUserDao.xml中调用该映射是穿过来的参数column="id",
            而且这个是user中的id,对应着account中的uid字段。
        -->
        select * from account where uid = #{uid}
    </select>
```
1.4.5 测试只加载用户信息
```
    /**
     * 测试查询所有
     */
    @Test
    public void testFindAll(){
    
        List<User> users = usertDao.findAll();
        
    }

```
1.4.6 测试结果

发现只执行了查询用户的sql语句,没有加载Account账户信息。

2 Mybatis中的缓存

2.1 缓存概述

1. 什么是缓存:
    存在于内存中的临时数据,下次使用的时候不用再去数据库查询,减少和数据库交互的次数,从而能够快速的
将数据获取出来。
2. 为什么使用缓存
    减少和数据库的交互次数,提高执行效率
3. 什么样的数据能使用缓存,什么样的数据不能使用
    适用于缓存的:
        经常查询的并且不经常改变的。
        数据的正确与否对最终结果不大的。
    不适用于缓存的:
        经常改变的,数据的正确与否对最终的结果影响很大的。
        例如:商品的库存,银行的汇率,股市的牌价
    可能出现的问题:导致缓存和数据库中的数据不同步的问题。
    
概述:像大多数的持久化框架一样,Mybatis也提供了缓存策略,通过缓存策略来减少数据库的查询次数
从而提高性能

2.2 Mybatis中的一级缓存

    一级缓存:SqlSession级别的缓存。
        它指的是Mybatis中的SqlSession对象的缓存。
        当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。
        该区域的结构是一个Map。当我们再次查询同样的数据,mybatis会先去sqlsession中
    查询是否有,有的话直接拿出来用。
        当sqlsessoin对象消失时,mybatis的一级缓存也就消失了。
        
    
    测试代码1:
    * 第一次是sql查询,第二次是从缓存中取的数据,两次获取的数据是一样的
    ```
        public void testFirstLevelCache(){
            //第一次是sql查询
            User user1 = usertDao.findById(41);
            System.out.println(user1);
    
            //第二次是从缓存中取的数据
            User user2 = usertDao.findById(41);
            System.out.println(user2);
    
            System.out.println(user1 == user2);
        }
    ```
    测试代码2:
    * 因为中间sqlSession关闭了,两次获取数据都是通过sql查询的。那么查询获得的不是同一个对象
    ```
        public void testFirstLevelCache(){
            //第一次是sql查询
            User user1 = usertDao.findById(41);
            System.out.println(user1);

            //关闭sqlSession
            sqlSession.close();
            //再次获取sqlSession对象
            sqlSession = factory.openSession();
            //重新获取代理对象
            usertDao = sqlSession.getMapper(IUserDao.class);*/
    
            //第二次是从缓存中取的数据
            User user2 = usertDao.findById(41);
            System.out.println(user2);
    
            System.out.println(user1 == user2);
        }
    ```
    
   测试代码3:
   sqlSession.clearCache(); 可以清空sqlSession中的缓存。
    ```
    public void testFirstLevelCache(){
        //第一次是sql查询
        User user1 = usertDao.findById(41);
        System.out.println(user1);

        //清空缓存
        sqlSession.clearCache();

        //第二次是从缓存中取的数据
        User user2 = usertDao.findById(41);
        System.out.println(user2);

        System.out.println(user1 == user2);
    }
    ```
2.2.2 触发清空Mybatis中的一级缓存
    为了防止缓存中的数据和数据库中的数据不一致。
    一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,删除,commit().close()等方法时,
就会清空一级缓存

2.3 Mybatis中的二级缓存

    二级缓存:
        它指的是Mybatis中的SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession
    共享其缓存。
    
    二级缓存的使用步骤:
        1.让Mybatis矿建二级缓存(在SqlMapConfig.xml中配置)
        ```
            <settings>
                <setting name="cacheEnabled" value="true"/>
            </settings>
        ```
        
        2.让当前的映射文件支持二级缓存(在IUserDao.xml中配置)

        3.让当前的操作支持二级缓存(在select标签中配置)

    *测试代码

*结果:

    发现第二次查找是从二级缓存中得到的数据,但是为什么输出结果是false呢?
        因为二级缓存中存放的内容,是数据而不是对象。
        谁要拿这个数据,就会创建一个新的实体类对象(和里面的数据对应的--这里是user),把数据
    填充到对象中。
        所以虽然没有发起新的查询,但是创建了新的对象,这样的话这两个对象就不是同一个了。

3 Mybatis中的注解开发

3.1 概述

    Mybatis也可以使用注解开发,这样我们就可以减少编写Mapper映射文件了(IUserDao.xml)
    
    @Select一个注解就干完了IUserDao.xml的所有事情:一个@Select注解就拿到了所有的IUserDao.xml信息。
    
        @Select注解在findAll()方法上,我们可以得到方法对象
        找到这个方法,那么就可以同事找到这个方法声明的类,以及它的全限定类名。
        
        @Select注解的value包含着Sql语句。
        
        返回值类型:List<User> findAll(); 就是<User>
        所以说:一个@Select注解就拿到了所有的IUserDao.xml信息。

3.2 存在IUserDao.xml配置文件时,再用注解

会报错:
    Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
    ### Error building SqlSession.
    ### The error may exist in com/itheima/dao/IUserDao.xml
    提示我们:存在IUserDao.xml配置文件
    
小细节:只要你使用了注解开发,同时你的配置文件下包含了对应的IUserDao.xml,就会报错。
        他发现你用了注解,又用了IUserDao.xml,mybatis不知道该以谁为准。
        
        如果一个dao用注解,那么这个dao就不会用IUserDao.xml
        
        所以我们要么把IUserDao.xml移到不相关的配置文件,或者删掉。
        即我们使用注解开发时,不能在同一个dao文件下使用IUserDao.xml。

3.3 如果实体类的属性名和数据库表中的列名没有一一对应

3.3.1 代码演示

* User类
```
@Data
public class User implements Serializable {

    /**
     * 由于属性名和数据库的列名没有一一对应,所以只能封装uerName(userName对应上了)
     */
    private Integer userId;
    private String userName;
    private String userAddress;
    private String userSex;
    private Date userBirthday;
    
}
```
* IUserDao
```
@Select("select * from user")
List<User> findAll();

```

* 查询结果

3.3.2 解决办法

1. 在Sql语句中起别名
    显然不仅仅调用findAll()方法时要起别名,调用findByiD()方法也要起别名。
    因为起别名只作用在一个SQL,一个方法上,不太好。

2. @Results注解
    ```
    @Select("select * from user")
    @Results(value={
            @Result(id=true,column="id",property = "userId"),
            @Result(column="username",property = "userName"),
            @Result(column="sex",property = "userSex"),
            @Result(column="address",property = "userAddress"),
            @Result(column="birthday",property :  = "userBirthday"),
    }, id="userMap")
    List<User> findAll();
    ```
    
    @Results 注解是一个 @Result 注解数组,
        @Result注解在这里用到3个值:
            id:默认值是false。 是不是主键,id列
            column :数据库表的列名
            property : 实体类的属性名
        @Results :
            id : 为 @Results指定一个唯一id值
    
3. 在其他方法处也可以引用这个注解:

    注意:@ResultMap()的value是一个数组:
        所以正确的写法:@ResultMap(value={"userMap"})
        
        如果只有一个属性value,可以省略
        如果value只有一个值:{}可以省略
        @ResultMap("userMap")  

4 Mybatis中多表查询的注解开发

4.1 一对一查询:一个账户对应一个用户

    明确:一对一查询:积极加载
    * Account实体类:
        ```
        public class Account implements Serializable {

            private Integer id;
            private Integer uid;
            private Double money;

            private User user;
        }
        ```
    * IAccountDao:
        ```
        @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.itheima.dao.IUserDao.findById", fetchType= FetchType.EAGER)),
    
        })
        List<Account> findAll(); 
        ```
        
        one=@One(select="com.itheima.dao.IUserDao.findById", fetchType= FetchType.EAGER):
            one属性:是@Result中的属性,表示一对一关系,值是@One注解
                @One注解:两个属性:
                    select:用于指定我们要调用的select映射信息
                    fetchType:一对一关系中,你所指定的加载方式(LAZY,EAGER,DEFAULT)

4.2 一对多查询:一个用户对应一个账户

many=@Many(select="", fetchType=FETCHTYPE.LAZY)

IUserDao:

IAccountDao;

4.3 缓存的配置

一级缓存无需配置,默认就有。
二级缓存开启:
  *1 SqlMapConfig.xml:

*2 测试代码:

  *3 因为是IUserDao的代理对象:所以在IUserDao在配置一下二级缓存

* 开启二级缓存前的测试结果:

* 开启二级缓存后的测试结果: