MyBatis 示例-聯合查詢

簡介

MyBatis 提供了兩種聯合查詢的方式,一種是嵌套查詢,一種是嵌套結果。先說結論:在項目中不建議使用嵌套查詢,會出現性能問題,可以使用嵌套結果。

測試類:com.yjw.demo.JointQueryTest,提供了對嵌套查詢嵌套結果的測試。

數據庫表模型關係

學生信息級聯模型關係:鏈接

image.png

學生信息級聯模型關係是一個多種類型關聯關係,包含了如下幾種情況:

  • 其中學生表是我們關注的中心,學生證表和它是一對一的關聯關係;
  • 而學生表和課程成績表是一對多的關係,一個學生可能有多門課程;
  • 課程表和課程成績表也是一對多的關係;
  • 學生有男有女,而健康項目也有所不一,所以女性學生和男性學生的健康表也會有所不同,這些是根據學生的性別來決定的,而鑑別學生性別的就是鑑別器。

關聯關係

在聯合查詢中存在如下幾種對應關係:

  • 一對一的關係;
  • 一對多的關係;
  • 多對多的關係,實際使用過程中是把多對多的關係分解爲兩個一對多的關係,以降低關係的複雜度;
  • 還有一種是鑑別關係,比如我們去體檢,男女有別,男性和女性的體檢項目並不完全一樣;

所以在 MyBatis 中聯合分爲這麼3種:association、collection 和 discriminator。

  • association:代表一對一關係;
  • collection:代表一對多關係;
  • discriminator:代表鑑別器,它可以根據實際選擇採用哪種類作爲實例,允許你根據特定的條件去關聯不同的結果集;

嵌套查詢(不建議使用)

一對一關係

以學生表作爲關注的中心,學生表和學生證表是一對一的關係。POJO 對象和映射文件的實現如下:

StudentDO

public class StudentDO {

    private Long id;
    private String name;
    private Sex sex;
    private Long selfcardNo;
    private String note;
    private StudentSelfcardDO studentSelfcard;
    // get set 方法
}

StudentMapper.xml

<!-- 聯合查詢:嵌套查詢 -->
<resultMap id="studentMap1" type="studentDO">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="sex" jdbcType="TINYINT" property="sex"
            typeHandler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/>
    <result column="selfcard_no" jdbcType="BIGINT" property="selfcardNo" />
    <result column="note" jdbcType="VARCHAR" property="note" />
    <!-- 嵌套查詢:一對一級聯 -->
    <association property="studentSelfcard" column="{studentId=id}"
                 select="com.yjw.demo.mybatis.biz.dao.StudentSelfcardDao.listByConditions" />
</resultMap>

一對一的關係建立通過 <association> 元素實現,該元素中的屬性描述如下所示:

  • property:JavaBean 中對應的屬性字段;
  • column:數據庫的列名或者列標籤別名。與傳遞給 resultSet.getString(columnName) 的參數名稱相同。注意: 在處理組合鍵時,您可以使用 column= "{prop1=col1,prop2=col2}" 這樣的語法,設置多個列名傳入到嵌套查詢語句。這就會把 prop1 和 prop2 設置到目標嵌套選擇語句的參數對象中;
  • select:通過這個屬性,通過 ID 引用另一個加載複雜類型的映射語句。
  • fetchType: 設置局部延遲加載,它有兩個取值範圍,即 eager 和 lazy。它的默認值取決於你在配置文件settings 的配置,如果沒有配置它,默認是 eager,一旦配置了,全局配置(lazyLoadingEnabled)就會被他們覆蓋;

一對多關係

以學生表作爲關注的中心,學生表和課程表是一對多的關係。POJO 對象和映射文件的實現如下:

StudentDO

public class StudentDO {

    private Long id;
    private String name;
    private Sex sex;
    private Long selfcardNo;
    private String note;
    private StudentSelfcardDO studentSelfcard;
    private List<StudentLectureDO> studentLectures;
    // get set 方法
}

StudentMapper.xml

<!-- 聯合查詢:嵌套查詢 -->
<resultMap id="studentMap1" type="studentDO">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="sex" jdbcType="TINYINT" property="sex"
            typeHandler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/>
    <result column="selfcard_no" jdbcType="BIGINT" property="selfcardNo" />
    <result column="note" jdbcType="VARCHAR" property="note" />
    <!-- 嵌套查詢:一對一級聯 -->
    <association property="studentSelfcard" column="{studentId=id}"
                 select="com.yjw.demo.mybatis.biz.dao.StudentSelfcardDao.listByConditions" />
    
    <!-- 嵌套查詢:一對多級聯 -->
    <collection property="studentLectures" column="{studentId=id}"
                select="com.yjw.demo.mybatis.biz.dao.StudentLectureDao.listByConditions" />
