介绍一些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<#{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算法:
先清除最久未使用的。
缓存对象 | 01 | 02 | 03 | 04 | ... | 0512 |
---|---|---|---|---|---|---|
最久未使用的时间/s | 14 | 99 | 83 | 1 | 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片装)
调试观察:
成功实现了多对一
上面只是一个商品描述,为更有说服力,试着查询前二十个商品描述:
成功显示
Comments | NOTHING