簡介
MyBatis 提供了兩種聯合查詢的方式,一種是嵌套查詢,一種是嵌套結果。先說結論:在項目中不建議使用嵌套查詢,會出現性能問題,可以使用嵌套結果。
測試類:com.yjw.demo.JointQueryTest,提供了對嵌套查詢和嵌套結果的測試。
數據庫表模型關系
學生信息級聯模型關系:鏈接
學生信息級聯模型關系是一個多種類型關聯關系,包含了如下幾種情況:
- 其中學生表是我們關注的中心,學生證表和它是一對一的關聯關系;
- 而學生表和課程成績表是一對多的關系,一個學生可能有多門課程;
- 課程表和課程成績表也是一對多的關系;
- 學生有男有女,而健康項目也有所不一,所以女性學生和男性學生的健康表也會有所不同,這些是根據學生的性別來決定的,而鑒別學生性別的就是鑒別器。
關聯關系
在聯合查詢中存在如下幾種對應關系:
- 一對一的關系;
- 一對多的關系;
- 多對多的關系,實際使用過程中是把多對多的關系分解為兩個一對多的關系,以降低關系的復雜度;
- 還有一種是鑒別關系,比如我們去體檢,男女有別,男性和女性的體檢項目並不完全一樣;
所以在 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 實用篇