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