Blog of CodeJELLOW’S ENGINEERING NOTES
← 返回文章

MyBatis学习总结(八)

MyBatis学习总结(八) 整合Log4j、延迟加载、查询缓存

整合Log4j

通过日志信息,可以阅读mybatis执行情况,观察mybatis实际执行sql语句以及SQL中的参数和返回结果。

常见日志有这些:SLF4J 、Apache Commons Logging、Log4j2、Log4j 、JDK logging。这里我们使用Log4j。

第一步是导入jar包。mybatis.zip文件中的lib目录下有log4j的jar包,如果没有的可以自行去下载。

第二步是在conf中开启日志。在settings标签中开启:

TEXT
 <settings>
<!--        开启日志,并指定使用的日志-->
        <setting name="logImpl" value="LOG4J"/>
    </settings>

value属性指定了使用的日志类型,如果不指定,MyBatis就会按照以下的顺序去寻找:

TEXT
SLF4J →Apache Commons Logging→Log4j2→Log4j → JDK logging

尤其要注意name属性的内容严格区分大小写。

第三步编写配置日志输出文件log4j.properties

TEXT
log4j.rootLogger=DEBUG, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

第一行表示日志输出debug类型的信息,且输出到标准输出流中,日志级别DEBUG< INFO<WARN<ERROR;第二行表明日志显示的方式是追加显示到控制台中;第三行表示日志显示的样式,可以是平铺的也可以是竖列的;最后一行表示日志信息的输出格式。后面三行不重要。

log4j.properties文件名必须和使用的日志的jar包类型一致,因为在setting中指定了value之后MyBatis会自动寻找这个log4j.properties文件。

延迟加载

延迟加载是指在一对一、一对多、多对一、多对多等设计多个表的查询时,首先查询指定的表的结果,延时查询所关联的表的结果。只有当需要用到关联表的结果时(在java中使用到了所关联表的实体类对象),MyBatis才会根据指定查询的表的外键去查询所关联表的结果。意思是原本查询结果涉及两个表,需要用一个sql语句执行完,查询的范围也是两张表的所有元组,现在延时加载后将一个sql语句分为两个分别查询两张表的语句,首先只执行一个,当需要用到第二个表的结果时再去查询第二个。

首先需要开启延迟加载。延迟加载需要手动开启,在conf.xml文件中要先关闭立即加载,再开启延迟加载:

TEXT
<settings>
...
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

先以一对一为例

在Mapper文件中,原来的查询是这样的:

TEXT
<!--    使用MyBatis提供的resultMap来实现一对一-->    <select id="queryStudentWithHashMapForFk" parameterType="int" resultMap="person_idCard_map">        select  p.*,c.*        from person p inner join personidcard c on p.cardid =c.cardid        where p.id=#{id}    </select>        <resultMap id="person_idCard_map" type="Person">        <id property="id" column="id"/>        <result property="name" column="name"/>        <result property="age" column="age"/><!--        对象成员时,如果是一对一映射,就用association;javaType指定改属性的类型-->        <association property="personIdCard" javaType="PersonIdCard">            <id property="cardid" column="cardid"/>            <result property="cardinfo" column="cardinfo"/>        </association>    </resultMap>

延迟加载的语句如下:

TEXT
 <!--    利用延时加载来实现一对一-->
    <select id="queryStudentWithLazyloading" resultMap="person_idCard_lazyload_map">
        select * from person
    </select>

    <resultMap id="person_idCard_lazyload_map" type="Person">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <!-- 这里采用延迟加载:在查询人时,并不立即加载身份证-->
        <association property="personIdCard" javaType="PersonIdCard" select="shopkeeper.mapper.personIdCardMapper.queryCardById" column="cardid">
        </association>
    </resultMap>

改变的地方有两处,一是sql语句,现在只需要先查询person表的信息了;二是在resultMap中的association标签,personIdCard这张表并不是直接写出要查询的元素,而是多了select和column两个属性:select表示延时加载时执行的查询语句在mapper文件中的id值,column是这两张表的关联,即外键,作为参数传入select中的查询语句里(column写入的是select标签中查询的表的外键,这里的cardid在两张表中名字是相同的,在一对多的例子中就不相同了)。select引用的语句如下:

TEXT
<select id="queryCardById" parameterType="int" resultType="personIdCard">   select * from personidcard where cardid =#{cardid}</select>

