MyBatis-Plus中默认方法对应的SQL到底长啥样?

我希望成为:自媒体圈中技术最好、实战经验最丰富的达人,技术圈中最会分享的架构师。加油!
我的公众号:Hoeller

过段时间要给公司同事做Mybatis-Plus相关的培训,所以抓紧时间看看Mybatis-Plus的源码,顺便也分享出来让大家看看内容如何,希望大家多给意见。

在用Mybatis-Plus时,我们通常会继承BaseMapper接口,比如:

@Mapper
public interface DaduduMapper extends BaseMapper<MyEntity> {

    @Select("select 1")
    String test();
}

BaseMapper接口中提供了很多增删查改的方法,比如:

public interface BaseMapper<T> extends Mapper<T> {

    int insert(T entity);

    int deleteById(Serializable id);

    T selectById(Serializable id);

    // 我删除了很多
}

我一直有个疑问,为什么我们在执行这些方法时就能执行对应的SQL逻辑,比如执行selectById方法就会真正根据id去查数据并返回,这些方法上并没有使用@Select注解定义SQL,也找不到相应的XML文件,**这些方法对应的SQL到底长什么样?是如何生成的?**这篇文章就来给大家粗浅的分析一下。

上面我们自定义了一个DadududuMapper接口,并自定义了一个test()方法和对应SQL语句:

@Mapper
public interface DaduduMapper extends BaseMapper<MyEntity> {

    @Select("select 1")
    String test();
}

在Mybatis中会解析test()方法以及@Select中的SQL,生成一个MappedStatement对象,该对象长下面这样:
image.png

可以看到,一个MappedStatement对象包含了两个非常重要的部分:

  1. 方法部分:比如id属性、parameterMap属性,表示方法相关的信息
  2. SQL部分:比如sqlSource属性,表示方法对应的SQL语句信息

对于我们自定义的方法,生成这两部分信息是比较自然的,解析方法和SQL就可以了。那对于BaseMapper接口中的方法呢?它的SQL部分信息是如何得来的呢?比如BaseMapper接口中存在一个selectById()方法,这个方法对应的SqlSource对象是如何生成的呢?

在Mybatis-Plus的源码中有这样一段代码:

// 判断type是不是继承了Mapper接口
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
	parserInjector();
}

其中type参数就是我们定义的DadududuMapper接口,而isSupperMapperChildren()方法会判断DadududuMapper接口是不是继承了Mapper接口,如果是,则执行parserInjector()方法。
image.png
由于BaseMapper继承了Mapper接口,所以DadududuMapper接口自然就继承了Mapper接口,所以在解析DadududuMapper接口时会执行parserInjector()方法:

void parserInjector() {
    // 先得到DefaultSqlInjector对象,再执行inspectInject方法
	GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}

parserInjector()方法默认会获取到一个DefaultSqlInjector对象,看名字就知道跟SQL有关了,而且叫做SQL注入器,是不是有点感觉了,它是不是用来生成SQL并注入或绑定到BaseMapper接口中各个方法的?我们继续看inspectInject()方法。

该方法中有几段关键的代码,我分段来分析,第一段:

Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);

传入的mapperClass参数就是DaduduMaper接口,返回的是MyEntity,也就是DaduduMapper接口指定的泛型:
image.png

然后:

// 根据modelClass生成表信息
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);

根据modelClass,也就是MyEntity类,生成TableInfo对象,也就是表相关的信息。

然后:

// 得到AbstractMethod集合
List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);

// 遍历集合进行注入
if (CollectionUtils.isNotEmpty(methodList)) {
    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
}

这里的this,既DefaultSqlInjector对象,它的getMethodList()方法实现如下:

public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
    Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
    	.add(new Insert())
    	.add(new Delete())
    	.add(new Update())
    	.add(new SelectCount())
    	.add(new SelectMaps())
    	.add(new SelectObjs())
    	.add(new SelectList());

    // 如果有主键,则添加跟主键相关的对象
    if (tableInfo.havePK()) {
        builder.add(new DeleteById())
        .add(new DeleteBatchByIds())
        .add(new UpdateById())
        .add(new SelectById())
        .add(new SelectBatchByIds());
    } else {
        // 日志打印而已...
    }
    return builder.build().collect(toList());
}

以上代码并不难,就是生成一个List,并且该List中存储一些Insert、Delete、Update、SelectById等对象,这些对象的父类是AbstractMethod类,得到List后就遍历这些对象,分别调用这些对象的inject(),该方法中会调用injectMappedStatement()抽象方法,我们拿SelectById对象举例,最终就会调用它所实现的injectMappedStatement()方法,它的逻辑是:

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

    // 是一个枚举
    SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;

    // 生成SQL语句后封装为SqlSource对象
    SqlSource sqlSource = super.createSqlSource(configuration, String.format(sqlMethod.getSql(),
            sqlSelectColumns(tableInfo, false),
            tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
            tableInfo.getLogicDeleteSql(true, true)), Object.class);

    // 根据方法信息和SqlSource对象生成并返回MappedStatement对象
    return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
}

这个方法里面就会负责生成SqlSource对象,有了SqlSource对象在结合方法信息,就可以生成MappedStatement对象了,注意该返回的就是MappedStatement对象。

在看具体生成SqlSource对象的逻辑之前,到此我想先总结一下,我们上面看到了有很多Insert、Delete、Update、SelectById等对象,而这些对象其实就分别对应了BaseMapper接口中的方法,比如selectById()方法对应的就是SelectById对象,并且每个对象中都有injectMappedStatement()方法,也就是每个对象有各自的策略来生成Sql,所以,我们要知道selectById()方法对应的SQL长什么样,那我们就找到SelectById对象看它里面是如何生成SqlSource对象就可以了,SqlSource对象中就包含了具体的SQL语句。

