美文网首页
Mybatis学习笔记(四):映射器介绍(下)

Mybatis学习笔记(四):映射器介绍(下)

作者: 简单一点点 | 来源:发表于2019-01-02 16:46 被阅读1次

    本文档讲述MyBatis映射器较为高级的部分。

    级联

    级联是resultMap中的配置,用来实现一对一或一对多的连表查询。

    级联不是必须的,使用级联的好处是获取关联数据十分便捷,但是级联过多会增加系统的复杂度,降低系统的性能。因此当层级超过3层是,就不再考虑使用级联。

    MyBatis中的级联分为3种:

    • 鉴别器(discriminator):它是一个根据某些条件决定采用具体实现类级联的方案。
    • 一对一(association):一对一的级联。
    • 一对多(collection): 一对多的级联。

    先给出例子。创建3张表:教师表、班级表和学生表。假设一个班只有一个负责老师,但有多个学生。那么班级和老师就是一对一,班级和学生就是一对多。

    CREATE TABLE t_teachers(
     t_id INT PRIMARY KEY AUTO_INCREMENT, 
     t_name VARCHAR(20)
    )ENGINE=INNODB DEFAULT CHARSET=utf8;
    CREATE TABLE t_classes(
     c_id INT PRIMARY KEY AUTO_INCREMENT, 
     c_name VARCHAR(20), 
     teacher_id INT
    )ENGINE=INNODB DEFAULT CHARSET=utf8;
    ALTER TABLE t_class ADD CONSTRAINT fk_teacher_id FOREIGN KEY (teacher_id) REFERENCES t_teacher(t_id);    
    CREATE TABLE t_students(
     s_id INT PRIMARY KEY AUTO_INCREMENT, 
     s_name VARCHAR(20), 
     class_id INT
    )ENGINE=INNODB DEFAULT CHARSET=utf8;
    

    在表里面插入几条数据

    INSERT INTO t_teacher(t_name) VALUES('张老师');
    INSERT INTO t_teacher(t_name) VALUES('蔡老师');
    
    INSERT INTO t_class(c_name, teacher_id) VALUES('318班', 1);
    INSERT INTO t_class(c_name, teacher_id) VALUES('319班', 2);
    INSERT INTO t_students(s_name, class_id) VALUES('张三', 1);
    INSERT INTO t_students(s_name, class_id) VALUES('李四', 1);
    INSERT INTO t_students(s_name, class_id) VALUES('小明', 1);
    INSERT INTO t_students(s_name, class_id) VALUES('王五', 2);
    INSERT INTO t_students(s_name, class_id) VALUES('小红', 2);
    

    创建实体类Teacher,Student和ClassInfo。

    package com.wyk.mybatisDemo.pojo;
    
    public class Teacher {
    
        private int id;
        private String name;
        
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        
        @Override
        public String toString() {
            return "Teacher [id=" + id + ", name=" + name + "]";
        }
    }
    
    package com.wyk.mybatisDemo.pojo;
    
    public class Student {
    
        private int id;
        private String name;
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        
        public String toString() {
            return "Student[id=" + id + ", name=" + name + "]";
        }
    }
    
    package com.wyk.mybatisDemo.pojo;
    
    import java.util.List;
    
    public class ClassInfo {
    
        private int id;
        private String name;
        private Teacher teacher;
        private List<Student> students;
        
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Teacher getTeacher() {
            return teacher;
        }
        public void setTeacher(Teacher teacher) {
            this.teacher = teacher;
        }
        
    
        public List<Student> getStudent() {
            return students;
        }
        public void setStudent(List<Student> students) {
            this.students = students;
        }
        public String toString() {
            return "ClassInfo [id=" + id + ", name=" + name + ", teacher=" + teacher + 
                    ", students=" + students +"]";
        }
    }
    

    可以看到,ClassInfo类中包含一个Teacher对象和一个Student的List。定义查询ClassInfo信息的方法,有2种方式,先创建接口。

    package com.wyk.mybatisDemo.mapper;
    
    import com.wyk.mybatisDemo.pojo.ClassInfo;
    
    public interface SchoolMapper {
    
        public ClassInfo getClassInfo(int id);
        
        public ClassInfo getClassInfo2(int id);
    }
    

    一种方法是通过联表查询获取结果,另一种方法是多次查询。具体方法请看映射器文件。

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wyk.mybatisDemo.mapper.SchoolMapper">
        <resultMap type="com.wyk.mybatisDemo.pojo.ClassInfo" id="classMap">
            <id property="id" column="c_id" />
            <result property="name" column="c_name" />
            <association property="teacher" javaType="com.wyk.mybatisDemo.pojo.Teacher">
                <id property="id" column="t_id" />
                <result property="name" column="t_name" />
            </association>
            <collection property="students" ofType="com.wyk.mybatisDemo.pojo.Student">
                <id property="id" column="s_id" />
                <result property="name" column="s_name" />
            </collection>
        </resultMap>
        <!-- 方法一 联表查询 -->
        <select id="getClassInfo" parameterType="int" resultMap="classMap">
            select c.c_id, c.c_name, t.t_id, t.t_name , s.s_id, s.s_name
            from t_classes c, t_teachers t, t_students s
            where c.teacher_id = t.t_id and c.c_id = s.class_id and c.c_id=#{id} 
        </select>
        <resultMap type="com.wyk.mybatisDemo.pojo.ClassInfo" id="classMap2">
            <id property="id" column="c_id" />
            <result property="name" column="c_name" />
            <association property="teacher" column="teacher_id" select="getTeacher" />
            <collection property="students" ofType="com.wyk.mybatisDemo.pojo.Student"
                column="c_id" select="getStudent"></collection>
        </resultMap>
        <!-- 方法二 多次查询 -->
        <select id="getClassInfo2" parameterType="int" resultMap="classMap2">
            select c_id, c_name, teacher_id from t_classes where c_id=#{id}
        </select>
        <select id="getTeacher" parameterType="int" resultType="com.wyk.mybatisDemo.pojo.Teacher">
            select t_id id, t_name name from t_teachers where t_id=#{id}
        </select>
        <select id="getStudent" parameterType="int" resultType="com.wyk.mybatisDemo.pojo.Student">
            select s_id id, s_name name from t_students where class_id=#{id}
        </select>
    </mapper>
    

    在单元测试中运行一下,查看效果。

    @Test
    public void testGetClassInfo() {
        SqlSession sqlSession = DataConnection.openSqlSession();
        SchoolMapper schoolMapper = sqlSession.getMapper(SchoolMapper.class);
        ClassInfo classInfo = schoolMapper.getClassInfo(1);
        System.out.println(classInfo);
        ClassInfo classInfo2 = schoolMapper.getClassInfo2(1);
        System.out.println(classInfo2);
    }
    
    级联结果.png

    鉴别器的使用在实际中用的较少,就没有写,主要是根据某些条件决定采用哪个类。

    延迟加载

    关注上一小节的方式二,每当查询一个班级信息,都要查询N个学生信息,会造成很大的资源浪费,尤其是我们并不想知道学生信息的时候。

    为了应对上述的N+1问题,MyBatis提供了延迟加载功能。settings配置中有2个元素可以配置级联。

    • lazyLoadingEnabled,延迟加载的全局开关,默认为false。
    • aggressiveLazyLoading, 层级延迟加载开关,处于同一个层级的关联表会同时延迟加载,或者同时被加载。

    使用延迟加载首先要引用对应的jar包cglib。

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>${mybatis.version}</version>
    </dependency>
    
    <settings>
        <setting name="lazyLoadingEnabled" value="true" />
        <setting name="aggressiveLazyLoading" value="true" />
    </settings>
    

    上面的配置属于全局配置,会出现一个问题,同一个层级的级联表也存在需求性的差异,同一级的某个数据库表的数据或许不是我们经常使用的,那么上述的配置也会影响系统的性能。

    fetchType属性可以处理全局定义无法处理的问题,进行自定义,出现在级联元素association和collection中,有2个可选值:

    • eager,获取当前实体后立即加载对应的数据。
    • lazy,获取当前实体后延迟加载对应的数据。
    <collection property="students" ofType="com.wyk.mybatisDemo.pojo.Student" fetchType="eager">
            <id property="id" column="s_id" />
            <result property="name" column="s_name" />
        </collection>
    

    fetchType属性会忽略全局配置项lazyLoadingEnabled和aggressiveLazyLoading。

    说了这么多,感觉最好还是使用上一小节的方法一连接查询,而不是方法二嵌套查询比较好。

    缓存

    MyBatis中拥有一级缓存和二级缓存,其中一级缓存是在SqlSession上的缓存,二级缓存是在SqlSessionFactory上的缓存。

    一级缓存

    MyBatis默认开启一级缓存,而无需进行任何配置。

    还是上面的例子,编写一个查找的测试用例。

    @Test
    public void testFirstLevelCache() {
        SqlSession sqlSession = null;
        try {
            sqlSession = DataConnection.openSqlSession();
            SchoolMapper schoolMapper = sqlSession.getMapper(SchoolMapper.class);
            ClassInfo classInfo1= schoolMapper.getClassInfo2(1);
            logger.info("再获取一次实体...");
            ClassInfo classInfo2 = schoolMapper.getClassInfo2(1);
        } catch (Exception e) {
            logger.info(e.getMessage(), e);
        } finally {
            if(sqlSession != null) {
                sqlSession.close();
            }
        }   
    }
    

    例子里进行了两次查询,查看控制台,发现只执行了一次SQL语句。

    一级缓存结果.png

    每次查询结果MyBatis都会进行缓存,因此第二次查询的时候可以直接获取结果。但是如果执行了增、删、改等操作,则缓存会刷新。

    二级缓存

    将测试用例改为创建2个sqlSessoin,如下所示。

    @Test
    public void testSecondLevelCache() {
        SqlSession sqlSession1 = null;
        SqlSession sqlSession2 = null;
        try {
            sqlSession1 = DataConnection.openSqlSession();
            SchoolMapper schoolMapper1 = sqlSession1.getMapper(SchoolMapper.class);
            ClassInfo classInfo1= schoolMapper1.getClassInfo2(1);
            //需要提交,MyBatis才会缓存对象到SqlSessionFactory
            sqlSession1.commit();
            logger.info("再获取一次实体...");
            sqlSession2 = DataConnection.openSqlSession();
            SchoolMapper schoolMapper2 = sqlSession2.getMapper(SchoolMapper.class);
            ClassInfo classInfo2= schoolMapper2.getClassInfo2(1);
            sqlSession2.commit();
        } catch (Exception e) {
            logger.info(e.getMessage(), e);
        } finally {
            if(sqlSession1 != null) {
                sqlSession1.close();
            }
            if(sqlSession2 != null) {
                sqlSession2.close();
            }
        }   
    }
    

    commit方法是为了将方法提交到SqlSessionFactory。查看结果,发现进行了两次查询。

    二级缓存测试结果1.png

    在映射器文件中添加二级缓存配置cache。

    <cache />
    

    再次运行用例,查看结果,发现命中缓存,只查询了一次。

    二级缓存测试结果2.png

    缓存配置项

    前面是通用配置,还可以为每个语句单独配置。下面是各个语句的默认配置,可以根据实际需求修改。

    <select ... flushCache="false" useCache="true" />
    <insert ... flushCache="true" />
    <update ... flushCache="true" />
    <delete ... flushCache="true" />
    

    另外,MyBatis还可以使用Redis等外部缓存。

    存储过程

    待补充。

    相关文章

      网友评论

          本文标题:Mybatis学习笔记(四):映射器介绍(下)

          本文链接:https://www.haomeiwen.com/subject/wpozlqtx.html