MyBatis进阶
笔记内容:日志管理、动态SQL、缓存、对象关联查询、分页、批处理和注解
日志管理
日志文件作用:用于记录系统操作事件的记录文件或文件集合,日志保存历史数据,是诊断问题以及理解系统活动的重要依据。
日志分为两部分:比如,SLF4j与Logback,如下图所示
日志门面和日志实现作用区别:统一的门面屏蔽了底层复杂的实现,门面就像插盘的面板规格,插盘内部的电路设计细节不同。门面和实现分开有助于数据迁移。
日志实现组件作用:提供日志的打印、输出、管理
使用步骤
在pom文件中加入logback依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
可以自定义控制台输出日志的格式:在resources目录下新建logback.xml,规定控制台的输出日志格式。
一般调试时,设置root level级别为debug以上,方便调试。
<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>
<!--
日志输出级别(优先级高到低):
error: 错误 - 系统的故障日志
warn: 警告 - 存在风险或使用不当的日志
info: 一般性消息
debug: 程序内部用于调试信息
trace: 程序运行的跟踪信息
-->
<root level="debug">
<appender-ref ref="console"/>
</root>
</configuration>
实现效果,控制台的日志信息,按照设置的规定显示
[main] 10:53:18.778 DEBUG o.a.i.t.jdbc.JdbcTransaction - Opening JDBC Connection
[main] 10:53:19.019 DEBUG o.a.i.d.pooled.PooledDataSource - Created connection 1616974404
MyBatis二级缓存
一级缓存特点:一级缓存默认开启,缓存范围仅限于一个SqlSession会话,即一个session对象,范围太小,声明周期短。两个session对象查询后,数据存储在不同的内存地址中。而且,commit提交后会强制清空namespace缓存,session对象缓存的查询数据就没了。为了合理提高缓存命中率,提高查询速度,使用二级缓存。
二级缓存特点:二级缓存的范围大,属于范围Mapper Namespace,周期长。需要设置catch标签。在一个namespace空间内,多个session对象执行同一个id的查询,查询后的数据放在一个缓存地址中。第一次从硬盘的数据库中查询数据,后面再次查询不再执行sql语句,直接从缓存中提取数据,速度更快。
一级缓存和二级缓存范围对比图:
二级缓存使用方法
例1,在goods命名空间内实现二级缓存,需要在空间内使用cache标签,它有四个设置项
<!--eviction是缓存策略,flushInterval是间隔时间,size是二级缓存大小-->
<cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/>
二级缓存的参数说明
<!--cache标签中
eviction是缓存的清除策略,当缓存对象数量达到上限后,自动触发对应算法对缓存对象清除
1.LRU – 最近最少使用的:移除最长时间不被使用的对象。
O1 O2 O3 O4 .. O512
14 99 83 1 893
2.FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
3.SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
4.WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval 代表间隔多长时间自动清空缓存,单位毫秒,600000毫秒 = 10分钟
size 缓存存储上限,用于保存对象或集合(1个集合里面有很多数据也算1个对象)的数量上限
readOnly 设置为true ,代表返回只读缓存,每次从缓存取出的是缓存对象本身.这种执行效率较高
设置为false , 代表每次取出的是缓存对象的"副本",每一次取出的对象都是不同的,这种安全性较高
-->
使用规则:
- 二级开启后默认所有查询操作均使用缓存
- 写操作commit提交时对该namespace缓存强制清空
- 配置useCache=false可以不用缓存
- 配置flushCache=true代表强制清空缓存
例2,不使用缓存的情况,不建议把包含很多的list集合保存到缓存中
<!-- useCache="false"代表不使用缓存 -->
<!-- 不建议把包含很多的list集合保存到缓存中,缓存命中率低 设置useCache="false"不使用缓存-->
<select id="selectAll" resultType="com.imooc.mybatis.entity.Goods" useCache="false">
select * from t_goods order by goods_id desc limit 10
</select>
例3,测试一级缓存
xml文件中的select标签为
<!-- 单参数传递,使用parameterType指定参数的数据类型即可,SQL中#{value}提取参数-->
<select id="selectById" parameterType="Integer" resultType="com.imooc.mybatis.entity.Goods">
select * from t_goods where goods_id = #{value }
</select>
两个session对象的goods对象数据地址空间不一样,对象的数据随着session存在,session销毁,对象数据就没了,缓存利用率低
try {
session = MyBatisUtils.openSession();
Goods goods = session.selectOne("goods.selectById", 1603);
Goods goods1 = session.selectOne("goods.selectById", 1603);
//一级缓存中,一个session对象的数据存在一个地址中,goods和goods1的地址空间一样
//sql语句也执行一次
System.out.println(goods.hashCode() + ":" + goods1.hashCode());
...
}
try {
session = MyBatisUtils.openSession();
Goods goods = session.selectOne("goods.selectById", 1603);
session.commit();
//commit提交时对该namespace缓存强制清空,清空后,上一个session的查询数据就没了。
//再次执行同样的操作时,会重新执行sql语句,从硬盘的数据库中查询数据,所以goods和goods1的地址空间不同
Goods goods1 = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode() + ":" + goods1.hashCode());
...
}
运行结果
447718425:447718425
例4、测试二级缓存
使用二级缓存后,两个session对象的goods对象数据地址空间一样,同样的sql语句只执行一次,缓存命中率为0.5,越高越好。
二级缓存把对象存储到命名空间级别上,不会随着session的打开和关闭销毁
try {
session = MyBatisUtils.openSession();
Goods goods = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
}
try {
session = MyBatisUtils.openSession();
Goods goods = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
}
运行结果为:
447718425
447718425
动态SQL筛选实现
动态SQL:根据参数的数据动态组织SQL的技术。应用场景:比如淘宝的筛选商品功能
实现方法:使用if标签和where标签。
例1:查询t_goods表格中,category_id是44,当前价格小于500的商品
<!-- 动态SQL实现,使用if标签和where标签实现,if标签中的test判断map中的某个key是否存在,存在就and添加一个条件 -->
<!-- where标签用<>包括起来,不是单纯的sql中的where,可以避免多个条件and连接问题 -->
<!-- 多个筛选条件有多个参数,使用Map接口实现,使用parameterType指定Map接口,SQL中#{key}提取参数 -->
<select id="dynamicSQL" parameterType="java.util.Map" resultType="com.imooc.mybatis.entity.Goods">
select * from t_goods
<where>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="currentPrice != null">
and current_price < #{currentPrice}
</if>
</where>
</select>
查询t_goods表中,category_id是44,当前价格小于500的商品,测试类中的java代码为:
session = MyBatisUtils.openSession();
//map用来保存筛选条件
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());
}
运行结果为t_goods表中满足条件的商品标题、商品目录id,商品价格
爱恩幼 孕妇护肤品润养颜睡眠面膜 100g: 44: 49.0
【欧洲直邮】德国Hipp喜宝有机奶粉pre段 600g*2: 44: 208.0
小米 Yeelight床头灯 白色: 44: 249.0
对象关联查询,由一个对象查询另外对象的数据
一对多查询
例子,查询一个商品对应的多个detail详细信息
goods_detail.xml文件中配置
<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>
新建一个GoodsDetail类
/**
* 描述t_goods_detail数据表格对应的属性
*/
public class GoodsDetail {
private Integer gdId;
private Integer goodsId;
private String gdPicUrl;
private Integer gdOrder;
public Integer getGdId() { return gdId; }
public void setGdId(Integer gdId) { this.gdId = gdId; }
...
goods类中也要配置GoodsDetail属性,用来保存sql语句执行后,返回的一个商品对应的多个detail信息
private List<GoodsDetail> goodsDetails;
//以及get和set方法
在goods.xml文件中添加对象关联查询
<!--
resultMap可用于说明一对多或者多对一的映射逻辑
id 是resultMap属性引用的标志
type 指向One的实体(Goods)
-->
<resultMap id="rmGoods1" type="com.imooc.mybatis.entity.Goods">
<!-- 映射goods对象的主键到goods_id字段,其他字段符合驼峰命名装换规则,不需要再说明 -->
<id column="goods_id" property="goodsId"></id>
<!--
collection的含义是,在
select * from t_goods limit 0,1 得到结果后,对所有Goods对象遍历得到goods_id字段值,
并代入到goodsDetail命名空间的selectByGoodsId的SQL中执行查询,
将得到的"商品详情"集合赋值给goodsDetails List对象.
-->
<collection property="goodsDetails" select="goodsDetail.selectByGoodsId"
column="goods_id"/>
</resultMap>
<select id="selectOneToMany" resultMap="rmGoods1">
select * from t_goods limit 0,10
</select>
collection标签里面使用select,跳转到了goods_detail.xml的sql语句,并且对所有Goods对象遍历得到goods_id字段值,并代入到goodsDetail命名空间的selectByGoodsId的SQL中执行查询
调用代码,一对多对象关联查询
...
session = MyBatisUtils.openSession();
List<Goods> list = session.selectList("goods.selectOneToMany");
for(Goods goods:list) {
//输出商品的标题和商品对应详细信息的数量,一个商品只有一个标题,但是有多条商品描述信息
System.out.println(goods.getTitle() + ":" + goods.getGoodsDetails().size());
}
...
返回结果
爱恩幼 孕妇护肤品润养颜睡眠面膜 100g 该商品对应的详细信息数量:11
亲润 孕妇专用遮瑕保湿隔离提亮肤色气垫CC霜 该商品对应的详细信息数量:12
...
多对一关联查询
在GoodsDetail类中添加一个Goods属性,保存多个detail对应的一个商品对象
private Goods goods;
//以及set、get方法
在goods_detail.xml文件中配置结果映射,描述多对一的映射关系
<mapper namespace="goodsDetail">
<resultMap id="rmGoodsDetail" type="com.imooc.mybatis.entity.GoodsDetail">
<id column="gd_id" property="gdId"/>
<result column="goods_id" property="goodsId"/>
<association property="goods" select="goods.selectById" column="goods_id">
</association>
<!--通过association的select跳转到goods.selectById,并且把前20条商品信息的goods_id带入goods.selectById进行SQL查询,结果返回给goods属性-->
</resultMap>
<select id="selectManyToOne" resultMap="rmGoodsDetail">
select * from t_goods_detail limit 0,20
</select>
</mapper>
在goods.xml文件配置要跳转到的sql语句
<mapper namespace="goods">
<!-- 单参数传递,使用parameterType指定参数的数据类型即可,SQL中#{value}提取参数-->
<select id="selectById" parameterType="Integer" resultType="com.imooc.mybatis.entity.Goods">
select * from t_goods where goods_id = #{value }
</select>
</mapper>
多对一关联时使用association标签。多对一的执行顺序是:Java代码调用goodsDetail空间下的selectManyToOne语句,返回goods_detail的前20条商品信息,通过association的select跳转到goods.selectById,并且把前20条商品信息的goods_id带入goods.selectById进行SQL查询,结果返回给goods属性
创建测试类和方法,测试多对一对象关联映射
...
session = MyBatisUtils.openSession();
List<GoodsDetail> list = session.selectList("goodsDetail.selectManyToOne");
for(GoodsDetail gd:list) {
//输出t_goods_detail表格的图片地址和对应商品的标题,多个图片描述对应一个商品标题
System.out.println(gd.getGdPicUrl() + ":" + gd.getGoods().getTitle());
}
...
输出结果
pageHelper使用
添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>2.0</version>
</dependency>
mybatis-config.xml增加Plugin配置
<!--启用Pagehelper分页插件-->
<plugins>
<!-- 配置拦截器插件,新版拦截器是 com.github.pagehelper.PageInterceptor-->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!--设置数据库类型-->
<property name="helperDialect" value="mysql"/>
<!--分页合理化-->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
编写sql语句
<!-- 分页查询-->
<select id="selectPage" resultType="com.imooc.mybatis.entity.Goods">
select * from t_goods where current_price < 1000
</select>
PageHelper分页查询,使用PageHelper.startPage()自动分页
...
session = MyBatisUtils.openSession();
/*startPage方法会自动将下一次查询进行分页*/
//startPage(2,10),每页10条数据,查询第2页数据
PageHelper.startPage(2,10);
//分页查询session.selectList,返回结果是page对象
Page<Goods> page = (Page) session.selectList("goods.selectPage");
System.out.println("总页数:" + page.getPages());
System.out.println("总记录数:" + page.getTotal());
System.out.println("开始行号:" + page.getStartRow());
System.out.println("结束行号:" + page.getEndRow());
System.out.println("当前页码:" + page.getPageNum());
List<Goods> data = page.getResult();//当前页数据
for (Goods g : data) {
System.out.println(g.getTitle());
}
System.out.println("");
...
运行结果
总页数:181
总记录数:1808
开始行号:10
结束行号:20
当前页码:2
康泰 家用智能胎心仪 分体探头操作方便 外放聆听 与家人分享宝宝心声
惠氏 启赋(Wyeth illuma)有机1段 900g (0-6月)婴儿配方奶粉(罐装)
...
MyBatis整合C3P0连接池
虽然mybatis自带的有,但是我们用其他更强大的数据库连接池,比如C3P0,等等,整合步骤为:
配置依赖
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>
新建一个datasource目录,用来保存创建数据源类C3P0DataSourceFactory,数据源c3p0化
/**
* C3P0与MyBatis兼容使用的数据源工厂类
* 继承数据源工厂UnpooledDataSourceFactory,在构造函数中,dataSource属性由对应的连接池创建
*/
public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
public C3P0DataSourceFactory(){
this.dataSource = new ComboPooledDataSource();
}
}
在mybatis-config.xml中修改数据连接池,property的name值和Mybatis自带的不同,注意区分
<!--采用连接池方式管理数据库连接-->
<!--<dataSource type="com.imooc.mybatis...">-->
<dataSource type="com.imooc.mybatis.datasource.C3P0DataSourceFactory">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/babytun?useUnicode=true&characterEncoding=UTF-8"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
<property name="initialPoolSize" value="5"/>
<property name="maxPoolSize" value="20"/>
<property name="minPoolSize" value="5"/>
</dataSource>
在测试类中运行程序后,在控制台的日志模式下,可以看到有有mchange,c3p0字样,说明整合成功
[MLog-Init-Reporter] 16:49:33.350 DEBUG com.mchange.v2.log.MLog - Reading VM config for path list /com/mchange/v2/log/default-mchange-log.properties, /mchange-commons.properties, /c3p0.properties, hocon:/reference,/application,/c3p0,/, /mchange-log.properties, /
[MLog-Init-Reporter] 16:49:33.350 DEBUG com.mchange.v2.log.MLog - The configuration file for resource identifier '/mchange-commons.properties' could not be found. Skipping.
MyBatis批处理
批量增加数据
批量增加数据,把10000条数据作为一个list集合,只需要执行一个sql语句,就可以把数据插入到数据库中。直接新增数据,没增加一条,就会执行一次sql语句,耗时耗力。
批量新增数据,配置xml文件
<!--批量新增数据-,因为有大量数据,所以parameterType是List集合类型。
关键标签时foreach标签,collection值为小写“list”,item是临时变量遍历list中的每一个对象数据,
index是索引,separator是分隔符-->
<!--INSERT INTO table-->
<!--VALUES ("a" , "a1" , "a2"),("b" , "b1" , "b2"),(....)-->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery, category_id)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(#{item.title},#{item.subTitle}, #{item.originalCost}, #{item.currentPrice}, #{item.discount}, #{item.isFreeDelivery}, #{item.categoryId})
</foreach>
</insert>
java代码,批量插入测试的java代码,插入10000条数据。
...
//记录程序开始和结束时间,计算程序运行时间
long st = new Date().getTime();
session = MyBatisUtils.openSession();
List list = new ArrayList();
//生成10000条goods对象数据,添加到list集合中,这样只需要执行一次sql语句
for (int i = 0; i < 10000; i++) {
Goods goods = new Goods();
goods.setTitle("测试商品");
goods.setSubTitle("测试子标题");
goods.setOriginalCost(200f);
goods.setCurrentPrice(100f);
goods.setDiscount(0.5f);
goods.setIsFreeDelivery(1);
goods.setCategoryId(43);
list.add(goods);
}
//insert()方法把list集合添加到t_goods表中
//insert()返回值代表本次成功插入的记录总数
session.insert("goods.batchInsert", list);
session.commit();//提交事务数据
long et = new Date().getTime();
System.out.println("执行时间:" + (et - st) + "毫秒");
...
运行结果为:执行完成后,数据库t_goods表格中新增了10000条测试数据
...
执行时间:5574毫秒
...
批量删除数据
批量删除测试,删除一个范围内的数据。注意和增加数据不同,这里有open="(" 和close=")"代表从开始和结束
<!--in (1901,1902)-->
<delete id="batchDelete" parameterType="java.util.List">
DELETE FROM t_goods WHERE goods_id in
<foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
#{item}
</foreach>
</delete>
java代码,删除1920,1921,1922这3个Id对应的商品信息
...
session = MyBatisUtils.openSession();
List list = new ArrayList();
list.add(1920);
list.add(1921);
list.add(1922);
session.delete("goods.batchDelete", list);
session.commit();//提交事务数据
...
运行结果:
在t_goods表格中,删除了1920,1921,1922这3个Id对应的商品信息
Mybatis注解开发方式
MyBatis注解,把原来在xml文件书写的sql语句放在java程序中,开发更快。适合大型,团队合作项目。xml文件方式适合小型,单独,敏捷开发形式。
使用步骤
例1,查询数据
新建了一个项目,项目结构为:
配置依赖pom.xml
创建mybatis-config.xml,配置驼峰命名转换和数据库
<settings>
<!-- goods_id ==> goodsId 驼峰命名转换 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!--设置默认指向的数据库-->
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
...
工具类MyBatisUtils,实现创建和关闭SqlSession对象的方法
实体类Goods,实现数据库表格字段名和类属性的映射
数据传输对象GoodsDTO,用来做结果映射
新建dao包,创建GoodsDAO接口,使用dao接口和注解sql代替原来的mapper文件
public interface GoodsDAO {
@Select("select * from t_goods where current_price between #{min} and #{max} order by current_price limit 0,#{limt}")
public List<Goods> selectByPriceRange(@Param("min") Float min ,@Param("max") Float max ,@Param("limt") Integer limt);
}
在mybatis-config.xml中,用mapper标签指明接口包的位置,mabatis会扫描整个包下配置的接口和注解,不会遗漏
<mappers>
<!--<mapper class="com.imooc.mybatis.dao.GoodsDAO"/>不便维护-->
<package name="com.imooc.mybatis.dao"/>
</mappers>
java代码,查询商品价格在min,max区间内的前20条数据
...
session = MyBatisUtils.openSession();
//xml文件配置sql使用的是session.selectList("goods.selectByPriceRange", param)
//接口方式使用getMapper,得到映射器,方法内参数是类对象,返回是GoodsDAO
GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
List<Goods> list = goodsDAO.selectByPriceRange(100f, 500f, 20);
for (Goods goods: list) {
System.out.println(goods.getTitle());
}
...
运行结果
测试商品
测试商品
...
例2,新增数据
增加一条数据,使用到两个注解,@Insert("sql语句"),@SelectKey(设置新增数据的主键值)
GoodsDAO接口中的java代码为
...
@Insert("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>
@SelectKey(statement = "select last_insert_id()" , before = false , keyProperty = "goodsId" , resultType = Integer.class)
public int insert(Goods goods);
...
添加一个good对象到数据库t_goods表格中,返回新增数据的goods_id,测试类的java方法为
...
session = MyBatisUtils.openSession();
Goods goods = new Goods();
goods.setTitle("测试商品");
goods.setSubTitle("测试子标题");
goods.setOriginalCost(200f);
goods.setCurrentPrice(100f);
goods.setDiscount(0.5f);
goods.setIsFreeDelivery(1);
goods.setCategoryId(43);
GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
//insert()方法返回值代表本次成功插入的记录总数
int num = goodsDAO.insert(goods);
session.commit();//提交事务数据
System.out.println(goods.getGoodsId());
...
运行结果为新增数据的goods_id
35685
例3,结果映射
select * from t_goods,把t_goods表格查询结果映射为GoodsDTO对象。
结果映射用到两个注解,@Select(sql语句),@Results({主键字段与对象属性映射,非主键字段与对象属性映射}),GoodsDAO接口中的代码为
...
@Select("select * from t_goods")
//Results相当于<resultMap>标签
@Results({
//主键字段与对象属性映射,相当于xml中的<id>标签
@Result(column = "goods_id" ,property = "goodsId" , id = true) ,
//<result>标签和在xml文件中一样,设置非主键字段与属性映射
@Result(column = "title" ,property = "title"),
@Result(column = "current_price" ,property = "currentPrice")
})
public List<GoodsDTO> selectAll();
...
测试类的调用方法为:goodsDAO.selectAll();返回的结果是GoodsDTO对象
...
session = MyBatisUtils.openSession();
GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class);
List<GoodsDTO> list = goodsDAO.selectAll();
System.out.println(list.size());
...
运行结果为:list集合的数据条数
34937