也就是说,一开始我们只得到了person表的查询结果,只有当我们要用到personIdCard的结果时,MyBatis才会动用延时加载来根据外键来找到personIdCard的结果。

可以发现的时select中的语句在另外一个Mapper中,其实写在一个Mapper里也是可以的,但是要遵循一个实体类对应一个Mapper文件的开发习惯。新建了一个mapper文件后一定要记得去conf.xml文件中建立映射。

在测试类中检测延时加载的效果:

TEXT
public static void queryStudentWithLazyloading() throws IOException {    Reader reader=Resources.getResourceAsReader("conf.xml");    SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(reader);//可以通过build的第二参数指定数据库环境    SqlSession sqlSession=sqlSessionFactory.openSession();    personMapper personMapper =sqlSession.getMapper(personMapper.class);    List<Person> pers=personMapper.queryStudentWithLazyloading();    for (Person person:pers)    {        System.out.println(person.getId()+", "+person.getName());    }    for (Person person:pers)    {        PersonIdCard card=person.getPersonIdCard();        System.out.println(card.getCardid()+", "+card.getCardinfo());    }    sqlSession.close();    reader.close();}

我是先得到了所有的person表的查询结果,再根据每一个person来获取其idcard的信息。日志输出如下:

TEXT
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 359922172.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1573f9fc]
DEBUG [main] - ==>  Preparing: select * from person
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 4
1, 刘茜元
2, 陈曦
3, 刘闹闹
13, 刘咯咯
DEBUG [main] - ==>  Preparing: select * from personidcard where cardid =?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
1, 刘茜元 info
DEBUG [main] - ==>  Preparing: select * from personidcard where cardid =?
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
2, 陈曦 info
DEBUG [main] - ==>  Preparing: select * from personidcard where cardid =?
DEBUG [main] - ==> Parameters: 3(Integer)
DEBUG [main] - <==      Total: 1
3, 刘闹闹 info
DEBUG [main] - ==>  Preparing: select * from personidcard where cardid =?
DEBUG [main] - ==> Parameters: 13(Integer)
DEBUG [main] - <==      Total: 1
13, 刘咯咯 info
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1573f9fc]
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1573f9fc]
DEBUG [main] - Returned connection 359922172 to pool.

可以很明显地看出查询身份证的操作分了多次进行,且完全在查询人的操作之后。

一对多

和一对一的延迟加载配置方法相同。

在personClassMapper中建立延时加载的查询:

TEXT
<select id="queryClassAndPersonsWithLazyloading" resultMap="class_person_Lazyload_map">    select c.* from personclass c</select><resultMap id="class_person_Lazyload_map" type="personclass">    <id property="classid" column="classid"/>    <result property="classname" column="classname"/>    <collection property="persons" ofType="person" column="classid" select="shopkeeper.mapper.personMapper.queryPersonByClassId" >    </collection></resultMap>

select中引用的查询语句:

TEXT
<select id="queryPersonByClassId" parameterType="int" resultType="person">    select * from person where class= #{class}</select>

这里可以发现column中传入的是外键名,classid在personClass表中的名称是classid,但是在person表中的名称只是class。原因是在resultMap中传入参数给延时加载的sql的。

测试类:

TEXT
public static void queryClassAndPersonsWithLazyloading() throws IOException {
    Reader reader=Resources.getResourceAsReader("conf.xml");
    SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(reader);//可以通过build的第二参数指定数据库环境
    SqlSession sqlSession=sqlSessionFactory.openSession();

    personClassMapper personClassMapper =sqlSession.getMapper(personClassMapper.class);


    List<PersonClass> classes=personClassMapper.queryClassAndPersonsWithLazyloading();

    for (PersonClass personClass:classes)
    {
        System.out.println(personClass.getClassid()+", "+personClass.getClassname());
    }

    for (PersonClass personClass:classes)
    {

        List<Person> persons=personClass.getPersons();

        System.out.println(persons);
    }

    sqlSession.close();
    reader.close();
}

日志输出和一对一的差不多。但是这里是使用了personClassMapper中的查询语句,所以要先实现一个接口。

查询缓存

一级缓存

作用范围是同一个sqlsession对象。

MyBatis默认开启一级缓存,当某个查询第一次执行时,会从DB中查询,结果缓存在sqlsession中。之后这一个sqlsession执行同一个语句的所有查询都会直接去sqlSession中取出结果(不会重新去数据库中再进行一次查询)。

