MyBatis

MyBatis学习总结(八)

整合Log4j

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

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

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

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

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

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

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

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

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

log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.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文件中要先关闭立即加载,再开启延迟加载:

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

先以一对一为例

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

<!--    使用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>

延迟加载的语句如下:

 <!--    利用延时加载来实现一对一-->
    <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引用的语句如下:

<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文件中建立映射。

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

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的信息。日志输出如下:

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中建立延时加载的查询:

<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中引用的查询语句:

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

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

测试类:

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中开启:

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

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

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

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),则进入二级缓存查询。

测试类方法:

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();
}

运行测试类的日志如下:

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,说明第二次用到时二级缓存中的结果。

发表评论