MyBatis学习记录04——Mapper(下)

###0.前言
《Java EE互联网轻量级框架整合开发——SSM框架(Spring MVC+Spring+MyBatis)和Redis实现》

Mapper的学习记录

本文主要记录Mapper的级联以及缓存设置

###1.级联
通过级联获取关联数据十分便捷,但是级联过多会增加系统的复杂度,降低系统的性能。建议超过3层时就不要考虑级联。

MyBatis中的级联有3种:

  1. 一对一:association
  2. 一对多:collection
  3. 鉴别器: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中的属性,用于接收associationselect返回的结果集
  • 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中的属性,用于接收collectionselect返回的结果集
  • 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只会设置idsexmaleHealthForm3个属性,其他属性为空
  • 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问题。

  1. MyBatis的settings配置延时加载:
  • lazyLoadingEnable:延时加载全局开关。如果为false,则所有级联都会被初始化加载。(默认值:false)
  • aggressiveLazyLoading:按需加载属性,如果为true,只要加载对象就会加载该对象的所有属性(lazyLoadingEnable:true时。只限于属性同一层级,即属性内部的层级暂不加载)。如果false,该属性则会按需加载,即使用到某关联属性时,实时执行嵌套查询加载该属性(默认值:3.4.1之前为true,之后为false)
  1. 级联(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层面

<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

分享到