每当进行一次commit之后sqlSession就会自动清空所有缓存的结果。

二级缓存

MyBatis自带二级缓存,也有第三方提供的二级缓存。

Mybatis自带二级缓存, 作用范围是同一个namespace生成的mapper对象。默认不打开,需要手动打开:

第一步在conf中开启:

TEXT
<settings>
...
<!--        开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

二级缓存就算打开了,也不是所有的Mapper都会使用;如果要使用二级缓存,还需要再Mapper文件中声明开启二级缓存,实现方式是加一个<cache/>标签。

此时如果直接运行测试类,会报以下错错误:

TEXT
Exception in thread "main" org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: shopkeeper.entity.Person

说明二级缓存是在硬盘中进行的,准备缓存的对象必须实现了序列化接口。需要序列化的对象就是开启了二级缓存的mapper对应的对象(personMapper->person)。这同时也说明了为什么二级缓存的使用需要在每一个Mapper文件中要手动添加一个cache标签:向磁盘io操作很占用系统资源且实体类一般在创建时也不会实现Serializable接口。

此外由可序列化的知识可知:级联和父类都要序列化。

namespace的值就是接口的全类名(包名.类名),通过接口可以产生代理对象(personMapper等);而namespace决定了studentMapper对象的产生所以只要产生的Mapper对象来自于同一个namespace,则这些对象共享二级缓存。

如果是同一个SqlSession对象进行多次相同的查询,则直接进入一级缓存查询;当一个SqlSession被close时,就会触发将 一级缓存的内容存入到二级缓存中(貌似commit也会触发,存疑);如果不是同一个SqlSession对象进行多次相同的查询(但是均来自同一个namespace),则进入二级缓存查询。

测试类方法:

TEXT
public static void queryClassAndPersonsWithLazyloading() throws IOException {    Reader reader=Resources.getResourceAsReader("conf.xml");    SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(reader);//可以通过build的第二参数指定数据库环境    SqlSession sqlSession=sqlSessionFactory.openSession();    personClassMapper personClassMapper =sqlSession.getMapper(personClassMapper.class);    List<PersonClass> classes=personClassMapper.queryClassAndPersonsWithLazyloading();    for (PersonClass personClass:classes)    {        System.out.println(personClass.getClassid()+", "+personClass.getClassname());    }    for (PersonClass personClass:classes)    {        List<Person> persons=personClass.getPersons();        System.out.println(persons);    }    sqlSession.close();    SqlSession sqlSession2=sqlSessionFactory.openSession();    personClassMapper personClassMapper2 =sqlSession2.getMapper(personClassMapper.class);    List<PersonClass> classes2=personClassMapper2.queryClassAndPersonsWithLazyloading();    for (PersonClass personClass:classes2)    {        System.out.println(personClass.getClassid()+", "+personClass.getClassname());    }    for (PersonClass personClass:classes2)    {        List<Person> persons=personClass.getPersons();        System.out.println(persons);    }    sqlSession2.close();    reader.close();}

运行测试类的日志如下:

TEXT
DEBUG [main] - ==>  Preparing: select c.* from personclass c
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 2
2, c2
6, c6
DEBUG [main] - Cache Hit Ratio [shopkeeper.mapper.personMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from person where class= ?
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
[Person{age=20, name='陈曦', id=2}]
DEBUG [main] - Cache Hit Ratio [shopkeeper.mapper.personMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from person where class= ?
DEBUG [main] - ==> Parameters: 6(Integer)
DEBUG [main] - <==      Total: 3
[Person{age=19, name='刘茜元', id=1}, Person{age=19, name='刘闹闹', id=3}, Person{age=19, name='刘咯咯', id=13}]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3eb738bb]
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3eb738bb]
DEBUG [main] - Returned connection 1052195003 to pool.
DEBUG [main] - Cache Hit Ratio [shopkeeper.mapper.personClassMapper]: 0.5
2, c2
6, c6
[Person{age=20, name='陈曦', id=2}]
[Person{age=19, name='刘茜元', id=1}, Person{age=19, name='刘闹闹', id=3}, Person{age=19, name='刘咯咯', id=13}]

可以看见当第一次查询时,命中率为0,第二次查询时命中率为0.5,说明第二次用到时二级缓存中的结果。

通过 RSS 订阅更新 →