Mybatis学习笔记(四):映射器介绍(下)
本文档讲述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等外部缓存。
存储过程
待补充。