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

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

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

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

java 复制代码
@Mapper
public interface DaduduMapper extends BaseMapper<MyEntity> {

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

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

java 复制代码
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语句:

java 复制代码
@Mapper
public interface DaduduMapper extends BaseMapper<MyEntity> {

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

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

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

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

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

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

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

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

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

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

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

java 复制代码
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);

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

然后:

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

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

然后:

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

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

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

java 复制代码
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()方法,它的逻辑是:

java 复制代码
@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()部分:

java 复制代码
// 枚举
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对象是一个枚举,该枚举就更重要了,部分内容如下: 所以,这个SqlMethod枚举定义了BaseMapper中各个方法对应的SQL模板,比如SelectById对象中取的就是该枚举中的SELECT_BY_ID

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

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

java 复制代码
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就可以啦,比如:

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

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

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

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

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

相关推荐
路在脚下@6 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一7 分钟前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien9 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl