11.3 MyBatis进阶(上)



介绍一些MyBatis的高级特性。

一、MyBatis日志管理

1.1 日志

就是系统的历史记录。比如:

飞机失事的黑匣子,保存着失事前各个仪器的运行状况、通话记录等。

作用:

系统正常运行时,并不是特别关心;一旦系统出错,或者想要了解系统正在执行哪些任务时,就需要日志跟踪判断。

MyBatis框架自然也离不开日志。

1.2 Java中的日志:门面、实现

1.生活场景

比如插排:

外观上看,中国所有的插排面板要么是两孔,要么是三孔,统一的。

但是,内部结构上,不同的品牌比如公牛、小米等之间,是不同的。

2.开发场景

因为,上述将日志的门面、实现分开,所以给程序迁移提供了极大的便利。门面相同,只更换实现的组件即可。

其中,logback是目前主流的日志实现组件。

下面介绍,如何让MyBatis与logback一起协同作业,来输出日志。

1.3 让MyBatis与logback协同作业,自定义输出日志

logback日志实现的组件,能将执行过程中的SQL进行输出,对调式程序有极大帮助。

  • 在maven的核心文件pom.xml中,增加logback依赖:

  • 编写自定义日志的显示模式:

​ 在logback.xml中:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%thread] %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="debug">
        <appender-ref ref="console"/>            // 将上面的console传进来
    </root>
</configuration>
  • 相关的结果展现方式如下:

1.4 日志输出的级别

日志按照重要程度从高到低,分五种:

  • error--------错误:系统的故障日志
  • warn--------警告:存在有潜在风险或使用不当的日志
  • info----------一般性消息--------------------------------------------- 推荐生产环境
  • debug--------程序内部的调试信息--------------------------------推荐开发环境
  • trace---------程序运行的跟踪信息

注意:指定某级别,意思是:该级别及以上的级别,都会展现。

其他细节,可以查看官网文档:

二、MyBatis动态SQL

2.1 动态SQL

1.生活场景

以下就是动态SQL的典型案例:电商多条件检索

2.开发场景

动态SQL:本质就是在程序运行时,根据用户传入的参数,动态决定SQL长什么样的技术。

其中:

  • where标签可以保证完整的SQL语句语法正确

2.2 案例演示

在配置文件goods.xml中:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="goods">
    ...
    <!---->
    <select id="dynamicSQL" parameterType="java.util.Map" resultType="com.imooc.mybatis.entity.Goods">
        select * from t_goods
        where        // 先用where关键字形式试试
        <if test="categoryId!=null">                 <!--表示在参数的Map中,存在categoryId这个key-->
            and category_id=#{categoryId}            <!--SQL子句1-->
        </if>
        <if test="currentPrice!=null">
            and current_price&lt;#{currentPrice}     <!--SQL子句2-->
        </if>
    </select>
    
</mapper>

在单元测试类中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testDynamicSQl(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();

            Map param = new HashMap();
            param.put("categoryId", "44");
            param.put("currentPrice", "500");
            // 查询条件
            List<Goods> list = session.selectList("goods.dynamicSQL", param);
            for (Goods g : list) {
                System.out.println(g.getTitle() + ":" +
                        g.getCategoryId()+":"+g.getCurrentPrice());
            }
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }

以上报错的原因是:

SQL语句语法不对

2.3 改正写法1:增加1=1

成功得到了查询的数据:

动态改变输入的参数时,也成功查询到了相关的数据:

2.4 改正写法2:增加where标签

上面功能虽可,但是1=1明明没有意义,非得写在这里。所以,换个写法:where标签。

作用:就是根据查询条件,动态的组织SQ语句,保证语法正确。

不管参数怎么变,都成功得到了查询的数据:

三、MyBatis一级缓存、二级缓存

3.1 缓存用途

缓存就是缓冲存储、放于内存。

用于数据优化、提高程序执行效率。比如:

第一次查询时,从数据库中(硬盘)获得婴幼儿奶粉数据,并同时放一份到内存中;

下次查询相同数据时,直接从内存中提取。

内存的速度,比硬盘快几十倍

3.2 缓存分类

1.比较:

缓存分类一级缓存二级缓存
范围大小
开启方式默认开启手动开启
范围仅SqlSession
会话对象内
Mapper Namesapce
映射器的命名空间内
来源测试用例源码xml配置文件
缺点命中率低

2.图示:

其中:

  • 蓝色范围内:一级缓存
  • 红色范围内:二级缓存,里面的所有对象都可以被里面的一级缓存共享

3.3 二级缓存运行规则

(1)所有查询操作均使用缓存

(2)任何用户的写操作,一旦提交后,该namespace下的缓存,强制清空----------保证数据一致性

(3)如果想配置不使用缓存:useCache=false

(4)代表强制清空缓存flushCache=true

3.4 案例验证:一级缓存生存周期仅在SqlSession中

在单元测试类中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv1Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.getTitle());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
 }

    
--->
[main] 15:56:15.079 DEBUG goods.selectById - ==> Parameters: 1603(Integer)
[main] 15:56:15.100 DEBUG goods.selectById - <==      Total: 1
贝乐乐 环保实木无油漆婴儿床 多功能宝宝摇床可变书桌 送小摇送两个蚊帐519Y

在单元测试类中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv1Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            Goods goods1 = session.selectOne("goods.selectById", 1603);        // 相同查询
            System.out.println(goods.getTitle());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
 }

    
--->
[main] 15:56:15.079 DEBUG goods.selectById - ==> Parameters: 1603(Integer)        // 日志显示,只打印一次SQL
[main] 15:56:15.100 DEBUG goods.selectById - <==      Total: 1
贝乐乐 环保实木无油漆婴儿床 多功能宝宝摇床可变书桌 送小摇送两个蚊帐519Y    

打印上述两个变量的hashcode码:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv1Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            Goods goods1 = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode() + ":" + goods1.hashCode());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }
}

--->
752001567:752001567        // 哈希码相同,说明左右两个变量均指向同一个对象,因为两个变量在同一个Session会话范围内

如果两个变量,不再同一个Session会话范围内呢?

哈希码就会不同。如下:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv1Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            Goods goods1 = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode() + ":" + goods1.hashCode());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
        
        
        try {                                    // 多加一个try块,创建另一个Session会话范围
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            Goods goods1 = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode() + ":" + goods1.hashCode());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }
}

--->
752001567:752001567            // 上下两个变量,不再同一个Session会话范围内
1950701640:1950701640

一旦提交,一级缓存就会强制清空

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv1Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            Goods goods1 = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode() + ":" + goods1.hashCode());

        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
        
        
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            session.commit();                   // 一旦提交,该namespace中缓存强制清空
            Goods goods1 = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode() + ":" + goods1.hashCode());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }
}

--->
[main] 16:12:09.967 DEBUG goods.selectById - ==>  Preparing: select * from t_goods where goods_id = ?; 
[main] 16:12:09.996 DEBUG goods.selectById - ==> Parameters: 1603(Integer)
[main] 16:12:10.015 DEBUG goods.selectById - <==      Total: 1
752001567:752001567

[main] 16:12:10.018 DEBUG goods.selectById - ==>  Preparing: select * from t_goods where goods_id = ?; 
[main] 16:12:10.019 DEBUG goods.selectById - ==> Parameters: 1603(Integer)
[main] 16:12:10.022 DEBUG goods.selectById - <==      Total: 1
[main] 16:12:10.022 DEBUG goods.selectById - ==>  Preparing: select * from t_goods where goods_id = ?; 
[main] 16:12:10.023 DEBUG goods.selectById - ==> Parameters: 1603(Integer)
[main] 16:12:10.026 DEBUG goods.selectById - <==      Total: 1
243194708:931480286                // 因为上面中途提交了,内存中的缓存强制清空,所以才会有执行了两次SQL语句

3.5 案例验证:二级缓存范围扩大到Mapper Namesapce内

因为上述默认的一级缓存中,只能执行较少的语句,即内存的使用率不高。

因此,二级缓存应运而生。

在配置文件goods.xml中:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="goods">
    ...
     <!--手动开启了二级缓存-->
    <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/>
    
</mapper>

在单元测试类中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testLv2Cache(){
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }

        try {
            session = MyBatisUtils.openSession();
            Goods goods = session.selectOne("goods.selectById", 1603);
            System.out.println(goods.hashCode());
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }
}

--->
.JDBC4Connection@3315d2d7]
[main] 16:21:36.649 DEBUG goods.selectById - ==>  Preparing: select * from t_goods where goods_id = ?; 
[main] 16:21:36.677 DEBUG goods.selectById - ==> Parameters: 1603(Integer)
[main] 16:21:36.702 DEBUG goods.selectById - <==      Total: 1
199449817

[main] 16:21:36.704 DEBUG goods - Cache Hit Ratio [goods]: 0.5        // 因为二级缓存已手动开启,同一命名空间下,即使是不同的session也指向同一个对象。即直接从内存中提取,SQL执行一次
199449817

3.6 二级缓存标签cache中的四个属性

1.eviction:缓存的清除策略

缓存数量达到上限后,会自动触发回应算法来清除缓存。

其中,默认使用LRU算法。后三者较少使用。

(1)LRU算法:

先清除最久未使用的。

缓存对象01020304...0512
最久未使用的时间/s1499831 893

缓存数量上限是512个。

