本文最后更新于:2022-10-11T22:47:54+08:00
简介
在前面的select标签当中,我们提到一定要指定resultType或者resultMap。前面我们一直使用的都是resultType,接下来就来介绍resultMap的使用。
无论是resultType还是resultMap,它们完成的工作都是确定如何将查询得到的一行数据与Java中的实体类完成映射。在前面的环境中,我们数据库中的users表有如下的表结构:
而我们的实体类User中的定义如下:
1 2 3 4 5 6 7 8 9 10 11 public class User { private int id; private String username; private String password; private int age; private String gender; private String email; }
可以看到的是,表中的字段和实体类中的属性是可以对应上的。(注意这里关于成员变量以及属性的区分)默认属性名和字段名保持一致的话,可以完成映射,因此我们前面一直使用的都是resultType直接指定实体类。而resultMap为自定义映射,解决的是一些属性和字段没有保持一致,或者一些多对一,一对多的映射情况。接下来就进行相关的说明。
这里说明后续的数据库环境。在数据库中新建了两张表,分别是员工表以及部门表,其中员工表中的字段dept_id表示员工所处的部门,部门表的主键。
员工表emp
中的字段如下,同时准备了一些测试数据:
部门表dept
中的字段如下:
resultMap使用
字段和属性映射
在Java中,我们一般遵循的是驼峰命名法,而在数据库中,我们一般使用下划线。这样就可能出现字段和属性无法对应的问题。
例如现在我们需要查询员工中的数据,我们准备了一个员工的实体类如下,其中的命名遵循的是驼峰命名法。
1 2 3 4 5 6 7 8 public class Emp { private int empId; private String empName; private int deptId; }
之后我们写一个简单的接口,按照员工id来查询该员工的信息。
1 2 3 4 5 6 <select id ="getEmpById" resultType ="com.syh.bean.Emp" > select * from emp where emp_id = #{id}</select >
执行测试方法,发现输出为null。但是实际上我们能够查询到数据的,这实际上就是因为表中的字段名和实体类的属性名没有对应上,我们查出来的一行,它的字段名是emp_id, emp_name, dept_id
,而我们的属性名则是empId, empName, deptId
,无法对应,因此查出为null。
解决这个问题有两种方式,第一种是在核心配置文件中开启下划线到驼峰的转换设置,如下所示,这样就可以开启下划线方式与驼峰方式命名之间的对应,二者能够对应上了,也就可以查出结果来了。
1 2 3 <settings > <setting name ="mapUnderscoreToCamelCase" value ="true" /> </settings >
第二种方式则是使用resultMap。我们在映射文件中先定义一个resultMap,然后在其中指定属性与字段的对应关系,如下所示。最后在select标签中指定使用对应的resultMap,这样也可以查出结果。这种解决方案同样是给出了对应关系,告诉MyBatis该如何完成对应,因此可以查出结果。
1 2 3 4 5 6 7 8 9 10 11 12 <resultMap id ="empMap" type ="com.syh.bean.Emp" > <id property ="empId" column ="emp_id" /> <result property ="empName" column ="emp_name" /> <result property ="deptId" column ="dept_id" /> </resultMap > <select id ="getEmpById" resultMap ="empMap" > select * from emp where emp_id = #{id}</select >
在resultMap标签中,使用了id属性以及type属性:
id:表示自定义映射的唯一标识
type:表示查询的数据需要映射到的实体类的类型
在子标签中,我们使用到了id标签以及result标签,还有property,column属性:
id 标签:设置主键的对应关系
result 标签:设置普通字段的映射关系
property 属性:设置映射关系中实体类的属性名
column 属性:设置映射关系中表的字段名
后面我们还会用到association标签以及collection标签,分别解决多对一和一对多的映射关系。
多对一映射处理
多对一的关系指的是数据库中表的关系。例如在我们目前的环境中,多个员工对应一个部门,就是一种多对一的关系。我们稍微修改一下实体类的设计,使其能够对应我们的关系。
修改后的Emp
类:
1 2 3 4 5 6 7 8 public class Emp { private int empId; private String empName; private Dept dept; }
修改后的Dept
类:
1 2 3 4 5 6 7 8 public class Dept { private int deptId; private String deptName; private List<Emp> empList; }
我们目前希望完成的查询同样是查询一个员工的信息,可以写出如下的查询语句(这里开启了全局的下划线转驼峰的设置):
1 2 3 4 5 6 7 <select id ="getEmpById" resultType ="com.syh.bean.Emp" > select * from emp left join dept on emp.dept_id = dept.dept_id where emp.emp_id = #{id}</select >
但是经过测试输出后,我们发现输出结果如下:
1 Emp{empId =1, empName ='甲' , dept =null }
其中emp相关的属性确实正确的返回了,但是部门信息没有返回。分析原因,同样是MyBatis无法根据现有的信息找到对应关系,查询结果中的字段分别为emp_id, emp_name, emp.dept_id, dept.dept_id, dept_name
,能够对应的字段只有emp_id和emp_name,所以也就只有这两个属性能够对应上。
解决多对一映射问题的方法有三种,分别是级联方式,association标签以及分步查询,下面分别进行介绍。
级联方式
使用级联方式,就是让我们在resultMap中指定相关的对应关系。如果存在类之间的级联,使用.
来表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <resultMap id ="empMap" type ="com.syh.bean.Emp" > <id property ="empId" column ="emp_id" /> <result property ="empName" column ="emp_name" /> <result property ="dept.deptId" column ="dept_id" /> <result property ="dept.deptName" column ="dept_name" /> </resultMap > <select id ="getEmpById" resultMap ="empMap" > select * from emp left join dept on emp.dept_id = dept.dept_id where emp.emp_id = #{id}</select >
查出结果如下,发现可以查出对应的部门信息:
1 Emp{empId =1, empName ='甲' , dept =Dept{deptId =1, deptName ='A' , empList =null }}
association
第二种方式是使用resultMap下的子标签association,在其中指定对应的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <resultMap id ="empMap" type ="com.syh.bean.Emp" > <id property ="empId" column ="emp_id" /> <result property ="empName" column ="emp_name" /> <association property ="dept" javaType ="com.syh.bean.Dept" > <id property ="deptId" column ="dept_id" /> <result property ="deptName" column ="dept_name" /> </association > </resultMap > <select id ="getEmpById" resultMap ="empMap" > select * from emp left join dept on emp.dept_id = dept.dept_id where emp.emp_id = #{id}</select >
association标签是用来设置多对一的映射关系的,其中的property
表示需要映射的实体类中的属性,javaType
表示要映射成为哪个类。在association子标签中,同样有id以及result子标签,表示主键以及普通字段。
分步查询
分步查询则将这个问题分成两个步骤来进行解决。这个问题可以拆解成两步完成,第一步先查出员工的部门id,第二步则是根据员工所对应的部门id查询部门信息。
首先第一步,查询员工的信息:
1 2 3 4 5 6 7 8 9 10 11 12 <resultMap id ="empMapStep" type ="com.syh.bean.Emp" > <id property ="empId" column ="emp_id" /> <result property ="empName" column ="emp_name" /> <association property ="dept" select ="com.syh.mapper.DeptMapper.getDeptById" column ="dept_id" /> </resultMap > <select id ="getEmpByIdStep" resultMap ="empMapStep" > select * from emp where emp_id = #{id}</select >
这里,association中设置的select
表示下一步需要指定的操作,column
表示提供给它的参数。这里下一步操作即调用对应的接口,这里的操作应该是根据部门id来查询部门的相关信息,因此接下来就实现第二步操作。
1 2 3 4 5 6 <select id ="getDeptById" resultType ="com.syh.bean.Dept" > select * from dept where dept_id = #{id}</select >
最终查询的输出如下,可以看到是能够正常查询得到结果的,并且是进行了分步的查询:
1 2 3 4 5 6 7 DEBUG 10 -11 20 :54 :39 ,378 ==> Preparing: select * from emp where emp_id = ? (BaseJdbcLogger.java:137 ) DEBUG 10 -11 20 :54 :39 ,414 ==> Parameters: 1 (Integer) (BaseJdbcLogger.java:137 ) DEBUG 10 -11 20 :54 :39 ,441 ====> Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137 ) DEBUG 10 -11 20 :54 :39 ,442 ====> Parameters: 1 (Integer) (BaseJdbcLogger.java:137 ) DEBUG 10 -11 20 :54 :39 ,444 <==== Total: 1 (BaseJdbcLogger.java:137 ) DEBUG 10 -11 20 :54 :39 ,446 <== Total: 1 (BaseJdbcLogger.java:137 ) Emp {empId=1 , empName='甲', dept=Dept{deptId=1 , deptName='A', empList=null}}
一对多映射处理
上面的情况中,多个员工对应一个部门是多对一的情况,那么反过来就是一对多的情况。反映的需求当中,现在我们希望实现根据部门id来查询部门以及部门中员工的信息。在Dept实体类中,存在一个Emp列表,直接查询是无法得到结果的,因此还是需要使用自定义映射来解决。解决问题的方式有两种,分别是使用collection标签和分步查询。
collection
使用resultMap中的collection标签可以解决一对多映射处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <resultMap id ="deptMap" type ="com.syh.bean.Dept" > <id property ="deptId" column ="dept_id" /> <result property ="deptName" column ="dept_name" /> <collection property ="empList" ofType ="com.syh.bean.Emp" > <id property ="empId" column ="emp_id" /> <result property ="empName" column ="emp_name" /> </collection > </resultMap > <select id ="getDeptEmpById" resultMap ="deptMap" > select * from dept join emp on dept.dept_id = emp.dept_id where dept.dept_id = #{id}</select >
在collection标签中,我们需要指定property属性,以及集合中元素的类型ofType
,内部子标签id以及result,则同样代表了主键以及普通字段的映射。
测试输出结果如下,发现可以正常得到结果。
1 Dept{deptId =1, deptName ='A' , empList=[Emp{empId =1, empName ='甲' , dept =null }, Emp{empId =4, empName ='丁' , dept =null }]}
其中每个Emp的部门显示为null也是因为我们没有在collection中指定对应关系。
分步查询
第二种方式是分步查询。这个问题也可以拆分成两个步骤,第一步查询出部门的相关信息,得到部门id;第二步根据部门id来查询员工信息。
1 2 3 4 5 6 7 8 9 10 11 12 <resultMap id ="deptMapStep" type ="com.syh.bean.Dept" > <id property ="deptId" column ="dept_id" /> <result property ="deptName" column ="dept_name" /> <collection property ="empList" select ="com.syh.mapper.EmpMapper.getEmpByDeptId" column ="dept_id" /> </resultMap > <select id ="getDeptEmpByIdStep" resultMap ="deptMapStep" > select * from dept where dept_id = #{id}</select >
这里在collection标签中,同样需要指定select
表示下一步的操作,column
表示传递过去的参数。下一步是根据部门id来查询员工信息,则可以用下面的操作来完成:
1 2 3 4 5 6 <select id ="getEmpByDeptId" resultType ="com.syh.bean.Emp" > select * from emp where dept_id = #{id}</select >
(这里的方法返回类型,定义成List<Emp>
还是Emp
,都能够得到正确的结果,不过按照场景,定义成列表更加符合逻辑)
之后进行测试,可以得到下面的输出结果:
1 2 3 4 5 6 7 DEBUG 10 -11 21 :22 :01 ,439 ==> Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :22 :01 ,474 ==> Parameters: 1 (Integer) (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :22 :01 ,498 ====> Preparing: select * from emp where dept_id = ? (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :22 :01 ,499 ====> Parameters: 1 (Integer) (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :22 :01 ,502 <==== Total: 2 (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :22 :01 ,504 <== Total: 1 (BaseJdbcLogger.java:137 ) Dept {deptId=1 , deptName='A', empList=[Emp{empId=1 , empName='甲', dept=null}, Emp{empId=4 , empName='丁', dept=null}]}
可以发现确实查询到了正确的结果,并且使用了分步查询。
延迟加载
在多对一和一对多的映射处理中,我们都使用到了分步查询来解决。分步查询的优点在于可以实现延迟加载。延迟加载指的是可以完成按需加载,当前获取的数据是什么,就执行相应的SQL,而不需要全部执行。
开启延迟加载需要在核心配置文件中开启全局配置信息:
1 2 3 4 <settings > <setting name ="lazyLoadingEnabled" value ="true" /> <setting name ="aggressiveLazyLoading" value ="false" /> </settings >
lazyLoadingEnabled
:延迟加载的全局配置。当设置为true时,所有的关联对象都会延迟加载
aggressiveLazyLoading
:加载属性的策略。当设置为true的时候,任何方法的调用都会加载该对象的所有属性。否则按需加载。默认值为false
如果测试代码中,我们只访问部门的id,而不需要访问部门的员工列表,则它会进行按需加载。测试代码和输出分别如下,可以看到其中只访问了部门的id,只执行了第一步的SQL语句。
关键测试代码:
1 2 3 DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);Dept deptEmpById = mapper.getDeptEmpByIdStep(1 ); System.out.println(deptEmpById.getDeptName());
输出:
1 2 3 4 DEBUG 10 -11 21 :29 :25 ,743 ==> Preparing: select * from dept where dept_id = ? (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :29 :25 ,772 ==> Parameters: 1 (Integer) (BaseJdbcLogger.java:137 ) DEBUG 10 -11 21 :29 :25 ,841 <== Total: 1 (BaseJdbcLogger.java:137 ) A
上面的配置是全局配置信息,对全局生效。实际上每个collection或者association标签也有自己的按需加载的开关,即标签中的fetchType
属性。该属性可选的取值有两种,分别是lazy
延迟加载,以及eager
立即加载,默认取值为lazy
。