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层时就不考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、复杂和难以维护,在现实的使用过程中,要根据实际情况判断使用。按照我的理解,分层查询像一棵树的层次遍历,复杂度随着层次纵向增加而快速增加。而连接查询的复杂度随着表数量的增加而横向扩展。具体使用还需要结合实际情况。

以上结论均基于个人的理解和总结,如果有不当之处还望指正!

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值