并且我们的入口是解析DaduduMapper接口,在这个过程中Mybatis-Plus会把Insert、Delete、Update、SelectById这些对象获取出来,然后遍历它们一个一个按照各自的策略进行解析得到SqlSource对象,并得到对应的MappedStatement对象,所以在解析DaduduMapper接口时,除开我们自定义的test()方法会生成一个MappedStatement对象,其实额外还会得到BaseMapper中各个方法所对应的MappedStatement对象,而最终在调用方法时,就会根据方法得到MappedStatement对象,从而得到SqlSource对象,从而执行SQL语句。

总结完了,不知道大家是否明白了呢?如果有疑问欢迎联系我讨论,下面有我的联系方式。

我们继续来看SqlSource对象的生成过程,本文我只以SelectById对象为例,来看看它是如何生成SQL的,其他对象后续文章或大家可以自行分析。

其实上面SqlSource部分的代码中,最关键的就是String.format()部分:

// 枚举
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;

String.format(sqlMethod.getSql(),
            sqlSelectColumns(tableInfo, false),
            tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
            tableInfo.getLogicDeleteSql(true, true))

它的作用是拼装SQL语句,所以很重要,其中它用到的SqlMethod对象是一个枚举,该枚举就更重要了,部分内容如下:
image.png
image.png
所以,这个SqlMethod枚举定义了BaseMapper中各个方法对应的SQL模板,比如SelectById对象中取的就是该枚举中的SELECT_BY_ID

SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"),

它对应的SQL模板为:SELECT %s FROM %s WHERE %s=#{%s} %s,所以,回到上面的代码:

String.format(sqlMethod.getSql(),
            sqlSelectColumns(tableInfo, false),
            tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
            tableInfo.getLogicDeleteSql(true, true))
  1. sqlMethod.getSql()得到就是枚举中的SQL模板,比如SELECT %s FROM %s WHERE %s=#{%s} %s
  2. sqlSelectColumns(tableInfo, false)方法会根据表信息返回要查询的字段信息,比如"id,name"
  3. tableInfo.getTableName()得到表名,比如"my_entity"
  4. tableInfo.getKeyColumn()得到主键名,比如"id"
  5. tableInfo.getKeyProperty()得到主键参数名,比如"id"
  6. tableInfo.getLogicDeleteSql(true, true)在SelectById对象中得到的是空字符串""

最终把这些信息format到SQL模板中就得到了最终SQL:SELECT id,name FROM my_entity WHERE id=#{id}),而这,就是BaseMapper接口中selectById()方法所对应的SQL,此次应该有掌声或点赞、分享、收藏。

再次提醒,如果你想知道BaseMapper接口中deleteBatchIds()方法对应的SQL语句是怎样的,那你直接看DeleteBatchByIds对象以及SqlMethod枚举中的DELETE_BATCH_BY_IDS就可以啦,比如:

DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量删除数据", "<script>\nDELETE FROM %s WHERE %s IN (%s)\n</script>"),

好啦,本文就分析到这,其实为了阅读体验,这中间我省去了很多细节,后续再来分享吧,期待我后续的文章吗?关注我的公众号:Hoeller

我是大都督周瑜,欢迎关注我的公众号:Hoeller。这是我的个人号,非机构号,已从机构离职,重回一线了。

现在网络上有很多的文章和视频,但是其中大部分都是一些八股文和理论知识,由不同的人翻来覆去,重复写、重复发,这种做法对作者本身是有用的,但是对于大部分读者来说可能是没有意义的,对于读者来说,不管是面试还是实际工作,我相信实战经验才是真正有价值的,所以我现在的分享都来源于我的实际工作,再结合我多年讲课的经验,希望做到把实战经验、理论知识、授课技巧融合起来,让读者能轻轻松松的从我的文章或视频中学到真正有价值的知识。

我希望成为:自媒体圈中技术最好、实战经验最丰富的达人,技术圈中最会分享的架构师。加油!

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis-Plus 提供了一个非常方便的分页插件,可以很方便地实现分页查询。在使用 Mybatis-Plus 进行分页查询时,可以通过编写 XML 映射文件来实现。 假设要查询 A 表和 B 表的连表结果并进行分页,可以按照以下步骤进行操作: 1. 在 Mybatis-Plus 配置文件(一般是 application.yml 或 application.properties)开启分页插件: ```yaml mybatis-plus: configuration: # 开启分页插件 page-helper: true ``` 2. 编写 A 表和 B 表的实体类,并使用 Mybatis-Plus 提供的注解(如@TableField)来进行关联映射。 3. 在 XML 映射文件编写 SQL 查询语句,并使用 Mybatis-Plus 提供的分页标签(如<page>)来实现分页。 例如,假设要查询 A 表和 B 表的连表结果,并按照 id 排序,查询第 11 到 20 条记录,可以编写如下 XML 映射文件: ```xml <!-- A 表的映射文件 --> <mapper namespace="com.example.mapper.AMapper"> <select id="selectByPage" resultMap="BaseResultMap"> SELECT A.*, B.* FROM A JOIN B ON A.id = B.a_id ORDER BY id <!-- 使用 Mybatis-Plus 提供的分页标签 --> <page limit="10" offset="10"/> </select> </mapper> ``` 4. 在代码调用对应的 mapper 方法进行查询即可。例如,在 AMapper 接口定义如下方法: ```java List<A> selectByPage(); ``` 在实际使用,需要根据具体情况进行调整,例如根据查询条件进行筛选等。需要注意的是,Mybatis-Plus 的分页插件默认使用的是 PageHelper,如果需要使用其他分页插件,可以在配置文件进行配置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值