返回介绍

4.7 resultMap 结果映射集

发布于 2025-04-26 13:08:33 字数 37279 浏览 0 评论 0 收藏

resultMap 是 MyBatis 里面最复杂的元素。它的作用是定义映射规则、级联的更新、定制类型转化器等。不过不用担心,路还是需要一步步走的,让我们先从最简单的功能开始了解它。resultMap 定义的主要是一个结果集的映射关系。MyBatis 现有的版本只支持 resultMap 查询,不支持更新或者保存,更不必说级联的更新、删除和修改了。

4.7.1 resultMap 元素的构成

resultMap 元素里面还有以下元素,如代码清单 4-18 所示。

代码清单 4-18:resultMap 元素里的元素

<resultMap>
    <constructor >
        <idArg/>
        <arg/>
    </constructor>
    <id/>
    <result/>
    <association/>
    <collection/>
    <discriminator>
        <case/>
    </discriminator>
</resultMap>

其中 constructor 元素用于配置构造方法。一个 POJO 可能不存在没有参数的构造方法,这个时候我们就可以使用 constructor 进行配置。假设角色类 RoleBean 不存在没有参数的构造方法,它的构造方法声明为 public RoleBean(Integer id,String roleName),那么我们需要配置这个结果集,如代码清单 4-19 所示。

代码清单 4-19:resultMap 使用构造方法 constructor

<resultMap ......>
    <constructor >
        <idArg column= id  javaType= int />
        <arg column= role_name  javaType= string />
    </constructor>
......
</resultMap>

这样 MyBatis 就知道需要用这个构造方法来构造 POJO 了。

id 元素是表示哪个列是主键,允许多个主键,多个主键则称为联合主键。result 是配置 POJO 到 SQL 列名的映射关系。这里的 result 和 id 两个元素都有如表 4-5 所示的属性。

表 4-5 result 元素和 id 元素的属性

100-1

此外还有 association、collection 和 discriminator 这些元素,我们将在级联那里详细讨论它们的运用方法。

4.7.2 使用 map 存储结果集

一般而言,任何的 select 语句都可以使用 map 存储,如代码清单 4-20 所示。

代码清单 4-20:使用 map 作为存储结果

