一、用法提前看
场景假设:有A、B两个对象,在A对象中引用了B对象。Java层面上,通俗来说就是通过A对象的getter方法可以拿到B对象的引用。数据库层面,其实就是两表的关联查询。对于这种情况,大概有以下三种编写方式(都是使用resultMap进行结果集映射)。
- 不使用association标签,result标签的property属性使用A.B的形式;
- 使用association标签,并将其property属性的值设为A对象中为B对象指定的名称,添加javaType属性,将其值设为B的全限定名称,最后在association的子标签中完成对B对象的映射;
- 使用association标签,并将其property属性的值设为A对象中为B对象指定的名称,添加select属性,将其值设为查询B方法的全限定名称,column属性对应字段的值将作为查询B方法的参数,最后在association的子标签中完成对B对象的映射。
看完以上描述,你可能觉得有些抽象或者难以理解,没关系,结合下面的代码可以很好的理解这几种不同的编写方式。
二、用法示例
本节示例沿用上一章的Employee实体及相关的mapper等,新增一张业务表tbl_department用来表示员工的部门。下面是这张表的结构,java层面的相关类和配置文件会在下面具体说明。
- 不使用association标签的方式
使用这种方式,写一条sql语句,比较直接
sql部分:
<select id="getEmployeeWithDeptById0" resultMap="employeeDept0">
select e.id id, e.name name, e.gender gender, e.email email, e.d_id departmentId, d.department_name departmentName
from tbl_employee e, tbl_department d
where d.id=e.d_id and e.id=#{id}
</select>
对应的resultMap如下,可以看到这种写法比较清爽和直接:
<!-- 第一种:利用resultMap进行级联查询,不使用association标签-->
<resultMap id="employeeDept0" type="com.hly.entity.Employee">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="gender" property="gender"></result>
<result column="email" property="email"></result>
<result column="departmentId" property="department.id"></result>
<result column="departmentName" property="department.departmentName"></result>
</resultMap>
- 使用association标签+javaType属性
这种方法个人感觉跟第一种没有本质上的区别,还是一条sql语句对两张表进行关联查询,只不过在结果集映射的时候有一些不同,引入了association标签。可读性比较好,对象的结构关系相较于第一种方式来说更为清晰和明朗。
sql部分,与第一种无异:
<select id="getEmployeeWithDeptById" resultMap="employeeDept">
select e.id id, e.name name, e.gender gender, e.email email, e.d_id departmentId, d.department_name departmentName
from tbl_employee e, tbl_department d
where d.id=e.d_id and e.id=#{id}
</select>
resultMap在进行结果映射时,有一定的区别:
<!-- 第二种:利用resultMap进行级联查询,使用association标签 -->
<resultMap id="employeeDept" type="com.hly.entity.Employee">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="gender" property="gender"></result>
<result column="email" property="email"></result>
<association property="department" javaType="com.hly.entity.Department">
<id column="departmentId" property="id"></id>
<result column="departmentName" property="departmentName"></result>
</association>
</resultMap>
可以看到在这种写法中,通过association标签明确指定了department对象的类型,然后在这个association的子标签中对department对象进行结果映射。
- 使用association标签+select属性
这种方法就有意思了。与前面两种写法有比较大的不同,使用association的select标签,可以将原本两表联查的一条sql语句拆分为两条简单的sql语句。个人以为搞出这种方式的原因就是要支持级联查询的懒加载吧,这样可以很好的提升数据库的性能,毕竟只有在用到关联对象相关属性的时候,才会执行第二步的查询操作。这部分内容等到后面了解其原理,看过源码后再回来详细说明,在此留一个根。
sql部分,这里就分两部分了。第一是在tbl_employee表中,根据id查出对应的记录。第二步就是根据前一步中查出的d_id的值,在tbl_department中查询对应的记录。注意这两个sql是分散在两个mapper.xml中的哈。
sql1,员工的查询:
<select id="getEmployeeWithDeptById0" resultMap="employeeDept0">
select e.id id, e.name name, e.gender gender, e.email email, e.d_id departmentId, d.department_name departmentName
from tbl_employee e, tbl_department d
where d.id=e.d_id and e.id=#{id}
</select>
sql2,部门的查询:
<select id="getDeptById" resultType="com.hly.entity.Department">
SELECT id id, department_name departmentName FROM tbl_department where id=#{id}
</select>
这种方式下,对结果集的映射:
<!-- 第三种:利用resultMap进行分步查询 -->
<resultMap id="employeeByStep" type="com.hly.entity.Employee">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="gender" property="gender"></result>
<result column="email" property="email"></result>
<association property="department" select="com.hly.dao.DepartmentMapper.getDeptById" column="d_id">
<id column="id" property="id"></id>
<result column="department_name" property="departmentName"></result>
</association>
</resultMap>
association标签中有两个重要的属性,select是用来指定这个对象怎么去查,而column属性则是从第一步的查询结果中找出select所需的查询参数。
三、效果查看
本节省略了mapper接口类以及@Test方法的展示,如果需要看,可移步github瞅一眼,没什么好说的。
- 第一种&第二种
DEBUG [main] - ==> Preparing: select e.id id, e.name name, e.gender gender, e.email email, e.d_id departmentId, d.department_name departmentName from tbl_employee e, tbl_department d where d.id=e.d_id and e.id=?
DEBUG [main] - ==> Parameters: 4(Integer)
DEBUG [main] - <== Total: 1
Employee{id=4, name='John He1122', gender='1', email='1@webank.com'}
Department{id=1, departmentName='开发部'}
控制台打印的信息如上,执行了一条sql语句,输入一个Integer类型参数,得到一条结果。下面两行是打印的查询结果,员工信息和部门信息都打印出来了,没什么毛病。
- 第三种
DEBUG [main] - ==> Preparing: SELECT id,name,gender,email,d_id FROM tbl_employee where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: SELECT id id, department_name departmentName FROM tbl_department where id=?
DEBUG [main] - ====> Parameters: 1(Integer)
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
Lingyu He
开发部
控制台的输出也比较直观,执行了两条语句,各查出一条记录。最后两行分别打印的是e.getNmae(),e.getDepartment().getDepartmentName()。
四、懒加载
我用自己的话描述下懒加载的感念。懒加载也叫做延迟加载,可以理解为数据并不是随着动作的执行而立刻被加载(或查询),而是等到真正需要使用到这部分数据时,才会执行对数据的加载(或查询)。
mybatis的社区应该是非常优秀,官方文档提供了多语言的支持,当然不乏简体中文,这是很赞的。但是,本着学习英语的美好愿景,还是看一下英文文档里,对懒加载内容相关的说明吧。
全局配置文件中,有两个跟懒加载机制息息相关的参数,对执行模块的懒加载行为有着决定性的作用。
setting : lazyLoadingEnabled
description : Globally enables or disables lazy loading. When enabled, all relations will be lazily loaded. This value can be superseded for an specific relation by using the fetchType attribute on it.
valid values : true | false
default : false
这个东西呢就是说,在全局配置文件里,Settings这个配置标签中,有一个很重要的配置项叫做lazyLoadingEnabled。从名字就可以看出这是一个开关项,用来控制全局的懒加载功能。同时,描述中也说了,这个值可以通过在特定的级联查询上通过设置fetchType的方式,对全局的设值做一个局部覆盖。取值范围就是true或者是false,不设置的话默认是false。
setting : aggressiveLazyLoading
description : When enabled, any method call will load all the lazy properties of the object. Otherwise, each property is loaded on demand (see also lazyLoadTriggerMethods)
valid values : true | false
default : false (true in ≤3.4.1)
这个设置项的意思是说,当我是true的时候,一个对象中任意一个方法的调用,都会引起该对象所有延迟加载对象的加载;如果为false的话,那么就还是按需加载。这个aggressive的命名,意味深长呀。。。
具体的使用示例在下一小节里展示。
五、懒加载后续
先来看一下对上一小节两个配置的应用效果吧。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="true"></setting>
</settings>
先附上这个@Test方法的代码(也就是前面所说的第三种实现方式的test方法):
@Test
public void testAssociationByStep(){
SqlSession sqlSession = null;
try {
sqlSession = getSession();
EmployeeDeptMapper employeeMapper = sqlSession.getMapper(EmployeeDeptMapper.class);
Employee e = employeeMapper.getEmpByStep(1);
System.out.println(e.getName());
System.out.println(e.getDepartment().getDepartmentName());
}catch(Exception e){
e.printStackTrace();
}finally{
sqlSession.close();
}
}
还记得之前没加两个配置项时候的输出步?我们再来回顾一下:
DEBUG [main] - ==> Preparing: SELECT id,name,gender,email,d_id FROM tbl_employee where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: SELECT id id, department_name departmentName FROM tbl_department where id=?
DEBUG [main] - ====> Parameters: 1(Integer)
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
Lingyu He
开发部
可以看到,把两条语句都执行了,拿到结果后,打印了日志。再来看看加了两个配置项之后的输出。
DEBUG [main] - ==> Preparing: SELECT id,name,gender,email,d_id FROM tbl_employee where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Lingyu He
DEBUG [main] - ==> Preparing: SELECT id id, department_name departmentName FROM tbl_department where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
开发部
有木有发现细微的不同?没错,在两条sql语句之间打印了员工的name,后面因为调用了员工的getDepartment()方法,才又执行了第二条语句,查出了部门名称,是否看的出懒加载的特性了?如果觉得不明显,没关系,我们进一步进行修改。直接把@Test中对getDepartment的调用注释调,然后再运行一次观察结果:
DEBUG [main] - ==> Preparing: SELECT id,name,gender,email,d_id FROM tbl_employee where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Lingyu He
这下好了,只执行了一条语句,并没有去查询员工的部门信息。这下就完美的证实了懒加载的特性。关于另外一个配置项的使用效果,也可以很简单的通过修改部分代码来验证,不赘述。
六、问题留根
问题是这样的,上一节的@Test代码中,如果第一个输出语句,我们那不打印name,而是直接打印Employee这个对象,会发现懒加载失败。
sout语句在打印一个对象时,会去调这个对象的toString方法。此时作者认为如果再toString中不引用department对象的内容,应该就不会破坏懒加载的条件,然而事实是,依然不会懒加载。
那么这里自然而然就抛出一个问题,既然懒加载是按需加载,那么这个需到底是什么时候,是什么东西,也就是说加载的时机和条件是如何定义和设置的。mybatis里面肯定会有相关的设置和触发加载的定义,现在抛出这个问题,后面看到弄懂了再回来补上。