</resultMap>

一對一的關係建立通過 <collection> 元素實現,該元素中的屬性描述和 <association> 元素一樣

鑑別器

以學生表作爲關注的中心,不同性別的學生關聯不同的健康指標。POJO 對象和映射文件的實現如下:

首先,我們需要新建兩個健康情況的 POJO,即 StudentHealthMaleDO和 StudentHealthFemaleDO,分別存儲男性和女性的基礎信息,再新建兩個 StudentDO 的子類:MaleStudentDO 和 FemaleStudentDO,關聯健康情況的 POJO。

/**
 * 男生
 */
public class MaleStudentDO extends StudentDO {

    private List<StudentHealthMaleDO> studentHealthMales;
    // get set 方法
}

/**
 * 女生
 */
public class FemaleStudentDO extends StudentDO {

    private List<StudentHealthFemaleDO> studentHealthFemales;
    // get set 方法
}

StudentMapper.xml

<!-- 聯合查詢:嵌套查詢 -->
<resultMap id="studentMap1" type="studentDO">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="sex" jdbcType="TINYINT" property="sex"
            typeHandler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/>
    <result column="selfcard_no" jdbcType="BIGINT" property="selfcardNo" />
    <result column="note" jdbcType="VARCHAR" property="note" />
    <!-- 嵌套查詢:一對一級聯 -->
    <association property="studentSelfcard" column="{studentId=id}"
                 select="com.yjw.demo.mybatis.biz.dao.StudentSelfcardDao.listByConditions" />
    
    <!-- 嵌套查詢:一對多級聯 -->
    <collection property="studentLectures" column="{studentId=id}"
                select="com.yjw.demo.mybatis.biz.dao.StudentLectureDao.listByConditions" />
    
    <!-- 嵌套查詢:鑑別器 -->
    <!-- discriminator:使用結果值來決定使用哪個 resultMap -->
    <!-- case:基於某些值的結果映射 -->
    <discriminator javaType="int" column="sex">
        <case value="1" resultMap="maleStudentMap1" />
        <case value="2" resultMap="femaleStudentMap1" />
    </discriminator>
</resultMap>

<!-- 男 -->
<resultMap id="maleStudentMap1" type="maleStudentDO" extends="studentMap1">
    <collection property="studentHealthMales" column="{studentId=id}"
                select="com.yjw.demo.mybatis.biz.dao.StudentHealthMaleDao.listByConditions" />
</resultMap>

<!-- 女 -->
<resultMap id="femaleStudentMap1" type="femaleStudentDO" extends="studentMap1">
    <collection property="studentHealthFemales" column="{studentId=id}"
                select="com.yjw.demo.mybatis.biz.dao.StudentHealthFemaleDao.listByConditions" />
</resultMap>

MyBatis 中的鑑別器通過 <discriminator> 元素實現,它對應的列(column)是 sex,對應的 Java 類型(javaType)是 int,case 類似 Java 中的 switch 語句,當 sex=1(男性)時,引入的是 maleStudentMap1,當 sex=2(女性)時,引入的是 femaleStudentMap1,然後我們分別對這兩個 resultMap 定義。

N+1 問題

嵌套查詢存在 N+1 的問題,每次取一個 Student 對象,那麼它所有的信息都會被取出來,這樣會造成 SQL 執行過多導致性能下降。

我們通過日誌信息來看一下嵌套查詢 N+1 的問題:

2019-09-12 15:38:24.717 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : ==>  Preparing: select * from t_student 
2019-09-12 15:38:24.762 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : ==> Parameters: 
2019-09-12 15:38:24.839 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, check_date, heart, liver, spleen, lung, kidney, prostate, note from t_student_health_male WHERE student_id = ? 
2019-09-12 15:38:24.840 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 1(Long)
2019-09-12 15:38:24.843 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 1
2019-09-12 15:38:24.848 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, native_place, issue_date, end_date, note, student_effective from t_student_selfcard WHERE student_id = ? 
2019-09-12 15:38:24.849 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 1(Long)
2019-09-12 15:38:24.852 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 1
2019-09-12 15:38:24.856 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture WHERE student_id = ? 
2019-09-12 15:38:24.857 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 1(Long)
2019-09-12 15:38:24.859 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======>  Preparing: select id, lecture_name, note from t_lecture where id = ? 
2019-09-12 15:38:24.860 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======> Parameters: 1(Long)
2019-09-12 15:38:24.862 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : <======      Total: 1
2019-09-12 15:38:24.864 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======>  Preparing: select id, lecture_name, note from t_lecture where id = ? 
2019-09-12 15:38:24.864 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======> Parameters: 2(Long)
2019-09-12 15:38:24.867 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : <======      Total: 1
2019-09-12 15:38:24.868 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======>  Preparing: select id, lecture_name, note from t_lecture where id = ? 
2019-09-12 15:38:24.869 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : ======> Parameters: 3(Long)
2019-09-12 15:38:24.870 DEBUG 2660 --- [           main] c.y.d.m.b.d.LectureDao.getByPrimaryKey   : <======      Total: 1
2019-09-12 15:38:24.871 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 3
2019-09-12 15:38:24.874 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, check_date, heart, liver, spleen, lung, kidney, uterus, note from t_student_health_female WHERE student_id = ? 
2019-09-12 15:38:24.875 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 2(Long)
2019-09-12 15:38:24.878 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 1
2019-09-12 15:38:24.879 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, native_place, issue_date, end_date, note, student_effective from t_student_selfcard WHERE student_id = ? 
2019-09-12 15:38:24.879 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 2(Long)
2019-09-12 15:38:24.881 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 1
2019-09-12 15:38:24.882 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture WHERE student_id = ? 
2019-09-12 15:38:24.882 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 2(Long)
2019-09-12 15:38:24.886 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 3
2019-09-12 15:38:24.887 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, check_date, heart, liver, spleen, lung, kidney, prostate, note from t_student_health_male WHERE student_id = ? 
2019-09-12 15:38:24.887 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 3(Long)
2019-09-12 15:38:24.893 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 0
2019-09-12 15:38:24.894 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, native_place, issue_date, end_date, note, student_effective from t_student_selfcard WHERE student_id = ? 
2019-09-12 15:38:24.897 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 3(Long)
2019-09-12 15:38:24.899 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 0
2019-09-12 15:38:24.900 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====>  Preparing: select id, student_id, lecture_id, grade, note from t_student_lecture WHERE student_id = ? 
2019-09-12 15:38:24.901 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : ====> Parameters: 3(Long)
2019-09-12 15:38:24.908 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listByConditions           : <====      Total: 0
2019-09-12 15:38:24.909 DEBUG 2660 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : <==      Total: 3

學生數據有3條,在查詢學生證件、學生成績等這些信息的時候,分別執行了3次,每次都用 id = ? 執行,如果學生數據比較多時,嚴重影響性能。

爲了處理嵌套查詢帶來的 N+1 的問題,MyBatis 引入了延遲加載的功能。在 MyBatis 的配置中有兩個全局的參數 lazyLoadingEnabled、aggressiveLazyLoading。

 

我們設置延遲加載的全局開關(lazyLoadingEnabled)爲 true 的時候,當訪問學生信息的時候,MyBatis 已經把學生的健康情況也查詢出來了,當訪問學生的課程信息的時候,MyBatis 同時也把其學生證信息查詢出來了,爲什麼是這樣一個結果呢?因爲在默認情況下 MyBatis 是按層級延遲加載的,讓我們看看這個延遲加載的層級:

 

這不是我們需要的加載數據方式,我們不希望在訪問學生信息的時候去加載學生的健康情況數據。那麼這個時候就需要設置 aggressiveLazyLoading 屬性了,當它爲 true 的時候,MyBatis 的內容按層級加載,否則就按我們調用的要求加載。

這兩項配置既可以在 Spring Boot 配置文件中配置,也可以在 MyBatis  配置文件中配置,在 setting 元素中加入下面的代碼:

<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

按需加載的意思是我們不手動調用對應的屬性,就不會加載。通過執行如下測試代碼來演示一下按需加載的功能:

/**
 * 聯合查詢-嵌套查詢(一對一、一對多、鑑別器)
 *
 * @throws JsonProcessingException
 */
@Test
public void listStudentByNestingQuery() throws JsonProcessingException, InterruptedException {
    List<StudentDO> students = studentDao.listStudentByNestingQuery();
    Thread.sleep(3000L);
    System.out.println("睡眠3秒鐘");
    students.get(0).getStudentSelfcard();
}

在查詢完學生信息的時候,我們睡眠了3秒鐘,再調學生證件信息,來看下日誌的輸出:

2019-09-12 16:24:46.341  INFO 16772 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2019-09-12 16:24:46.355 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : ==>  Preparing: select * from t_student 
2019-09-12 16:24:46.402 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : ==> Parameters: 
2019-09-12 16:24:46.630 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listStudentByNestingQuery  : <==      Total: 3
睡眠3秒鐘
2019-09-12 16:24:49.655 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listByConditions           : ==>  Preparing: select id, student_id, native_place, issue_date, end_date, note, student_effective from t_student_selfcard WHERE student_id = ? 
2019-09-12 16:24:49.659 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listByConditions           : ==> Parameters: 1(Long)
2019-09-12 16:24:49.666 DEBUG 16772 --- [           main] c.y.d.m.b.d.S.listByConditions           : <==      Total: 1

看上面的日誌輸出,延遲加載的配置實現了按需加載的功能。但是嵌套查詢還是不建議使用,因爲不可控,我們不確定哪些操作會導致 N+1 的問題,比如如果我們使用了 JSON 的工具把查出來的學生信息轉成 JSON 字符串的時候,就會導致查詢出學生的所有關聯信息。

/**
 * 聯合查詢-嵌套查詢(一對一、一對多、鑑別器)
 *
 * @throws JsonProcessingException
 */
@Test
public void listStudentByNestingQuery() throws JsonProcessingException, InterruptedException {
    List<StudentDO> students = studentDao.listStudentByNestingQuery();
    // 1.測試延遲加載的效果
    // Thread.sleep(3000L);
    // System.out.println("睡眠3秒鐘");
    // students.get(0).getStudentSelfcard();

    // 2.使用JSON功能轉JSON字符串會導致N+1的問題
    System.out.println(JsonUtils.toJSONString(students));
}

日誌輸出和沒有使用延遲加載配置的效果一樣,其實這裏的配置是沒有問題的,只是 JSON 工具在生成 JSON 字符串的時候,會逐層調用數據,所以就導致了需要把學生的所有關聯信息都查出來。

嵌套結果

MyBatis 還提供了另外一種關聯查詢的方式(嵌套結果),這種方式更爲簡單和直接,沒有 N+1 的問題,因爲它的數據是一條 SQL 查出來的,代碼如下所示。

嵌套結果中的一對一、一對多、鑑別器和嵌套查詢類似,只是不引用外部的 select 語句,屬性都配置在了一個 resultMap 中。

<!-- 聯合查詢:嵌套結果 -->
<resultMap id="studentMap2" type="studentDO">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="selfcardNo" column="selfcard_no"/>
    <result property="note" column="note"/>
    <association property="studentSelfcard" javaType="studentSelfcardDO">
        <result property="id" column="ssid"/>
        <result property="nativePlace" column="native_place"/>
        <result property="issueDate" column="issue_date"/>
        <result property="endDate" column="end_date"/>
        <result property="note" column="ssnote"/>
    </association>
    <collection property="studentLectures" ofType="studentLectureDO">
        <result property="id" column="slid"/>
        <result property="grade" column="grade"/>
        <result property="note" column="slnote"/>
        <association property="lecture" javaType="lectureDO">
            <result property="id" column="lid"/>
            <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="maleStudentDO" extends="studentMap2">
    <collection property="studentHealthMales" ofType="studentHealthMaleDO">
        <id property="id" column="hid"/>
        <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="femaleStudentDO" extends="studentMap2">
    <collection property="studentHealthFemales" ofType="studentHealthFemaleDO">
        <id property="id" column="hid"/>
        <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>

<select id="listStudentByNestingResult" resultMap="studentMap2">
    SELECT s.id,s.name,s.sex,s.note,s.selfcard_no,
        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_place,
        ss.issue_date,ss.end_date,ss.note AS ssnote,
        sl.id AS slid,sl.grade,sl.note AS slnote,
        l.lecture_name,l.note AS lnote
    FROM t_student s
    LEFT JOIN t_student_health_male shm ON s.id=shm.student_id
    LEFT JOIN t_student_health_female shf ON s.id = shf.student_id
    LEFT JOIN t_student_selfcard ss ON s.id = ss.student_id
    LEFT JOIN t_student_lecture sl ON s.id=sl.student_id
    LEFT JOIN t_lecture l ON sl.lecture_id = l.id
</select>

collection 元素中的 ofType 屬性定義的是 collection 裏面的 Java 類型。

 

MyBatis 實用篇

MyBatis 概念

MyBatis 示例-簡介

MyBatis 示例-類型處理器

MyBatis 示例-傳遞多個參數

MyBatis 示例-主鍵回填

MyBatis 示例-動態 SQL

MyBatis 示例-聯合查詢

MyBatis 示例-緩存

MyBatis 示例-插件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章