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 示例-插件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM