深入学习Mybatis-Mybatis高级篇


1. 高级映射以及延迟加载

1.1 多对一映射

注意:谁在前谁就是主表

Student.java

public class Student {//多的一方
    private String id;
    private String name;
    private Teacher tutor;
}

Teacher.java

public class Teacher {//一的一方
    private String id;
    private String name;
    private Set<Student> students;
}

级联属性映射

<resultMap id="StudentResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Student">
    <id property="id" column="s_id"/>
    <result property="name" column="s_name"/>
    <result property="tutor.id" column="c_id"/>
</resultMap>
<select id="selectById" resultMap = "StudentResult">
    select s.s_id,s.s_name,t.c_id
    from s left join t on s.c_id = t.c_id
    where s.s_id=#{sid}
</select>

association

<resultMap id="StudentResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Student">
    <id property="id" column="s_id"/>
    <result property="name" column="s_name"/>
    <!--一个stu关联一个教师-->
    <!--property:映射的pojo类的属性名-->
    <!--javaType:指定要映射的java类型-->
    <association property="tutor" javaType="cn.edu.scau.cmi.zhangtingjie.orm.domain.Teacher">
        <id property="id" column="t_id"/>
        <result property="name" column="t_name"/>
    </association>
</resultMap>
<select id="findStudent" parameterType="java.lang.String" resultMap="StudentResult">
    select student.id as s_id,
           student.name as s_name,
    	   teacher.id as t_id,
           teacher.name as t_name
    from   student left join teacher on student.tutor = teacher.id
    where student.id = #{id};
</select>

分步查询

优点:可复用,支持懒加载

问题在于,怎么关联呢?

<select id="findStudentStep1" resultMap="StudentResult">
    select student.id as s_id,
           student.name as s_name,
           student.tutor as tutor
    from orm.student
    where student.id = #{id}
</select>
<select id="findTeacher" resultType="cn.edu.scau.cmi.zhangtingjie.orm.domain.Teacher">
    select * from orm.teacher where id = #{id};
</select>
  • 关联的办法
<resultMap id="StudentResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Student">
    <id property="id" column="s_id"/>
    <result property="name" column="s_name"/>
    <!--一个stu关联一个教师-->
    <!--select中指定你这个关联是从哪个来的-->
    <association property="tutor" select="cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO.findTeacher" column="tutor">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </association>
</resultMap>

其中column是你要传进去的参数

延迟加载

延迟加载的核心原理是:用到的时候再执行查询,不用的时候不查询,为了提高性能

<association property="tutor" fetchType="lazy">
    <!--开启延迟加载,用到的时候再进行查询-->
</association>

它是局部的设置,只对当前的association中的关联的sql语句有用

可以改成全局的,但凡有关联对象的,都可以使用延迟加载的形式

<settings>
    <setting name="logImpl" value="SLF4J"/>
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

注意,如果想要有特殊的需求的话,比如说某个association不需要懒加载,那么就将fetchType=eager即可,局部的状态是大于全局的状态的

1.2 一对多的映射原理

public class Teacher {//一个老师对应多个学生
    private String id;
    private String name;
    private Set<Student> students;
}
public class Student {//一个学生对应一个老师
    private String id;
    private String name;
    private Teacher tutor;
}

collection

<select id = "selectByCollection" resultMap="TeacherResult">
    select teacher.id as id,
           teacher.name as name,
           s.id as s_id,
           s.name as s_name
    from orm.teacher left join student s on teacher.id = s.tutor
    where teacher.id = #{id}
</select>
<resultMap id="TeacherResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Teacher">
    <result property="name" column="name"/>
    <result property="id" column="id"/>
    <!--ofType:指明你集合中的每一个元素的类型-->
    <collection property="students" ofType="cn.edu.scau.cmi.zhangtingjie.orm.domain.Student">
        <id property="id" column="s_id"/>
        <result property="name" column="s_name"/>
    </collection>
</resultMap>

分步查询

<resultMap id="TeacherResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Teacher">
    <result property="name" column="name"/>
    <result property="id" column="id"/>
    <!--ofType:指明你集合中的每一个元素的类型-->
    <collection property="students" select="cn.edu.scau.cmi.zhangtingjie.orm.dao.StudentDAO.findStudentStep1" column="sid">
        <id property= "id"/>
        <result property="name"/>
    </collection>