如果第513个对象进入缓存,那么就会将512个对象剔除代替;

接着,如果再第514个对象进入缓存,那么就会将02个对象进行剔除代替。

LFU算法;

先清除最近最少使用的,是次数

(2)FIFO算法

先进先出

(3)SOFT算法

软引用。

基于垃圾收集器和软引用规则

(4)WEAK算法

弱引用

基于垃圾收集器和软引用规则

2.flushInterval:缓存的清除间隔

单位:毫秒

600000=10min

3.size:缓存的长度,对象的数量上限

如果缓存是集合,就只能算一个对象。

建议保留单个的实体对象,不要保留大量的list集合查询结果。因为集合形式多变,缓存命中率比较低。

如果goods商品表有1400个商品,那么size应该设置为大于1400,确保所有的商品都可以进来“落落脚”

3.readOnly:是否是只读的

  • true代表只读。每次从缓存中取出的是缓存本身--------------------------------------------效率高,建议
  • false代表每次取出的只是缓存对象的“副本”,每一次取出的对象都是不同的--------安全性高

3.7 其他标签的缓存设置

除了在二级缓存标签cache中,其他的标签中也有一些关于缓存的设置。

1.useCache="false"不使用缓存

<mapper namespace="goods">
    ...
    // 全部查询获取的数据有1400条,很多且未来可能会更多,太占内存      // 所以,这里不使用缓存
    <select id="selectAll" resultType="com.imooc.mybatis.entity.Goods" useCache="false">
        select * from t_goods order by goods_id desc
    </select>
    
</mappe

2.flushCache="true"执行完SQL语句后,立马强制清空缓存

在某些特定的场景下,想要在执行完insert语句后立马清空缓存,而不是等待后面的commit提交后才清除。

<mapper namespace="goods">
    ...
    <insert id="insert2" parameterType="com.imooc.mybatis.entity.Goods" flushCache="true"> 
        insert into t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id)
        values (#{title},#{subTitle},#{originalCost},#{currentPrice},#{discount},#{isFreeDelivery},#{categoryId});
        <!--帮助主键回填-->                
        <selectKey resultType="Integer" keyProperty="goodsId" order="AFTER">
            select last_insert_id()
        </selectKey>
    </insert>
    
</mappe

四、MyBatis多表级联查询

以前接触的多表查询是关联查询,接下来介绍级联查询。

以下是两者对比:

比较关联查询级联查询
两个表,通过主外键,在一条SQL中完成所有数据的提取通过一个对象,获取与他关联的另外一个对象
执行的SQL语句是多条的
级联查询更像面向对象

4.1 三种实体关系

1.生活场景

学生的管理系统

其中:

  • 多对多的关系中,需要额外的第三章表

2.开发场景

电商系统,商品与详情对象之间,是一对多关系

即,一个商品有多个描述图片,但是一张描述图片只能是一种商品的。

一对多、多对一的好处:

  • 可以将繁琐的所有的SQL语句,被MyBatis自动执行
  • 降低风险,减少开发人员的工作量

4.2 OneToMany对象关联查询

新建商品详情的实体类GoodsDetail.java中:

public class GoodsDetail {
    private Integer gdId;
    private Integer goodsId;
    private String gdPicUrl;
    private Integer gdOrder;

    public Integer getGdId() {
        return gdId;
    }
    ...
}

相对应的,增加相应的mappers文件:

goods_detail.xml中:在Many中

<?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="goodsDetail">
    <select id="selectByGoodsId" parameterType="Integer"
            resultType="com.imooc.mybatis.entity.GoodsDetail">
        select * from t_goods_detail where goods_id = #{value}
    </select>
    
</mapper>

进行对象关联:在One中

public class Goods {

    private Integer goodsId;            // 商品编号     // 包装类
    private String title;               // 标题
    private String subTitle;            // 子标题
    private Float originalCost;         // 原始价格
    private Float currentPrice;         // 当前价格
    private Float discount;             // 折扣率
    private Integer isFreeDelivery;     // 是否包邮:1-包邮;0-不包邮
    private Integer categoryId;         // 分类编号

    private List<GoodsDetail> goodsDetails;      // 将Many设为One的一个属性,这样一个One对象下面,就有多个Many对象

   ...

    public List<GoodsDetail> getGoodsDetails() {
        return goodsDetails;
    }

    public void setGoodsDetails(List<GoodsDetail> goodsDetails) {
        this.goodsDetails = goodsDetails;
    }
}

上述已进行关联,但是list集合中的数据仍然是空的:

goods.xml中:

resultmap不仅可以像以前一样说明列与列之间的映射关系,同时还可以说明一对多或者多对一的映射逻辑。

<?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="goods">
    ...
    <!--type:执行One的实体-->
    <resultMap id="rmGoods1" type="com.imooc.mybatis.entity.Goods">
        <id column="goods_id" property="goodsId"></id><!--映射:goods对象的主键到goods_id字段-->
            <!--对集合list进行说明,说明从哪里取值-->
        <collection property="goodsDetails" select="goodsDetail.selectByGoodsId" 
                    column="goods_id"/>
    </resultMap>

    <select id="selectOneToMany" resultMap="rmGoods1">
        select * from t_goods limit 0,1
    </select>
</mapper>

在测试用例中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testOneToMany() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            List<Goods> list = session.selectList("goods.selectOneToMany");

            for (Goods goods : list) {
                System.out.println(goods.getTitle() + ":" + goods.getGoodsDetails().size());
            }
        } catch(Exception e){
            throw e;
        } finally{
            MyBatisUtils.closeSession(session);
        }
    }
}