<select id="findColorByNote" parameterType="string" resultType="map">
        select id, color, note from t_color where note like concat('%', #{note}, '%')
</select>

使用 map 原则上是可以匹配所有结果集的,但是使用 map 接口就意味着可读性的下降,所以这不是一种推荐的方式。更多的时候我们使用的是 POJO 的方式。

4.7.3 使用 POJO 存储结果集

使用 map 方式就意味着可读性的丢失。POJO 是我们最常用的方式,也是我们推荐的方式。一方面我们可以使用自动映射,正如 select 语句里论述的一样。我们还可以使用 select 语句的属性 resultMap 配置映射集合,只是使用前需要配置类似的 resultMap,如代码清单 4-21 所示。

代码清单 4-21:配置 resultMap

<resultMap id="roleResultMap" type="com.learn.chapter4.pojo.Role">
    <id property="id" column="id" />
    <result property="roleName" column="role_name"/>
    <result property="note" column="note"/>
</resultMap>

resultMap 元素的属性 id 代表这个 resultMap 的标识,type 代表着你需要映射的 POJO。我们可以使用 MyBatis 定义好的类的别名,也可以使用自定义的类的全限定名。

映射关系中,id 元素表示这个对象的主键,property 代表着 POJO 的属性名称,column 表示数据库 SQL 的列名,于是 POJO 就和数据库 SQL 的结果一一对应起来了。接着我们在映射文件中的 select 元素里面做如代码清单 4-22 所示的配置,便可以使用了。

代码清单 4-22:使用定义好的 resultMap

<select parameterType= "long "id="getRole" resultMap = "roleResultMap" >
        select id, role_name, note from t_role where id =#{id }
</select>

我们可以发现 SQL 语句的列名和 roleResultMap 的 column 是一一对应的。使用 XML 配置的结果集,我们还可以配置 typeHandler、javaType、jdbcType,但是这条语句配置了 resultMap 就不能再配置 resultType 了。

4.7.4 级联

在数据库中包含着一对多、一对一的关系,比方说一个角色可以分配给多个用户,也可以只分配给一个用户。有时候我们希望角色信息和用户信息一起显示出来,这个是很常见的场景,所以会经常遇见这样的 SQL,如代码清单 4-23 所示。

代码清单 4-23:查询角色包含用户 SQL

Select r.*, u.* from t_role r inner join t_user_role ur 
on r.id = ur.id inner join t_user u on ur.user_id = u.id
where r.id = #{id}

这里的查询是把角色和用户的信息都查询出来,我们希望的是在角色的信息中多一个属性,即 List<UserBean> userList 这样取出 Role 的同时也可以访问到它下面的用户了。我们把这样的情况叫作级联。

在级联中存在 3 种对应关系。其一,一对多的关系,如角色与用户的关系。举个通俗的例子,一家软件公司存在许多软件工程师,公司和软件工程师就是一对多的关系。其二,一对一的关系。例如,每个软件工程师都有一个编号(ID),这是它在软件公司的标识,它与工程师是一对一的关系。其三,多对多的关系。例如,有些公司一个角色可以对应多个用户,但是一个用户也可以兼任多个角色。通俗而言,一个人可以既是总经理,同时也是技术总监,而技术总监这个职位可以对应多个人,这就是多对多的关系。

在实际中,多对多的关系应用不多,因为它比较复杂,会增加理解和关联的复杂度。推荐的方法是,用一对多的关系把它分解为双向关系,以降低关系的复杂度,简化程序。有时候我们也需要鉴别关系,比如我们去体检,男女有别,男性和女性的体检项目并不完全一样,如果让男性去检查妇科项目,就会闹出笑话来。

所以在 MyBatis 中级联分为这么 3 种:association、collection 和 discriminator,下面分别介绍下。

  • association,代表一对一关系,比如中国公民和身份证是一对一的关系。

  • collection,代表一对多关系,比如班级和学生是一对多的关系,一个班级可以有多个学生。

  • discriminator,是鉴别器,它可以根据实际选择采用哪个类作为实例,允许你根据特定的条件去关联不同的结果集。比如,人有男人和女人。你可以实例化一个人的对象,但是要根据情况用男人类或者用女人类去实例化。

为了方便讲解,我们来建这样一系列数据库表,它们的模型关系,如图 4-2 所示,我们将以这个例子来讲解 MyBatis 的 resultMap 的级联。

103_0001

图 4-2 学生信息级联模型关系

学生信息级联模型关系是一个多种类型关联关系,包含了上述的 3 种情况,其中学生表是我们关注的中心,学生证表和它是一对一的关联关系;而学生表和课程成绩表是一对多的关系,一个学生可能有多门课程;课程表和课程成绩表也是一对多的关系;学生有男有女,而健康项目也有所不一,所以女性学生和男性学生的健康表也会有所不同,这些是根据学生的性别来决定的,而鉴别学生性别的就是鉴别器,本书的附录 A 部分给出了对应的建表语句 SQL。

4.7.4.1 association 一对一级联

实际操作中,首先我们需要确定对象的关系。仍然以学生信息级联为例,在学校里面学生(Student)和学生证(Selfcard)是一对一的关系,因此,我们建立一个 StudentBean 和 StudentSelfcardBean 的 POJO 对象。那么在 Student 的 POJO 我们就应该有一个类型为 StudentSelfcardBean 的属性 studentSelfcard,这样便形成了级联。

这时候我们需要建立 Student 的映射器 StudentMapper 和 StudentSelfcard 的映射器 StudentSelfcardMapper。而在 StudentSelfcardMapper 里面我们提供了一个 findStudentSelfcardByStudentId 的方法,如代码清单 4-24 所示。

代码清单 4-24:StudentSelfcardMapper 映射器

<?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.learn.chapter4.mapper.StudentSelfcardMapper">
    <resultMap id="studentSelfcardMap" type="com.learn.chapter4.po.StudentSelfcardBean">
        <id property="id" column="id"/>
        <result property="studentId" column="student_id"/>
        <result property="native_" column="native"/>
        <result property="issueDate" column="issue_date"/>
        <result property="endDate" column="end_date"/>
        <result property="note" column="note"/>
    </resultMap>

    <select id="findStudentSelfcardByStudentId" parameterType="int" resultMap="studentSelfcardMap">
        select id, student_id, native, issue_date, end_date, note
        from t_student_selfcard where student_id = #{studentId}
    </select>
</mapper>

有了以上代码,我们将可以在 StudentMapper 里面使用 StudentSelfcardMapper 进行级联,如代码清单 4-25 所示。

代码清单 4-25:StudentMapper 使用 StudentSelfcardMapper 进行级联

<?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.learn.chapter4.mapper.StudentMapper">
    <resultMap id="studentMap" type="com.learn.chapter4.po.StudentBean">
        <id property="id" column="id" />
        <result property="cnname" column="cnname"/>
        <result property="sex" column="sex" jdbcType="INTEGER"
                javaType="com.learn.chapter4.enums.SexEnum"
                typeHandler="com.learn.chapter4.typehandler.SexTypeHandler"/>
        <result property="note" column="note"/>
        <association property="studentSelfcard" column ="id" select = "com.learn.chapter4.mapper.StudentSelfcardMapper.findStudentSelfcardByStudentId"/>
    </resultMap>
    <select id="getStudent" parameterType="int" resultMap="studentMap">
        select id, cnname, sex, note from t_student where id =#{id}
    </select>
</mapper>

请看上面加粗的代码,这是通过一次关联来处理问题。其中 select 元素由指定的 SQL 去查询,而 column 则是指定传递给 select 语句的参数。这里是 StudentBean 对象的 id。当取出 Student 的时候,MyBatis 就会知道用下面的 SQL 取出我们需要的级联信息。

com.learn.chapter4.mapper.StudentSelfcardMapper.findStudentSelfcardByStudentId

其中参数是 Student 的 id 值,通过 column 配置,如果是多个参数,则使用逗号分隔。让我们测试一下代码,如代码清单 4-26 所示。

代码清单 4-26:测试 association 级联

SqlSession sqlSession = null;
try {
     sqlSession = SqlSessionFactoryUtil.openSqlSession();
     StudentMapper stuMapper = sqlSession.getMapper(StudentMapper.class);
     StudentBean stu = stuMapper.getStudent(1);
} finally {
    if (sqlSession != null) {
        sqlSession.close();
    }
}

接下来运行这个程序打印日志。

......
DEBUG 2016-03-16 14:52:18,739 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4 Connection@6e0e048a]
DEBUG 2016-03-16 14:52:18,739 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-16 14:52:18,776 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-16 14:52:18,797 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, native, issue_date, end_date, note from t_student_selfcard where student_id = ? 
DEBUG 2016-03-16 14:52:18,807 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 14:52:18,814 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 14:52:18,815 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==      Total: 1
DEBUG 2016-03-16 14:52:18,816 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4 Connection@6e0e048a]
.....

