我希望成为:自媒体圈中技术最好、实战经验最丰富的达人,技术圈中最会分享的架构师。加油!
我的公众号: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对象,该对象长下面这样:
可以看到,一个MappedStatement对象包含了两个非常重要的部分:
对于我们自定义的方法,生成这两部分信息是比较自然的,解析方法和SQL就可以了。那对于BaseMapper接口中的方法呢?它的SQL部分信息是如何得来的呢?比如BaseMapper接口中存在一个selectById()
方法,这个方法对应的SqlSource对象是如何生成的呢?
在Mybatis-Plus的源码中有这样一段代码:
// 判断type是不是继承了Mapper接口
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
parserInjector();
}
其中type参数就是我们定义的DadududuMapper接口,而isSupperMapperChildren()
方法会判断DadududuMapper接口是不是继承了Mapper接口,如果是,则执行parserInjector()
方法。
由于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接口指定的泛型:
然后:
// 根据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对象是一个枚举,该枚举就更重要了,部分内容如下:
所以,这个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))
sqlMethod.getSql()
得到就是枚举中的SQL模板,比如SELECT %s FROM %s WHERE %s=#{%s} %s
sqlSelectColumns(tableInfo, false)
方法会根据表信息返回要查询的字段信息,比如"id,name"tableInfo.getTableName()
得到表名,比如"my_entity"tableInfo.getKeyColumn()
得到主键名,比如"id"tableInfo.getKeyProperty()
得到主键参数名,比如"id"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。这是我的个人号,非机构号,已从机构离职,重回一线了。
现在网络上有很多的文章和视频,但是其中大部分都是一些八股文和理论知识,由不同的人翻来覆去,重复写、重复发,这种做法对作者本身是有用的,但是对于大部分读者来说可能是没有意义的,对于读者来说,不管是面试还是实际工作,我相信实战经验才是真正有价值的,所以我现在的分享都来源于我的实际工作,再结合我多年讲课的经验,希望做到把实战经验、理论知识、授课技巧融合起来,让读者能轻轻松松的从我的文章或视频中学到真正有价值的知识。
我希望成为:自媒体圈中技术最好、实战经验最丰富的达人,技术圈中最会分享的架构师。加油!