在核心配置文件mybatis-config.xml中:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    ...
    <mappers>
        <mapper resource="mappers/goods.xml"/>
        <mapper resource="mappers/goods_detail.xml"/>        // 在核心配置文件,进行注册
    </mappers>
</configuration>

结果为:

[main] 12:24:19.396 DEBUG goods.selectOneToMany - ==>  Preparing: select * from t_goods limit 0,1 
[main] 12:24:19.426 DEBUG goods.selectOneToMany - ==> Parameters: 
[main] 12:24:19.448 DEBUG goodsDetail.selectByGoodsId - ====>  Preparing: select * from t_goods_detail where goods_id = ? 
[main] 12:24:19.448 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 740(Integer)
[main] 12:24:19.465 DEBUG goodsDetail.selectByGoodsId - <====      Total: 11
[main] 12:24:19.465 DEBUG goods.selectOneToMany - <==      Total: 1
爱恩幼 孕妇护肤品润养颜睡眠面膜 100g:11        // 该产品有11个商品描述图片

调试观察:

上面只是一个产品,为更有说服力,试着查询前十个产品:

成功显示

4.3 ManyToOne对象关联查询

多的一方,要关联一的一方,只需要有一的实体即可:

在商品详情的实体类GoodsDetail.java中:

public class GoodsDetail {
    private Integer gdId;
    private Integer goodsId;
    private String gdPicUrl;
    private Integer gdOrder;

    private Goods goods;        // 多的一方要关联一的一方,只需要多方新增一属性,为一方实体

    ...

    public Goods getGoods() {
        return goods;
    }

    public void setGoods(Goods goods) {
        this.goods = goods;
    }
}

goods_detail.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="goodsDetail">
    ...
    <resultMap id="rmGoodsDetail" type="com.imooc.mybatis.entity.GoodsDetail">
        <id column="gd_id" property="gdId"/>
        <association property="goods" select="goods.selectById" column="goods_id"></association>
    </resultMap>

    <select id="selectManyToOne" resultMap="rmGoodsDetail">
        select * from t_goods_detail limit 0,1
    </select>
    
</mapper>

在单元测试类中:

// 单元测试类
public class MyBatisTestor {

    // 单元测试用例
    @Test
    public void testManyToOne() throws Exception {
        SqlSession session = null;
        try {
            session = MyBatisUtils.openSession();
            List<GoodsDetail> list = session.selectList("goodsDetail.selectManyToOne");
            for (GoodsDetail gd : list) {
                System.out.println(gd.getGdPicUrl() + ":" + gd.getGoods().getTitle());
            }
        } catch (Exception e) {
            throw e;
        } finally {
            MyBatisUtils.closeSession(session);
        }
    }
}

结果为:

[main] 13:02:16.387 DEBUG goodsDetail.selectManyToOne - ==>  Preparing: select * from t_goods_detail limit 0,1 
[main] 13:02:16.417 DEBUG goodsDetail.selectManyToOne - ==> Parameters: 
[main] 13:02:16.439 DEBUG goods - Cache Hit Ratio [goods]: 0.0
[main] 13:02:16.439 DEBUG goods.selectById - ====>  Preparing: select * from t_goods where goods_id = ? 
[main] 13:02:16.440 DEBUG goods.selectById - ====> Parameters: 739(Integer)
[main] 13:02:16.442 DEBUG goods.selectById - <====      Total: 1
[main] 13:02:16.443 DEBUG goodsDetail.selectManyToOne - <==      Total: 1
http://img05.meituncdn.com/group1/M00/04/63/987d578f4a05497190497ca46391bfb4.jpg:亲润 孕妇护肤品豆乳大米盈润保湿胶原蚕丝面膜(18片装)

调试观察:

成功实现了多对一

上面只是一个商品描述,为更有说服力,试着查询前二十个商品描述:

成功显示

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 11.3 MyBatis进阶(上)


Follow excellence, and success will chase you.