</resultMap>
<resultMap id="TeacherResult" type="cn.edu.scau.cmi.zhangtingjie.orm.domain.Teacher">
    <result property="name" column="name"/>
    <result property="id" column="id"/>
    <!--ofType:指明你集合中的每一个元素的类型-->
    <collection property="students" select="cn.edu.scau.cmi.zhangtingjie.orm.dao.StudentDAO.findStudentStep1" column="sid">
        <id property= "id"/>
        <result property="name"/>
    </collection>
</resultMap>

同样的,此处也可以开启延迟加载,开启的方法如上

2. Mybatis缓存

2.1 对缓存的理解

执行DQL语句的时候,将查询结果放到缓存当中,如果下一次还是执行完全相同的DQL语句,直接从缓存中取数据即可,不查询数据库了I/O,提高执行效率,避免I/O,其核心的目的还是减少I/O

既然是缓存,那么必须要解决缓存一致性的问题。其解决思路还是清空缓存,当执行了DML的时候,就会将缓存给清除,避免数据的不一致性。

mybatis缓存包括:

  • 一级缓存:将查询到的数据存储到SqlSession中,针对于一次会话
  • 二级缓存:将查询到的数据存储到SqlSessionFactory,代表的是整个数据库
  • 或者集成第三方的缓存

缓存只针对于DQL语句,也就是说缓存机制只对应于select语句

2.2 一级缓存

一级缓存是默认开启的,其作用域是会话域,不需要做任何配置,只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存

什么时候不走缓存?

SqlSession不是同一个,不走缓存,Sql语句不一致,不走缓存

一级缓存什么时候会失效?

第一次DQL和第二次DQL之间有如下的情况发生,就发生缓存的清空:

  • 执行SqlSession的clearCache()方法,手动清除缓存
  • 执行了Insert delete update语句,无论是操作哪张表,都会清空缓存

一级缓存是如何执行的?

首先对于每一个SqlSession对象,其内部有一个Executor对象,这个对象的功能是操作LocalCache,这个LocalCache就是缓存的实体数据,Executor对象就通过 写入查询两种指令来对Cache进行操作

注意:一级缓存是可能存在脏读的问题,比如说一个会话从始至终都没有执行DML,因此无论如何都不会清空缓存,而另外一个会话在不断修改数据,这时候就会引发脏读的问题了。

比如说,看下面的执行代码

