###0.前言
《Java EE互联网轻量级框架整合开发——SSM框架(Spring MVC+Spring+MyBatis)和Redis实现》
Mapper的学习记录
本文主要记录Mapper的级联以及缓存设置
###1.级联
通过级联获取关联数据十分便捷,但是级联过多会增加系统的复杂度,降低系统的性能。建议超过3层时就不要考虑级联。
MyBatis中的级联有3种:
- 一对一:association
- 一对多:collection
- 鉴别器:discriminator,根据某些条件决定采取具体实现类的方案。(eg:根据性别获取员工信息)
####1.1 association 一对一级联
一个员工对应着一个工牌:
//Employee.java POJO类
public class Employee {
private Long id;
private String realName;
private SexEnum sex = null;
//...
//工牌按一对一级联
private WorkCard workCard;
//...
}
//WorkCard.java POJO类
public class WorkCard {
private Long id;
private Long empId;
private String realName;
private String department;
private String mobile;
private String position;
private String note;
//...
}
//EmployeeMapper.xml
<mapper namespace="com.ssm.chapter5.mapper.EmployeeMapper">
<resultMap type="com.ssm.chapter5.pojo.Employee" id="employee">
<id column="id" property="id" />
<result column="real_name" property="realName" />
//...
<association property="workCard" column="id"
select="com.ssm.chapter5.mapper.WorkCardMapper.getWorkCardByEmpId" />
//...
</resultMap>
</mapper>
//WorkCardMapper.xml
<mapper namespace="com.ssm.chapter5.mapper.WorkCardMapper">
<select id="getWorkCardByEmpId" parameterType="long" resultType="com.ssm.chapter5.pojo.WorkCard">
SELECT id, emp_id as empId, real_name as realName, department, mobile, position, note FROM t_work_card
where emp_id = #{empId}
</select>
</mapper>
上述是association的示例,各个参数的含义:
- property:对应POJO中的属性,用于接收
association
中select
返回的结果集 - column : 对应SQL的列,作为属性传递给select属性定义的sql,多属性以”,”隔开
- 多参数格式:column=”{prop1=col1,prop2=col2}”
- select :Mapper类的全限定名+id组成,对应SQL语句,接收column指定的值作为参数
- 接收多参数时,参数类型指定为parameterType=”map”
- javaType:用于定义实体映射
####1.2 collection 一对多级联
一个员工对应着多个任务:
//Employee.java POJO类
public class Employee {
private Long id;
private String realName;
private SexEnum sex = null;
//...
//雇员任务,一对多级联
private List<EmployeeTask> employeeTaskList = null;
//...
}
//EmployeeTask.java POJO类
public class EmployeeTask {
private Long id;
private Long empId;
private Task task = null;
private String taskName;
private String note;
//...
}
//EmployeeMapper.xml
<resultMap type="com.ssm.chapter5.pojo.Employee" id="employee">
<id column="id" property="id" />
<result column="real_name" property="realName" />
//...
<collection property="employeeTaskList" column="id"
select="com.ssm.chapter5.mapper.EmployeeTaskMapper.getEmployeeTaskByEmpId" />
//...
</resultMap>
//EmployeeTask.xml
<mapper namespace="com.ssm.chapter5.mapper.EmployeeTaskMapper">
<resultMap type="com.ssm.chapter5.pojo.EmployeeTask" id="EmployeeTaskMap">
<id column="id" property="id"/>
<result column="emp_id" property="empId"/>
<result column="task_name" property="taskName"/>
<result column="note" property="note"/>
<association property="task" column="task_id"
select="com.ssm.chapter5.mapper.TaskMapper.getTask"/>
</resultMap>
<select id="getEmployeeTaskByEmpId" resultMap="EmployeeTaskMap">
select id, emp_id, task_name, task_id, note from t_employee_task
where emp_id = #{empId}
</select>
</mapper>
上述是collection
的示例,各个参数的含义与association
相似。
- property:对应POJO中的属性,用于接收
collection
中select
返回的结果集 - column : 对应SQL的列,作为属性传递给select属性定义的sql,多属性以”,”隔开
- 多参数格式:column=”{prop1=col1,prop2=col2}”
- select :Mapper类的全限定名+id组成,对应SQL语句,接收column指定的值作为参数
- 接收多参数时,参数类型指定为parameterType=”map”
- ofType:用于定义实体映射
####1.3 多对多级联
多对多级联通常会被拆分为两个一对多级联处理。
例如:一个用户拥有多个角色,一个角色可以有多个用户担当。
//Role2.java POJO类
public class Role2 {
private Long id;
private String roleName;
private String note;
// 关联用户信息,一对多关联
private List<User2> userList;
//...
}
//User2.java POJO类
public class User2 {
private Long id;
private String userName;
private String realName;
private SexEnum sex;
private String moble;
private String email;
private String note;
// 对角色一对多关联
private List<Role2> roleList;
//...
}
//RoleMapper2.xml
<mapper namespace="com.ssm.chapter5.mapper2.RoleMapper2">
<resultMap type="com.ssm.chapter5.pojo2.Role2" id="roleMapper">
<id column="id" property="id" />
<result column="role_name" property="roleName" />
<result column="note" property="note" />
<collection property="userList" column="id"
select="com.ssm.chapter5.mapper2.UserMapper2.findUserByRoleId" />
</resultMap>
//...
<select id="findRoleByUserId" parameterType="long" resultMap="roleMapper">
select r.id, r.role_name, r.note from t_role r, t_user_role ur
where r.id = ur.role_id and ur.user_id = #{userId}
</select>
</mapper>
//UserMapper2.xml
<mapper namespace="com.ssm.chapter5.mapper2.UserMapper2">
<resultMap type="com.ssm.chapter5.pojo2.User2" id="userMapper">
<id column="id" property="id" />
<result column="user_name" property="userName" />
//...
<collection property="roleList" column="id"
select="com.ssm.chapter5.mapper2.RoleMapper2.findRoleByUserId" />
</resultMap>
//...
<select id="findUserByRoleId" parameterType="long" resultMap="userMapper">
select u.id, u.user_name, u.real_name, u.sex, u.moble, u.email, u.note
from
t_user u , t_user_role ur where u.id = ur.user_id and ur.role_id =#{roleId}
</select>
</mapper>
上述为多对多拆分为两个一对多的例子。
为用户添加一个角色列表roleList
,为角色添加一个用户列表userList
。另一个方法是创建一个用户角色关系表记录关系。
####1.4 discriminator 鉴别器
鉴别器,通过指定字段的值返回不同的结果集。类似java中的switch语句。
示例:员工分为男性员工与女性员工,如果需要体检,那么男性员工与女性员工的体检项目不同。即显示的体检项目表不同。
//Employee.java POJO类
public class Employee {
private Long id;
private String realName;
private SexEnum sex = null;
//...
}
//MaleEmployee.java POJO类,继承Employee
public class MaleEmployee extends Employee {
private MaleHealthForm maleHealthForm = null; //男性员工体检表
public MaleHealthForm getMaleHealthForm() {
return maleHealthForm;
}
public void setMaleHealthForm(MaleHealthForm maleHealthForm) {
this.maleHealthForm = maleHealthForm;
}
}
//EmployeeMapper.xml
<mapper namespace="com.ssm.chapter5.mapper.EmployeeMapper">
<resultMap type="com.ssm.chapter5.pojo.Employee" id="employee">
<id column="id" property="id" />
<result column="real_name" property="realName" />
<result column="sex" property="sex"
typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" />
//...
<!--核心代码-->
<discriminator javaType="int" column="sex"> <!--根据性别指定体检表结果集的类型-->
<case value="1" resultMap="maleHealthFormMapper" />
<case value="2" resultMap="femaleHealthFormMapper" />
</discriminator>
</resultMap>
<resultMap type="com.ssm.chapter5.pojo.MaleEmployee" id="maleHealthFormMapper"
extends="employee"> <!--定义体检表结果集,此处继承基类-->
<association property="maleHealthForm" column="id"
select="com.ssm.chapter5.mapper.MaleHealthFormMapper.getMaleHealthForm" />
</resultMap>
//...
</mapper>
//MaleHealthFormMapper.xml
<mapper namespace="com.ssm.chapter5.mapper.MaleHealthFormMapper">
<select id="getMaleHealthForm" parameterType="long"
resultType="com.ssm.chapter5.pojo.MaleHealthForm">
select id, heart, liver, spleen, lung, kidney, prostate, note from
t_male_health_form where emp_id = #{id}
</select>
</mapper>
discriminator元素说明:
- column :指定用于鉴别的SQL字段名(eg:column=”sex”指定SQL中列名为sex的字段作为鉴别字段)
- javaType :指定字段对应的值的类型(eg:sex字段对应的值为整型:1、2)
- case:排它性的条件分支。case的结果集可以通过resultType或者resultMap设置(不能同时使用)
- resultMap:指定返回数据的结果集映射关系。
- resultType:指定返回数据的类型,自动映射。当列名与属性名不对应时,可以重新设置
<id />
、<result />
。
discriminator的作用归纳:
根据数据库查询出的数据的指定值,判断返回哪一个结果集。discriminator
中的case
定义了返回的结果集。
- 没有鉴别器时,
<select resultMap="employee">
语句的返回结果集由id=employee
的resultMap决定 - 存在鉴别器时,
<select resultMap="employee">
语句的返回结果集根据discriminator
中的case
决定
eg:上述例子中EmployeeMapper.xml
的结果集存在鉴别器,即调用<select resultMap="employee">
等语句返回的结果集的类型为:
- sex = 1 => id = maleHealthFormMapper 的 resultMap(MaleEmployee类)
- sex = 2 => id = femaleHealthFormMapper 的 resultMap(FemaleEmployee类)
- sex != 1和2(即未定义)=> id = employee的resultMap(Employee类-默认)
可以理解为<select resultMap="employee">
等语句返回类型有3种,分别是MaleEmployee、FemaleEmployee和Employee。
Case使用的注意事项:
case中可以指定返回结果集的类型,即resultType
或者resultMap中的type
,需要动态的修改结果文件mapper.java中的返回类型,否则会抛类型转化异常。
eg:
//mapper.xml
//...
<discriminator javaType="int" column="sex">
<case value="1" resultType="string" />
<case value="2" resultType="int" />
</discriminator>
//...
//mapper.java
int getData();
如上述示例,getData()在case=1的情况下,如果结果集无法强转为int类型,那么抛出异常。
Case设置自身属性:
//mapper.xml
<resultMap type="com.ssm.chapter5.pojo.Employee" id="employee">
//...
<result column="col" property="prop"/> 如果不配置此项,只要执行case语句,则POJO中的prop为空(列名与POJO属性名一样)
<discriminator javaType="int" column="sex">
<case value="1" resultType="com.ssm.chapter5.pojo.Employee" >
<result column="col" property="prop1"/>
</case>
<case value="2" resultType="com.ssm.chapter5.pojo.Employee" />
<result column="col" property="prop2"/>
</case>
</discriminator>
</resultMap>
//...
resultType指定的类型与外部的resultMap类型一致:
- case 1:{prop:null,prop1:col,prop2:null}
- case 2: {prop:null,prop1:null,prop2:col}
- case (value!=1&&value!=2) :{prop:col,prop1:null,prop2:null} (假设POJO的属性prop与SQL的列名col相等)
设置了<result column="col" property="prop"/>
后:
- case 1:{prop:col,prop1:col,prop2:null}
- case 2:{prop:col,prop1:null,prop2:col}
- case (value!=1&&value!=2) :{prop:col,prop1:null,prop2:null}
resultMap中的extends
的作用:
- 继承指定resultMap的所有参数(所有的
<id />
、<result />
),注意:POJO类如果不存在该属性会抛异常 - resultType不需
extends
eg:
- resultMap不使用extends时,MaleEmployee只会设置
id
、sex
、maleHealthForm
3个属性,其他属性为空 resultMap使用extends后,等价于下面的配置(即
MaleEmployee
包含Employee
所有的<id />
、<result />
)<resultMap type="com.ssm.chapter5.pojo.MaleEmployee" id="maleHealthFormMapper"> <id column="id" property="id" /> <result column="real_name" property="realName" /> <!--原属于employee的配置--> <result column="sex" property="sex" <!--原属于employee的配置--> typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" /> //... <!--定义体检表结果集,此处继承基类--> <association property="maleHealthForm" column="id" select="com.ssm.chapter5.mapper.MaleHealthFormMapper.getMaleHealthForm" /> </resultMap>
###1.5 延迟加载
对于不常的级联采用延时加载的策略,避免N+1问题。
- MyBatis的settings配置延时加载:
- lazyLoadingEnable:延时加载全局开关。如果为false,则所有级联都会被初始化加载。(默认值:false)
- aggressiveLazyLoading:按需加载属性,如果为true,只要加载对象就会加载该对象的所有属性(lazyLoadingEnable:true时。只限于属性同一层级,即属性内部的层级暂不加载)。如果false,该属性则会按需加载,即使用到某关联属性时,实时执行嵌套查询加载该属性(默认值:3.4.1之前为true,之后为false)
- 级联(association、collection)元素配置 fetch
fetch的属性值:
- eager:获取当前POJO后立即加载
- lazy:获得当前POJO后延迟加载对应数据
fetch会忽略全局的lazyLoadingEnable和aggressiveLazyLoading配置
####1.6 基于SQL表连接的级联方式(join)
在编写SQL联表查询时,通常会使用join关键字(eg:Left join)。
//Mapper.xml中的select语句 => left jion
<select id="getEmployee2" parameterType="long" resultMap="employee2">
select
emp.id, emp.real_name, emp.sex, emp.birthday,
emp.mobile, emp.email,
emp.position, emp.note,
et.id as et_id, et.task_id as et_task_id,
et.task_name as et_task_name,
et.note as et_note,
if (emp.sex = 1,
mhf.id, fhf.id) as h_id,
if (emp.sex = 1, mhf.heart, fhf.heart) as
h_heart,
if (emp.sex = 1, mhf.liver, fhf.liver) as h_liver,
if (emp.sex
= 1, mhf.spleen, fhf.spleen) as h_spleen,
if (emp.sex = 1, mhf.lung,
fhf.lung) as h_lung,
if (emp.sex = 1, mhf.kidney, fhf.kidney) as
h_kidney,
if (emp.sex = 1, mhf.note, fhf.note) as h_note,
mhf.prostate
as h_prostate, fhf.uterus as h_uterus,
wc.id wc_id, wc.real_name
wc_real_name, wc.department wc_department,
wc.mobile wc_mobile,
wc.position wc_position, wc.note as wc_note,
t.id as t_id, t.title as
t_title, t.context as t_context, t.note as t_note
from t_employee emp
left join t_employee_task et on emp.id = et.emp_id
left join
t_female_health_form fhf on emp.id = fhf.emp_id
left join
t_male_health_form mhf on emp.id = mhf.emp_id
left join t_work_card wc
on emp.id = wc.emp_id
left join t_task t on et.task_id = t.id
where
emp.id = #{id}
</select>
//Mapper.xml中对应的resultMap ---BEG
<resultMap id="employee2" type="com.ssm.chapter5.pojo.Employee">
<id column="id" property="id" />
<result column="real_name" property="realName" />
<result column="sex" property="sex"
typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" />
<result column="birthday" property="birthday" />
<result column="mobile" property="mobile" />
<result column="email" property="email" />
<result column="position" property="position" />
<association property="workCard" javaType="com.ssm.chapter5.pojo.WorkCard"
column="id"> <!--声明POJO映射类型-->
<id column="wc_id" property="id" />
<result column="id" property="empId" />
<result column="wc_real_name" property="realName" />
<result column="wc_department" property="department" />
<result column="wc_mobile" property="mobile" />
<result column="wc_position" property="position" />
<result column="wc_note" property="note" />
</association>
<collection property="employeeTaskList" ofType="com.ssm.chapter5.pojo.EmployeeTask"
column="id"> <!--声明POJO映射类型-->
<id column="et_id" property="id" />
<result column="id" property="empId" />
<result column="task_name" property="taskName" />
<result column="note" property="note" />
<association property="task" javaType="com.ssm.chapter5.pojo.Task" <!--定义POJO映射类型-->
column="et_task_id">
<id column="t_id" property="id" />
<result column="t_title" property="title" />
<result column="t_context" property="context" />
<result column="t_note" property="note" />
</association>
</collection>
<discriminator javaType="int" column="sex">
<case value="1" resultMap="maleHealthFormMapper2" />
<case value="2" resultMap="femaleHealthFormMapper2" />
</discriminator>
</resultMap>
<resultMap type="com.ssm.chapter5.pojo.MaleEmployee" id="maleHealthFormMapper2"
extends="employee2">
<association property="maleHealthForm" column="id"
javaType="com.ssm.chapter5.pojo.MaleHealthForm"> <!--定义POJO映射类型-->
<id column="h_id" property="id" />
<result column="h_heart" property="heart" />
<result column="h_liver" property="liver" />
<result column="h_spleen" property="spleen" />
<result column="h_lung" property="lung" />
<result column="h_kidney" property="kidney" />
<result column="h_prostate" property="prostate" />
<result column="h_note" property="note" />
</association>
</resultMap>
<resultMap type="com.ssm.chapter5.pojo.FemaleEmployee" id="femaleHealthFormMapper2"
extends="employee">
<association property="femaleHealthForm" column="id"
javaType="com.ssm.chapter5.pojo.FemaleHealthForm"> <!--声明POJO映射类型-->
<id column="h_id" property="id" />
<result column="h_heart" property="heart" />
<result column="h_liver" property="liver" />
<result column="h_spleen" property="spleen" />
<result column="h_lung" property="lung" />
<result column="h_kidney" property="kidney" />
<result column="h_uterus" property="uterus" />
<result column="h_note" property="note" />
</association>
</resultMap>
//Mapper.xml中对应的resultMap ---END
这种方式虽然消除了N+1问题,但一次性取出所有数据造成内存的浪费,且后期维护困难。
如果SQL联表查询仅仅单次使用,用resultType=”map”会减少很多resultMap的定义。缺点是map的可读性比较低。
###2.缓存
MyBatis中存在一级缓存和二级缓存。
- 一级缓存:SqlSession上的缓存,需要采用同一SqlSession对象(默认启动)
- 二级缓存:SqlSessionFactory上的缓存,可以在不同的SqlSession中获取同一条记录
- mapper.xml中配置
<cache />
- 序列化POJO类(即实现Serializable接口)
- SqlSession调用语句后,调用sqlSession.commit()才会缓存到SqlSessionFactory层面
- mapper.xml中配置
<cache />
节点说明:加入该元素后,MyBatis会缓存对应命名空间的所有select元素SQL查询结果,而insert、delete和update语句在操作时会刷新缓存。
<cache />
的属性:
- blocking:是否使用阻塞性缓存,在读写加入JNI的锁进行操作。保证读写安全,但性能不佳(默认:false)
- readonly:缓存是否只读。多线程读写一致性。(默认:false)
- eviction:缓存策略。
- LRU:最近最少使用的,移除最长时间不被使用的对象
- FIFO:先进先出,按对象进入缓存的顺序来移除
- SOFT:软引用,移除基于软引用规则的对象
- WEAK:弱引用,移除基于弱引用规则的对象
- flushInterval:缓存有效期,以毫秒为单位(默认:null,即没有刷新时间,所以只有insert、delete和update语句在操作时会刷新缓存)
- type:自定义缓存,需要实现Cache接口
- size:缓存对象个数。(默认值:1024)
<cache />
属性会缓存当前命名空间的select元素SQL查询结果,如果需要个别处理,可通过在语句中加入属性:
- flushCache:是否刷新缓存(select、insert、delete、update均可用)
- useCache:是否使用缓存(只限于select语句)
提示:引用其他映射器的缓存配置可以通过<cache-ref namespace="..."/>
引用
###3.总结
- MyBatis的级联主要是方便关联数据的读取,但级联层数过多会影响系统的性能,增加系统复杂度。所以在使用级联的时候需要考虑级联的深度(最好小于等于3层),
- 在使用级联时,可以通过配置延时加载来减少内存消耗
- 全局配置lazyLoadingEnable 和aggressiveLazyLoading
- 局部配置fetch
- 可以通过开启缓存来优化数据的加载性能和速度。
- 一级缓存:SqlSession
- 二级缓存:SqlSessionFactory
END
– Nowy
– 2018.12.07