MyBatis进阶

扫码查看

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 &lt; #{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 &lt; 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&amp;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
01-19 07:40
查看更多