@Test
public void testCache(){
    SqlSessionFactory build = new SqlSessionFactoryBuilder().build(getClass().getClassLoader().getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession1 = build.openSession();
    TeacherDAO mapper1 = sqlSession1.getMapper(TeacherDAO.class);
    SqlSession sqlSession2 = build.openSession();
    TeacherDAO mapper2 = sqlSession2.getMapper(TeacherDAO.class);
    for(int i = 0;i<5;i++){
        //sql2不断执行更新
        if(i%2 ==0){
            mapper2.updateTeacher(new Teacher("7","刘二"));
        }else{
            mapper2.updateTeacher(new Teacher("7","刘三"));
        }
        sqlSession2.commit();
        //sql1不断执行查询
        Teacher teacher = mapper1.findTeacher("7");
        System.out.println(teacher);
    }
    sqlSession1.close();
    sqlSession2.close();
}

执行结果是

[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==>  Preparing: update orm.teacher set name = ? where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==> Parameters: 刘二(String), 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - <==    Updates: 1
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO - Cache Hit Ratio [cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO]: 0.0
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
[main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 548554586.
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20b2475a]
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.findTeacher - ==>  Preparing: select * from orm.teacher where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.findTeacher - ==> Parameters: 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.findTeacher - <==      Total: 1
Teacher{id='7', name='刘二'}
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==>  Preparing: update orm.teacher set name = ? where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==> Parameters: 刘三(String), 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - <==    Updates: 1
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO - Cache Hit Ratio [cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO]: 0.0
Teacher{id='7', name='刘二'}
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==>  Preparing: update orm.teacher set name = ? where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==> Parameters: 刘二(String), 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - <==    Updates: 1
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO - Cache Hit Ratio [cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO]: 0.0
Teacher{id='7', name='刘二'}
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==>  Preparing: update orm.teacher set name = ? where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==> Parameters: 刘三(String), 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - <==    Updates: 1
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO - Cache Hit Ratio [cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO]: 0.0
Teacher{id='7', name='刘二'}
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==>  Preparing: update orm.teacher set name = ? where id = ?;
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - ==> Parameters: 刘二(String), 7(String)
[main] DEBUG c.e.s.c.z.orm.dao.TeacherDAO.updateTeacher - <==    Updates: 1
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO - Cache Hit Ratio [cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO]: 0.0
Teacher{id='7', name='刘二'}
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20b2475a]
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20b2475a]
[main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 548554586 to pool.
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3ad83a66]
[main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 987249254 to pool.

我们发现每次的查询都是刘二,这样就引发了问题了

2.3 二级缓存

二级缓存的范围是SqlSessionFacory,使用二级缓存需要:

  • <setting name = "cacheEnabled" value = "true">全局性的开启或者关闭所有映射器配置文件中已配置的任何缓存,默认就是true,不需要配置
  • 在需要使用二级缓存的SqlMapper.xml文件中添加配置<cache/>
  • 使用二级缓存的实体类对象必须是可序列化的,必须实现java.io.Serialiable接口
  • SqlSession对象关闭或者提交之后,一级缓存中的数据才会被写入到二级缓存中,此时二级缓存才是可用的
<mapper namespace="cn.edu.scau.cmi.zhangtingjie.orm.dao.TeacherDAO">
    <cache/>
</mapper>

它实际上是对一级中的Executor做了一次装饰,变成了CachingExecutor

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库

二级缓存的相关配置

eviction淘汰算法

  • LRU:最近最少使用算法,优先淘汰在间隔时间内使用频率最低的对象
  • FIFO:先进先出
  • SOFT:软引用,淘汰软引用指向的对象
  • WEAK:弱引用,淘汰弱引用所指向的对象

flushInterval

  • 二级缓存的刷新时间间隔,单位毫秒,如果没有设置,就代表不刷新缓存,只要内存足够大,一直就会向二级缓存中缓存数据,除非执行了增删改

readOnly

  • true:多条sql语句执行之后返回的对象是同一个,可能存在性能安全的问题
  • false:执行了一次深拷贝,每一次都会返回新的对象

size

  • 设置二级缓存中最多可存储的java对象的数量,默认为1024

3. 分页插件

3.1 分页原理

mysqllimit后面的两个数字

  • startIndex:起始下标,下标从0开始
  • pageSize:每页所显示的记录条数

在分页请求发送方的时候,至少都需要页码每页所需要的记录条数

分页sql

limit 开始的下标,记录条数
#如果使用上前端给的数据,那么
select *
from t_car
limit 0,3;
#从0开始,取3条数据
select *
from t_car
limit pageSize*pageNum,pageNum
#如果只写一个数字:那么就是默认起始下标为0

根据前端的约定不同,需要适当对pageSize进行偏移量的加减

3.2 pageHelper使用

  • 引入坐标
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.1</version>
</dependency>
  • 配置插件
<plugins>
    <!--添加拦截器-->
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
SqlSession sqlSession = MybatisUtils.openSession();
StudentDAO mapper = sqlSession.getMapper(StudentDAO.class);
//在执行之前开启分页功能,pageNum就是页码,pageSize就是页的大小,不需要人为计算了
PageHelper.startPage(2,3);
List<Student> students = mapper.selectAll();
students.forEach(System.out::println);
sqlSession.close();

可以获取分页结束之后的对象

List<Student> students = mapper.selectAll();
//用来封装分页信息的对象
PageInfo<Student> pageInfo = new PageInfo<>(students,2);

Mybatis是如何进行分页的?

逻辑分页:先将数据查询存储到内存中,如何根据条件过滤出想要的记录

物理分页:通过数据库所支持的分页语法进行数据的分页展示,如limit

  • 直接写SQL分页的语句
  • 使用RowBounds一次性加载所有符合查询条件的目标数据,根据分页参数的值在内存中实现分页,在JDBC中,这一步是做了优化的,它不会一次性加载太多的数据到内存中,而是先加载一部分数据,然后根据需求在数据库中进行滚动
  • 通过Interceptor拦截器进行实现,通过通过拦截需要select的语句,然后进行动态拼接,pageHelper就是通过拦截器进行扩展的


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录