我们看到了整个执行的过程,它先查询出 Student 的信息,然后根据其 id 查询出学生证的信息,而参数是 StudentBean 对象的 id 值。这样当我们查找到了 Student 的时候,便能把其学生证的信息也同时取到,这便是一对一的级联。

4.7.4.2 collection 一对多级联

这是一个一对多的级联,一个学生可能有多门课程,在学生确定的前提下每一门课程都会有自己的分数,所以每一个学生的课程成绩只能对应一门课程。所以这里有两个级联,一个是学生和课程成绩的级联,这是一对多的关系;一个是课程成绩和课程的级联,这是一对一的关系。一对一的级联我们使用的是 association,而一对多的级联我们使用的是 collection。

这个时候我们需要建立一个 LectureBean 的 POJO 来记录课程,而学生课程表则建立一个 StudentLectureBean 来记录成绩,里面有一个类型为 LectureBean 属性的 lecture,用来记录学生成绩,操作方法如代码清单 4-27 所示。

代码清单 4-27:LectureBean 和 StudentLectureBean 设计

public class LectureBean {
    private Integer id;
    private String lectureName;
    private String note;
    ......setter and getter......
}
#############################################################
public class StudentLectureBean {
    private int id;
    private Integer studentId;
    private LectureBean lecture;
    private BigDecimal grade;
    private String note;
    ......setter and getter......
}

StudentLectureBean 包含一个 lecture 属性用来读取的课程信息,用 4.7.4.1 节的 association 做一对一级联即可。为了能够读入到 StudentBean 里,我们需要在 StudentBean 里面增加一个类型为 List<StudentLectureBean>的属性 studentLectureList,用来保存学生课程成绩信息。这个时候我们需要使用 collection 级联,如代码清单 4-28 所示。

代码清单 4-28:使用 collection 做一对多级联

###################StudentMapper.xml#####################
<?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.learn.chapter4.mapper.StudentMapper">
    <resultMap id="studentMap" type="com.learn.chapter4.po.StudentBean">
        <id property="id" column="id" />
        <result property="cnname" column="cnname"/>
        <result property="sex" column="sex" jdbcType="INTEGER"
                javaType="com.learn.chapter4.enums.SexEnum"
                typeHandler="com.learn.chapter4.typehandler.SexTypeHandler"/>
        <result property="note" column="note"/>
        <association property="studentSelfcard" column ="id" select = "com.learn.chapter4.mapper.StudentSelfcardMapper.findStudentSelfcardByStudentId"/>
        <collection property="studentLectureList" column="id" select = "com.learn.chapter4.mapper.StudentLectureMapper.findStudentLectureByStuId"/>
    </resultMap>

    <select id="getStudent" parameterType="int" resultMap="studentMap">
        select id, cnname, sex, note from t_student where id =#{id}
    </select>
</mapper>
#####################StudentLectureMapper.xml###########################
<?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.learn.chapter4.mapper.StudentLectureMapper">
    <resultMap id="studentLectureMap" type="com.learn.chapter4.po. StudentLectureBean">
        <id property="id" column="id" />
        <result property="studentId" column="student_id"/>
        <result property="grade" column="grade"/>
        <result property="note" column="note"/>
        <association property="lecture" column ="lecture_id" select="com.learn.chapter4.mapper.LectureMapper.getLecture"/>
    </resultMap>
    <select id="findStudentLectureByStuId" parameterType="int" resultMap ="studentLectureMap">
        select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = #{id}
    </select>
</mapper>
###################LectureMapper.xml####################################
<?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.learn.chapter4.mapper.LectureMapper">
    <select id="getLecture" parameterType="int" resultType="com.learn. chapter4.po.LectureBean">
    select id, lecture_name as lectureName, note from t_lecture where id =#{id}
    </select>
</mapper>

