Mybatis中兩種級聯方式的性能對比

最近在做一個基於SpringBoot+MybatisPlus博客系統的項目,在管理後臺需要列出所有文章,效果是這樣的:

avatar

注意紅色部分,查出文章的信息時,還需要查文章的分類和文章的標籤。這很容易想到需要使用Mybatis的級聯查詢,但是在寫mapper文件代碼的時候,想到級聯其實有兩種方式:

  • 基於分層次查詢的
  • 基於SQL表連接的

不瞭解這兩種方式的話,可以先看看我的另一篇博客https://blog.csdn.net/weixin_41297079/article/details/105559358
那麼這兩種方式的區別在哪呢?

首先我們先了解一下數據庫表的結構和對應POJO對象:
數據庫表結構如下:

avatat

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層時就不考慮使用級聯了,因爲這樣會造成多個對象的關聯,導致系統的耦合、複雜和難以維護,在現實的使用過程中,要根據實際情況判斷使用。按照我的理解,分層查詢像一棵樹的層次遍歷,複雜度隨着層次縱向增加而快速增加。而連接查詢的複雜度隨着表數量的增加而橫向擴展。具體使用還需要結合實際情況。

以上結論均基於個人的理解和總結,如果有不當之處還望指正!

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