最近在做一個基於SpringBoot+MybatisPlus博客系統的項目,在管理後臺需要列出所有文章,效果是這樣的:
注意紅色部分,查出文章的信息時,還需要查文章的分類和文章的標籤。這很容易想到需要使用Mybatis的級聯查詢,但是在寫mapper文件代碼的時候,想到級聯其實有兩種方式:
- 基於分層次查詢的
- 基於SQL表連接的
不瞭解這兩種方式的話,可以先看看我的另一篇博客https://blog.csdn.net/weixin_41297079/article/details/105559358
那麼這兩種方式的區別在哪呢?
首先我們先了解一下數據庫表的結構和對應POJO對象:
數據庫表結構如下:
POJO - Article(文章)如下:
@Data
@TableName("t_article")
public class Article {
@TableId(type = IdType.AUTO)
private Long id;
...
@TableField(exist = false)
private Category category;
@TableField(exist = false)
private List<Tag> tagList;
}
POJO - Category(分類)如下:
@Data
@TableName("t_category")
public class Category {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
...
}
POJO - Tag(標籤)如下:
@Data
@TableName("t_tag")
public class Tag {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
...
}
#POJO - ArticleTag(文章標籤關聯)如下:
@Data
@TableName("t_article_tag")
public class ArticleTag {
@TableId
private Long articleId;
@TableId
private Long tagId;
}
需要特別說明一下的是,這裏用到的@Data
註解是一個叫lombok的插件提供的,使用這個註解作用在類上可以幫我們生成類的getter和setter方法等,因此代碼中不需要寫getter和setter。然後,爲了簡化,我把無關的屬性剔除了。最後,@TableName
、@TableId
和 @TableField
是MyBatisPlus提供的註解,其中@TableName("t_article")
指明該實體類(Article)對應數據庫表t_article,@TableId(type = IdType.AUTO)
指定該屬性(id)是對應表的註解,主鍵策略爲ID自增。@TableField(exist = false)
指明該屬性在表中沒有對應的字段,詳細說明可以查看MybatisPlus官方文檔 https://mp.baomidou.com/guide/annotation.html
從數據庫結構中,我們很明顯可以看到,文章(t_article)和分類(t_category)是多對一的關係,文章(t_article)和標籤(t_tag)是一對多的關係,那麼在配置文章的mapper文件(ArticleMapper)時可以這樣配置,代碼如下:
<?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="site.alanliang.geekblog.mapper.ArticleMapper">
<resultMap id="adminListResultMap" type="site.alanliang.geekblog.domain.Article">
<id property="id" column="id"/>
...
<association property="category" column="category_id"
select="site.alanliang.geekblog.mapper.CategoryMapper.selectByCid"/>
<collection property="tagList" column="id" select="site.alanliang.geekblog.mapper.TagMapper.selectByArticleId"/>
</resultMap>
</mapper>
其中<association>標籤配置了Article與Category的級聯關係,<collection>配置了Article與Tag的級聯關係。說到級聯我們先了解一下它的概念:
級聯是一個數據庫實體的概念。比如文章就需要存在標籤與之對應,這樣就有了文章標籤表,一篇文章可能有多個標籤,這就是一對多的級聯;除此之外,還有一對一的級聯,比如身份證和公民是一對一的關係。
級聯不是必須的,級聯的好處是獲取關聯數據十分便捷,但是級聯過多會增加系統複雜度,同時降低系統的性能。
Mybatis中有3種級聯:
- 鑑別器:它是一個根據某些條件決定採用具體實現類級聯的方案,比如體檢表要根據性別去區分。這裏我們不討論。
- 一對一(association):比如學生證和學生就是一種一對一的級聯,僱員和工牌表也是一種一對一級聯。
- 一對多(collection):比如班級和學生就是一種一對多的級聯。
看到這裏,有的小夥伴可能會疑惑,既然文章和分類是多對一關係,爲啥用association?其實這裏是站在文章的角度看的,一篇文章就對應一個分類,這不就是一對一級聯關係了嗎,但是站在分類的角度上看,一個分類有多篇文章,這就是一對多級聯關係,在代碼中就需要用到collection。總而言之,當POJO類中,其中一個屬性是另外一個類的引用,這就需要association級聯,而一個屬性是一個集合的時候,這就需要collection級聯。
關鍵來了,前面提到,級聯方式有兩種,一種是分層查詢,一種是連接查詢。那麼對t_article進行全表查詢,而且還需要查詢Article的category和tagList,那麼究竟哪個更快呢?假設有100個Article,1個Article有1個Category,1個Article有3個Tag,那麼根據N+1問題,查詢所有數據需要201條SQL(查詢所有Article只需要1條,而查詢Article的Category和Tag分別需要100條),而連接查詢只需要1條,但是將4個表連接起來查詢(t_article,t_category,t_article_tag, t_tag)規模好像也不小,感覺也不會太快,本人弱雞不懂SQL底層,所以沒有直觀感覺。只好做一個測試:
- 分層查詢
<resultMap id="resultMap1" type="site.alanliang.geekblog.domain.Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="category" column="category_id"
select="site.alanliang.geekblog.mapper.ArticleMapper.selectCategoryById"/>
<collection property="tagList" column="id" select="site.alanliang.geekblog.mapper.ArticleMapper.listTagsByArticleId"/>
</resultMap>
<select id="selectCategoryById" resultType="site.alanliang.geekblog.domain.Category">
select id, name from t_category where id = #{id}
</select>
<select id="listTagsByArticleId" resultType="site.alanliang.geekblog.domain.Tag">
select tt.id, tt.name
from t_tag tt
left join t_article_tag tat
on tt.id = tat.tag_id
where tat.article_id = #{articleId}
</select>
<select id="listArticles1" resultMap="resultMap1">
select id, title, category_id
from t_article
</select>
@Test
void listArticles1(){
long startTime = System.currentTimeMillis();
List<Article> articles = articleMapper.listArticles1();
long endTime = System.currentTimeMillis();
System.out.println("-----執行時間爲"+(endTime-startTime)+"ms-----");
System.out.println(articles);
}
3次執行結果:
-----執行時間爲146ms-----
-----執行時間爲143ms-----
-----執行時間爲140ms-----
- 連接查詢
<resultMap id="resultMap2" type="site.alanliang.geekblog.domain.Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="category" javaType="site.alanliang.geekblog.domain.Category">
<id property="id" column="id"/>
<result property="name" column="name"/>
</association>
<collection property="tagList" ofType="site.alanliang.geekblog.domain.Tag">
<id property="id" column="id"/>
<id property="name" column="name"/>
</collection>
</resultMap>
<select id="listArticles2" resultMap="resultMap2">
select ta.id, ta.title, ta.type, ta.comments, ta.views, ta.likes, ta.published, ta.appreciable, ta.commentable, ta.top, ta.recommend, ta.create_time, ta.update_time,
tc.id, tc.name, tt.id, tt.name
from t_article ta
inner join t_category tc
on ta.category_id = tc.id
inner join t_article_tag tat
on ta.id = tat.article_id
inner join t_tag tt
on tat.tag_id = tt.id
</select>
@Test
void listArticles2(){
long startTime = System.currentTimeMillis();
List<Article> articles = articleMapper.listArticles2();
long endTime = System.currentTimeMillis();
System.out.println("-----執行時間爲"+(endTime-startTime)+"ms-----");
System.out.println(articles);
}
-----執行時間爲99ms-----
-----執行時間爲112ms-----
-----執行時間爲97ms-----
這裏的數據確保了每個Article都只有1個Category,每個Article都至少有1個Tag。可以看出連接查詢比分層查詢快了50%左右。
原來的數據是我項目裏,記錄總共大概只有二三十多條,爲了進一步測試,所以再增加記錄1000條。
分層查詢的時間分別是:
-----執行時間爲2187ms-----
-----執行時間爲2275ms-----
-----執行時間爲2165ms-----
連接查詢的時間分別是:
-----執行時間爲230ms-----
-----執行時間爲188ms-----
-----執行時間爲186ms-----
這裏的差距就很明顯了,連接查詢所需要的時間只需要分層查詢的10%,可以看出需要查詢所有數據,進行全表關聯查詢時,連接查詢方式速度更快。但是連接查詢有個很明顯的缺點,就是SQL語句複雜,日後維護起來比較困難。
總結
-
分層查詢
優點:SQL語句簡單,容易理解和維護;
缺點:存在N+1問題,在進行大量數據查詢時效率慢(當然這可以通過延遲加載和分頁等進行優化)
-
連接查詢
優點:消除了N+1問題,在進行大量數據查詢時效率比較高
缺點:SQL語句複雜,不易理解和維護。
總而言之,連接查詢一般用於那些比較簡單且關聯不多的場景下,在這種場景下效率更高。而分層查詢獲取關聯數據十分便捷,但如果層次過多也會增加系統的複雜度,同時降低系統的性能,一般當級聯的層級超過3層時就不考慮使用級聯了,因爲這樣會造成多個對象的關聯,導致系統的耦合、複雜和難以維護,在現實的使用過程中,要根據實際情況判斷使用。按照我的理解,分層查詢像一棵樹的層次遍歷,複雜度隨着層次縱向增加而快速增加。而連接查詢的複雜度隨着表數量的增加而橫向擴展。具體使用還需要結合實際情況。
以上結論均基於個人的理解和總結,如果有不當之處還望指正!