我们看到 StudentMapper.xml 用 collection 去关联 StudentLectureBean,其中 column 对应 SQL 的列名,这里是用 id,属性是 Student 的 studentLectureList,而配置的 select 为 com.learn.chapter4.mapper.StudentLectureMapper.findStudentLectureByStuId,那么 MyBatis 就会启用这条语句来加载数据。我们用 StudentLectureBean 去级联 LectureBean 信息,它使用了列 lecture_id 作为参数,用对应的 select 语句进行加载。

我们可以测试一下结果,如代码清单 4-29 所示。

代码清单 4-29:测试一对多级联 collection

Logger logger = Logger.getLogger(Chapter4Main.class);
SqlSession sqlSession =null;
try {
    sqlSession = SqlSessionFactoryUtil.openSqlSession();
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    StudentBean student = studentMapper.getStudent(1);
    logger.info(student.getStudentSelfcard().getNative_());
    StudentLectureBean studentLecture = student.getStudentLectureList().get(0);
    LectureBean lecture = studentLecture.getLecture();
    logger.info(student.getCnname() + "\t" + lecture.getLectureName()
         + "\t" + studentLecture.getGrade());
} finally {
     if (sqlSession != null) {
         sqlSession.close();
     }
}

这样我们就可以看到代码运行的日志了:

......
DEBUG 2016-03-16 16:06:02,811 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Conne ction@43814d18]
DEBUG 2016-03-16 16:06:02,815 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-16 16:06:02,846 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-16 16:06:02,878 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, native, issue_date, end_date, note from t_student_selfcard where student_id = ? 
DEBUG 2016-03-16 16:06:02,878 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = ? 
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ======>  Preparing: select id, lecture_name as lectureName, note from t_lecture where id =? 
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ======> Parameters: 1(Integer)
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <======      Total: 1
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 16:06:02,888 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==      Total: 1
 INFO 2016-03-16 16:06:02,888 com.learn.chapter4.main.Chapter4Main: 广西南宁
 INFO 2016-03-16 16:06:02,888 com.learn.chapter4.main.Chapter4Main: learn       高数上     92.00

.....

我们打出了学生课程成绩信息,代码运行成功了。我们成功地从学生成绩表里取出了对应的学生成绩,而通过学生成绩表里面的课程 id,获得了课程的信息。

4.7.4.3 discriminator 鉴别器级联

鉴别器级联是在特定的条件下去使用不同的 POJO。比如本例中要了解学生的健康情况,如果是男生总不能去了解他的女性生理指标,这样会闹出笑话来的,同样去了解女生的男性生理指标也是个笑话。这个时候我们就需要用鉴别器了。

我们可以根据学生信息中的性别属性进行判断去关联男性的健康指标或者是女性的健康指标,然后进行关联即可,在 MyBatis 中我们采用的是鉴别器 discriminator,由它来处理这些需要鉴别的场景,它相当于 Java 语言中的 switch 语句。让我们看看它是如何实现的。首先,我们需要新建两个健康情况的 POJO,即 StudentHealthMaleBean 和 StudentHealthFemaleBean,分别存储男性和女性的基础信息,因此我们有了两个 StudtentBean 的子类:MaleStudentBean 和 FemeleStudentBean,让我们先看看它们的设计,如代码清单 4-30 所示。

代码清单 4-30:男女学生类设计

/****男学生****/
public class MaleStudentBean extends StudentBean {
private List<StudentHealthMaleBean> studentHealthMaleList = null;
/*****setter and getter*****/
}
/****女学生****/
public class FemaleStudentBean extends StudentBean {

private List<StudentHealthFemaleBean> studentHealthFemaleList = null;
/*****setter and getter*****/
}

然后,鉴别是男学生还是女学生。因此,我们找学生信息就要根据 StudentBean 的属性 sex 来确定是使用男学生(MaleStudentBean)还是女学生(FemaleStudentBean)的对象了。下面让我们看看如何使用 discriminator 级联来完成这个功能,如代码清单 4-31 所示。

代码清单 4-31:discriminator 的使用

<?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.learn.chapter4.mapper.StudentMapper">
        <resultMap id="studentMap" type="com.learn.chapter4.po.StudentBean">
                <id property="id" column="id" />
                <result property="cnname" column="cnname" />
                <result property="sex" column="sex" jdbcType="INTEGER"
                        javaType="com.learn.chapter4.enums.SexEnum" typeHandler="com.learn.chapter4.typehandler.SexTypeHandler" />
                <result property="note" column="note" />
                <association property="studentSelfcard" column="id"
                        select="com.learn.chapter4.mapper.StudentSelfcardMapper.findStudentSelfcardByStudentId" />
                <collection property="studentLectureList" column="id"
                        select="com.learn.chapter4.mapper.StudentLectureMapper.findStudentLectureByStuId" />
                <discriminator javaType="int" column="sex">
                        <case value="1" resultMap="maleStudentMap" />
                        <case value="2" resultMap="femaleStudentMap" />
                </discriminator>
        </resultMap>

        <select id="getStudent" parameterType="int" resultMap="studentMap">
                select id, cnname, sex, note from t_student where id =#{id}
        </select>

        <resultMap id="maleStudentMap" type="com.learn.chapter4.po.MaleStudentBean" extends="studentMap">
                <collection property="studentHealthMaleList" select="com.learn. chapter4.mapper.StudentHealthMaleMapper.findStudentHealthMaleByStuId" column="id" />
        </resultMap>

        <resultMap id="femaleStudentMap" type="com.learn.chapter4.po. FemaleStudentBean" extends="studentMap">
                <collection property="studentHealthFemaleList" select="com.learn. chapter4.mapper.StudentHealthFemaleMapper.findStudentHealthFemaleByStuId" column="id" />
        </resultMap>
</mapper> 

好了,大段的代码忽略吧,让我们看看加粗的代码。首先我们定义了一个 discriminator 元素,它对应的列(column)是 sex,对应的 java 类型(jdbcType)为 int,所以才有了下面这行代码。

<discriminator javaType="int" column="sex">

接着,我们配置了 case,这里类似 switch 语句。这样我们就可以在 case 里面引入 resultMap。当 sex=1(男性)时,引入的是 maleStudentMap;当 sex=2(女性)时,引入的是 femaleStudentMap,然后我们分别对这两个 resultMap 进行定义。

这两个 resultMap 的定义是大同小异,它们都扩展了原有的 studentMap,所以有了下面这行代码。

extends="studentMap"

正如类的继承关系一样,resultMap 也可以继承,再加入自己的属性。男学生是 studentHealthMaleList,女学生是 studentHealthFemaleList,它们都通过一对多的方式进行关联。

这样配置的结果就是当 Student 表中 sex=1 时,使用 MaleStudentBean 去匹配结果,然后使用 maleStudentMap 中配置的 collection 去获取对应的男学生的健康指标;同样,当 sex=2 时,使用 FemaleStudentBean 去匹配结果,然后用 femaleStudentMap 配置的 collection 去获取女学生的健康指标。只是无论性别如何,他们都是学生,因为 MaleStudentBean 和 FemaleStudentBean 都属于 StudentBean。

与男女学生的健康情况相关的 Bean 和 Mapper 都很简单,这里限于篇幅就不赘述了,请读者自己试一试。

让我们测试一下这个级联,代码清单 4-26 并不需要修改,运行结果如下。

......
DEBUG 2016-03-16 17:52:56,875 org.apache.ibatis.transaction.jdbc. JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4C onnection@50b494a6]
DEBUG 2016-03-16 17:52:56,875 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-16 17:52:56,927 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-16 17:52:56,949 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: SELECT id, student_id as studentId, check_date as checkDate, heart, liver, spleen, lung, kidney, prostate, note FROM t_student_health_male where student_id = ? 
DEBUG 2016-03-16 17:52:56,949 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 17:52:56,949 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 17:52:56,949 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, native, issue_date, end_date, note from t_student_selfcard where student_id = ? 
DEBUG 2016-03-16 17:52:56,949 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 17:52:56,959 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 17:52:56,959 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = ? 
DEBUG 2016-03-16 17:52:56,959 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-16 17:52:56,969 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ======>  Preparing: select id, lecture_name as lectureName, note from t_lecture where id =? 
DEBUG 2016-03-16 17:52:56,969 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ======> Parameters: 1(Integer)
DEBUG 2016-03-16 17:52:56,969 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <======      Total: 1
DEBUG 2016-03-16 17:52:56,969 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-16 17:52:56,969 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==      Total: 1
 INFO 2016-03-16 17:52:56,969 com.learn.chapter4.main.Chapter4Main: 广西南宁
 INFO 2016-03-16 17:52:56,969 com.learn.chapter4.main.Chapter4Main: learn       高数上     92.00
......

4.7.4.4 性能分析和 N+1 问题

级联的优势是能够方便快捷地获取数据。比如学生和学生成绩信息往往是最常用关联的信息,这个时候级联是完全有必要的。多层关联时,建议超过三层关联时尽量少用级联,因为不仅用处不大,而且会造成复杂度的增加,不利于他人的理解和维护。同时级联时也存在一些劣势。有时候我们并不需要获取所有的数据。例如,我只对学生课程和成绩感兴趣,我就不用取出学生证和健康情况表了。因为取出学生证和健康情况表不但没有意义,而且会多执行几条 SQL,导致性能下降。我们可以使用代码去取代它。

级联还有更严重的问题,假设有表关联到 Student 表里面,那么可以想象,我们还要增加级联关系到这个结果集里,那么级联关系将会异常复杂。如果我们采取类似默认的场景那么有一个关联我们就要多执行一次 SQL,正如我们上面的例子一样,每次取一个 Student 对象,那么它所有的信息都会被取出来,这样会造成 SQL 执行过多导致性能下降,这就是 N+1 的问题,为了解决这个问题我们应该考虑采用延迟加载的功能。

4.7.4.5 延迟加载

为了处理 N+1 的问题,MyBatis 引入了延迟加载的功能,延迟加载功能的意义在于,一开始并不取出级联数据,只有当使用它了才发送 SQL 去取回数据。正如我们的例子,我开始取出学生的情况,但是当前并未取出学生成绩和学生证信息。此时我对学生成绩感兴趣,于是我访问学生成绩,这个时候 MyBatis 才会去发送 SQL 去取出学生成绩的信息。这是一个按需取数据的样例。这才是符合我们需要的场景。

在 MyBatis 的配置中有两个全局的参数 lazyLoadingEnabled 和 aggressiveLazy Loading。lazyLoadingEnabled 的含义是是否开启延迟加载功能。aggressiveLazyLoading 的含义是对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将按需加载。lazyLoadingEnabled 是好理解的,而 aggressiveLazyLoading 则不是那么好理解了,别担心,它们很有趣,我们将在下面讨论它们,这样读者便能理解它们的机制了。下面我们将以代码清单 4-32 作为例子进行延迟加载的测试工作。

代码清单 4-32:测试延迟加载

sqlSession = SqlSessionFactoryUtil.openSqlSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
StudentBean student = studentMapper.getStudent(1);
student.getStudentLectureList();

此时让我们配置它们:settings 元素里面的 lazyLoadingEnabled 值开启延迟加载,使得关联属性都按需加载,而不自动加载。要知道在默认的情况下它是即时加载的,一旦关联多,那将造成不少性能问题啊!为了改变它,我们可以把 MyBatis 文件的内容配置为延迟加载,如代码清单 4-33 所示。

代码清单 4-33:把 MyBatis 配置文件从默认改为延迟加载

<settings>
  ......
  <setting name="lazyLoadingEnabled" value="true"/>
  ......
</settings>

此时再运行一下代码清单 4-28,我们便可以看到下面的结果了。

.......
DEBUG 2016-03-23 14:25:28,390 org.apache.ibatis.datasource.pooled. PooledDataSource: Created connection 671046933.
DEBUG 2016-03-23 14:25:28,392 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-23 14:25:28,426 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-23 14:25:28,513 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ====>  Preparing: SELECT id, student_id as studentId, check_date as checkDate, heart, liver, spleen, lung, kidney, prostate, note FROM t_student_health_male where student_id = ? 
DEBUG 2016-03-23 14:25:28,514 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-23 14:25:28,516 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-23 14:25:28,532 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <==      Total: 1
DEBUG 2016-03-23 14:25:34,693 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = ? 
DEBUG 2016-03-23 14:25:34,694 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-23 14:25:34,716 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <==      Total: 1
DEBUG 2016-03-23 14:25:34,718 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, student_id, native, issue_date, end_date, note from t_student_selfcard where student_id = ? 
DEBUG 2016-03-23 14:25:34,718 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-23 14:25:34,724 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <==      Total: 1
......

我们从日志中可以知道,当访问学生信息的时候,我们已经把其健康的情况也查找出来了;当我们访问其课程信息的时候,系统同时也把其学生证信息查找出来了。为什么是这样的一个结果呢?那是因为在默认的情况下 MyBatis 是按层级延迟加载的,让我们看看这个延迟加载的层级,如图 4-3 所示。

117_0001

图 4-3 延迟加载的层级

当我们加载学生信息的时候,它会根据鉴别器去找到健康的情况。而当我们访问课程成绩的时候,由于学生证和课程成绩是一个层级,它也会去访问学生证的信息。然而这并不是我们需要的,因为我们并不希望在访问学生成绩的时候去加载学生证的信息。那么这个时候 aggressiveLazyLoading 就可以用起来了,当它为 true 的时候,MyBatis 的内容按层级加载,否则就按我们调用的要求加载。所以这个时候我们修改一下 MyBatis 配置文件中的代码,在 settings 元素内加入下面这行代码。

 <setting name="aggressiveLazyLoading" value="false"/>

在默认的情况下 aggressiveLazyLoading 的默认值为 true,也就是使用层级加载的策略,我们这里把它修改为了 false。我们重新测试代码清单 4-32 得到日志如下。

DEBUG 2016-03-23 14:58:24,882 org.apache.ibatis.datasource.pooled. PooledDataSource: Created connection 36333492.
DEBUG 2016-03-23 14:58:24,884 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-23 14:58:24,904 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-23 14:58:25,004 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <==      Total: 1
DEBUG 2016-03-23 14:58:25,005 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = ? 
DEBUG 2016-03-23 14:58:25,005 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-23 14:58:25,023 org.apache.ibatis.logging.jdbc. BaseJdbcLogger: <==      Total: 1

我们发现这个时候它就完全按照我们的需要去延迟加载数据了,这就是我们想要的。那么 aggressiveLazyLoading 参数的含义,读者应该能够理解了。

上面的是全局的设置,但是还是不太灵活的,为什么呢?因为我们不能指定到哪些属性可以立即加载,哪些属性可以延迟加载。当一个功能的两个对象经常需要一起用时,我们采用即时加载更好,因为即时加载可以多条 SQL 一次性发送,性能高。例如,学生和学生课程成绩。当遇到类似于健康和学生证的情况时,则用延迟加载好些,因为健康表和学生证表可能不需要经常访问。这样我们就要修改 MyBatis 全局默认的延迟加载功能。不过不必担心,MyBatis 可以很容易地解决这些问题,因为它也有局部延迟加载的功能。我们可以在 association 和 collection 元素上加入属性值 fetchType 就可以了,它有两个取值范围,即 eager 和 lazy。它的默认值取决于你在配置文件 settings 的配置。假如我们没有配置它,那么它们就是 eager。一旦你配置了它们,那么全局的变量就会被它们所覆盖,这样我们就可以灵活地指定哪些东西可以立即加载,哪些东西可以延迟加载,很灵活。例如,我们希望学生成绩是即时加载而学生证是延迟加载,如代码清单 4-34 所示。

代码清单 4-34:及时加载和延迟加载的混合使用

<association property="studentSelfcard" column="id" fetchType="lazy"
select="com.learn.chapter4.mapper.StudentSelfcardMapper.findStudentSelfcardByStudentId" />
<collection property="studentLectureList" column="id" fetchType="eager"         select="com.learn.chapter4.mapper.StudentLectureMapper.findStudentLectureByStuId" />

在测试这段代码的时候我们把 aggressiveLazyLoading 设置为 false,然后运行代码,这时我们就可以发现再取出 Student 对象时,课程成绩是一并取出的,而学生证不会马上取出。这样学生证信息就是延迟加载,而学生成绩是即时加载。同样我们也可以使健康情况延迟加载,如代码清单 4-35 所示。

代码清单 4-35:修改健康情况为延迟加载

<resultMap id="maleStudentMap" type="com.learn.chapter4.po.MaleStudent Bean" extends="studentMap">
                <collection fetchType="lazy"  property="studentHealthMaleList" select="com.learn.chapter4.mapper.StudentHealthMaleMapper.findStudentHealthMaleByStuId" column="id" />
</resultMap>
        <resultMap id="femaleStudentMap" type="com.learn.chapter4.po.Female StudentBean" extends="studentMap">
                <collection fetchType="lazy" property="studentHealthFemaleList" select="com.learn.chapter4.mapper.StudentHealthFemaleMapper.findStudentHealthFemaleByStuId" column="id" />
        </resultMap> 

我们把课程成绩和课程表的级联修改为 lazy,那么当我们获取 StudentBean 对象的时候只有学生信息表和成绩表的信息被我们获取,这是我们最常用的功能,其他功能都被延迟加载了,这便是我们需要的功能。让我们看看运行的日志。

......
DEBUG 2016-03-18 01:20:46,103 org.apache.ibatis.transaction.jdbc.JdbcT ransaction: Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@42110406]
DEBUG 2016-03-18 01:20:46,105 org.apache.ibatis.logging.jdbc.Base JdbcLogger: ==>  Preparing: select id, cnname, sex, note from t_student where id =? 
DEBUG 2016-03-18 01:20:46,139 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 1(Integer)
DEBUG 2016-03-18 01:20:46,206 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture where student_id = ? 
DEBUG 2016-03-18 01:20:46,206 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ====> Parameters: 1(Integer)
DEBUG 2016-03-18 01:20:46,220 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <====      Total: 1
DEBUG 2016-03-18 01:20:46,221 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==      Total: 1
DEBUG 2016-03-18 01:20:46,222 org.apache.ibatis.transaction.jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.jdbc. JDBC4Connection@42110406]
......

我们看到仅仅有两条 SQL 被执行,一条是查询 StudentBean 的基本信息,另外一条是查询成绩的。我们访问延迟加载对象时,它才会发送 SQL 到数据库把数据加载回来。

也许读者对延迟加载感兴趣,延迟加载的实现原理是通过动态代理来实现的。在默认情况下,MyBatis 在 3.3 或者以上版本时,是采用 JAVASSIST 的动态代理,低版本用的是 CGLIB。当然你可以使用配置修改。有兴趣的读者可以参考第 6 章关于动态代理的内容,它会生成一个动态代理对象,里面保存着相关的 SQL 和参数,一旦我们使用这个代理对象的方法,它会进入到动态代理对象的代理方法里,方法里面会通过发送 SQL 和参数,就可以把对应的结果从数据库里查找回来,这便是其实现原理。

4.7.4.6 另一种级联

MyBatis 还提供了另外一种级联方式,这种方式更为简单和直接,也没有 N+1 的问题。首先,让我们用一条 SQL 查询出所有的学生信息,如代码清单 4-36 所示。

代码清单 4-36:一条 SQL 查询出所有的学生信息

<select id="findAllStudent" resultMap="studentMap2">
        SELECT s.id, s.cnname, s.sex, s.note AS snote,
        IF (sex = 1, shm.id, shf.id) AS hid, 
        IF (sex = 1, shm.check_date, shf.check_date) AS check_date,
        IF (sex = 1, shm.heart, shf.heart) AS heart, 
        IF (sex = 1, shm.liver, shf.liver) AS liver,
        IF (sex = 1, shm.spleen, shf.spleen) AS spleen,
        IF (sex = 1, shm.lung, shf.lung) AS lung,
        IF (sex = 1, shm.kidney, shf.kidney) AS kidney, 
        IF (sex = 1, shm.note, shf.note) AS shnote,
        shm.prostate, shf.uterus,
        ss.id AS ssid, ss.native AS native_,
        ss.issue_date, ss.end_date, ss.note AS ssnote,
        sl.id as slid,
        sl.lecture_id, sl.grade, sl.note AS slnote,
        l.lecture_name, l.note AS lnote
        FROM t_student s
        LEFT JOIN t_student_health_maleshm ON s.id = shm.student_id
        LEFT JOIN t_student_health_femaleshf ON s.id = shf.student_id
        LEFT JOIN t_student_selfcardss ON s.id = ss.student_id
        LEFT JOIN t_student_lecturesl ON s.id = sl.student_id
        LEFT JOIN t_lecture l ON sl.lecture_id = l.id
</select>

这条 SQL 的含义就是尽量通过左连接(LEFT JOIN)找到其他学生的信息。于是它返回的结果就包含了所有的学生信息。MyBatis 允许我们通过配置来完成这些级联信息。正如在这里配置的一样,我们将通过 studentMap2 定义的映射规则,来完成这个功能,因此有必要讨论一下它,让我们先看看 studentMap2 的映射集,如代码清单 4-37 所示。

代码清单 4-37:studentMap2 映射集

<resultMap id="studentMap2" type="com.learn.chapter4.po.StudentBean">
                <id property="id" column="id" />
                <result property="cnname" column="cnname" />
                <result property="sex" column="sex" jdbcType="INTEGER"
                        javaType="com.learn.chapter4.enums.SexEnum" typeHandler="com.learn.chapter4.typehandler.SexTypeHandler" />
                <result property="note" column="snote" />
                <association property="studentSelfcard" column="id"
                        javaType="com.learn.chapter4.po.StudentSelfcardBean">
                        <result property="id" column="ssid" />
                        <result property="studentId" column="id" />
                        <result property="native_" column="native_" />
                        <result property="issueDate" column="issue_date" />
                        <result property="endDate" column="end_date" />
                        <result property="note" column="ssnote" />
                </association>

                <collection property="studentLectureList"
                        ofType="com.learn.chapter4.po.StudentLectureBean">
                        <result property="id" column="slid" />
                        <result property="studentId" column="id" />
                        <result property="grade" column="grade" />
                        <result property="note" column="slnote" />
                        <association property="lecture" column="lecture_id"
                                javaType="com.learn.chapter4.po.LectureBean">
                                <result property="id" column="lecture_id" />
                                <result property="lectureName" column="lecture_name" />
                                <result property="note" column="lnote" />
                        </association>
                </collection>

                <discriminator javaType="int" column="sex">
                        <case value="1" resultMap="maleStudentMap2" />
                        <case value="2" resultMap="femaleStudentMap2" />
                </discriminator>
        </resultMap>

        <resultMap id="maleStudentMap2" type="com.learn.chapter4.po. MaleStudentBean" extends="studentMap2">
                <collection property="studentHealthMaleList"  ofType="com.learn. chapter4.po.StudentHealthMaleBean">
                <id property="id" column="hid" javaType="int"/>
                <result property="checkDate" column="check_date" />
                <result property="heart" column="heart" />
                <result property="liver" column="liver" />
                <result property="spleen" column="spleen" />
                <result property="lung" column="lung" />
                <result property="kidney" column="kidney" />
                <result property="prostate" column="prostate" />
                <result property="note" column="shnote" />
                </collection>
        </resultMap>

        <resultMap id="femaleStudentMap2" type="com.learn.chapter4.po. FemaleStudentBean" extends="studentMap2">
                <collection property="studentHealthfemaleList" ofType="com.learn. chapter4.po.StudentHealthFemaleBean">
                <id property="id" column="hid" javaType="int"/>
                <result property="checkDate" column="check_date" />
                <result property="heart" column="heart" />
                <result property="liver" column="liver" />
                <result property="spleen" column="spleen" />
                <result property="lung" column="lung" />
                <result property="kidney" column="kidney" />
                <result property="uterus" column="uterus" />
                <result property="note" column="shnote" />
                </collection>
        </resultMap>

请注意加粗代码,让我们说明一下。

  • 第一个 association 定义的 javaType 属性告诉了 MyBatis,将使用哪个类去映射这些字段。第二个 associationcolumn 定义的是 lecture_id,说明用 SQL 中的字段 lecture_id 去关联结果。

  • ofType 属性定义的是 collection 里面的泛型是什么 Java 类型,MyBatis 会拿你定义的 Java 类和结果集做映射。

  • discriminator 则是根据 sex 的结果来判断使用哪个类做映射。它决定了使用男生健康表,还是女生健康表。

这里我们看到了一条 SQL 就能完成映射,但是这条 SQL 有些复杂。其次我们是否需要在一次查询就导出那么多的数据,这会不会造成资源浪费,同时也给维护带来一定的困难,这些问题都需要读者在实际工作中去考量。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。