Mybatis-plus为我们提供了一些通用mapper方法,比如insert,update,selectById等等,我们通过让自己的mapper继承BaseMapper这个类就可以不用自己写sql就能直接调用一些基础的sql方法。
public interface BaseMapper<T> extends Mapper<T> {}
但是在使用过程中还是发现这里面提供的方法有点少,当我们想要添加自己的通用sql方法的时候,可以通过官方文档描述的 Sql 注入器 来实现。比我们我们自己定义一个saveBatch方法,用来批量的插入数据。
BaseMapper自定义扩展
mybatis-plus提供了ISqlInjector
接口,以及AbstractSqlInjector
抽象类。我们通过实现该接口,或者继承抽象类的方式注入我们自已定义的SQL逻辑,然后继承BaseMapper添加我们需要的方法就可以添加自定义的mapper方法。
在这2个接口之外,mybatis-plus其实为我们提供了一个默认实现:DefaultSqlInjector
,这里面已经包含了一些mybatis-plus已经封装好的BaseMapper里面的方法,我们想要扩展的话,可以直接继承这个类来进行扩展添加我们的方法。
这里我们希望在BaseMapper之外添加一个saveBatch方法,用来可以批量的插入数据:
- 实现
DefaultSqlInjector
类,我们可以看到需要实现getMethodList
方法,这个方法参数是mapper接口的class类,返回值是一个List<AbstractMethod>。所以我们自定义的方法是需要实现AbstractMethod
。可以参考mybatis-plus中已经实现了一些AbstractMethod
方法,我们仿造写一个SaveBatch类。
public class CustomSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 父类的list已经包含了BaseMapper的基础方法。
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 添加我们需要增加的自定义方法。
methodList.add(new SaveBatch());
return methodList;
}
}
- 实现SaveBatch类的逻辑(这是官方的samples)。我们可以看到,这里的逻辑主要就是为了生成MappedStatement对象。
public class SaveBatch extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
final String sql = "<script>insert into %s %s values %s</script>";
final String fieldSql = prepareFieldSql(tableInfo);
final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo);
final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, "saveBatch", sqlSource, new NoKeyGenerator(), null, null);
}
private String prepareFieldSql(TableInfo tableInfo) {
StringBuilder fieldSql = new StringBuilder();
fieldSql.append(tableInfo.getKeyColumn()).append(",");
tableInfo.getFieldList().forEach(x -> {
fieldSql.append(x.getColumn()).append(",");
});
fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
fieldSql.insert(0, "(");
fieldSql.append(")");
return fieldSql.toString();
}
private String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) {
final StringBuilder valueSql = new StringBuilder();
valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
valueSql.delete(valueSql.length() - 1, valueSql.length());
valueSql.append("</foreach>");
return valueSql.toString();
}
}
- 最后我们需要将我们的Injector注入Spring容器中,替换默认的Injector。
@Bean
public CustomSqlInjector myLogicSqlInjector() {
return new CustomSqlInjector();
}
- 验证:
public interface TB3Mapper extends MyBaseMapper<Tb3> {
}
@Test
public void test() {
List<Tb3> tb3s = Arrays.asList(Tb3.getInstance(), Tb3.getInstance());
tb3Mapper.saveBatch(tb3s);
}
// output log
==> Preparing: insert into tb3 (id,f1,f2,f3) values ( ?,?,?,? ),( ?,?,?,? )
==> Parameters: 38(Integer), 62(Integer), -1546785812(Integer), -16950756(Integer), 24(Integer), 17(Integer), -1871764773(Integer), 169785869(Integer)
<== Updates: 2
原理解析
首先简单说下mybatis-plus,我只是简单翻了下源码,mybatis-plus的工作原理就是:全面再次代理了mybatis的一些东西。比如自动配置转用了MybatisPlusAutoConfiguration,SqlSessionFactoryBean转用了MybatisSqlSessionFactoryBean等等,这些mybatis的核心部件,全部都被mybatis-plus给替换了,用成了它自己的,然后它就在自己的里面定制了自己的逻辑。
我只分析下BaseMapper的实现原理,当初在文档中没有看到这块,自己手写了一版自定义的逻辑,跟踪了这块代码。在这之前还是简单阐述下mybatis的一些核心的原理,如果没有看过mybatis源码的话,知道这些也应该能看懂。
mybatis的整体逻辑可以分为2块:
- 配置文件解析:这过程包括解析我们config的配置,以及mapper.xml文件。最终配置都会被解析到一个Configuration对象里面,后面的每个SqlSession也都会包含一个该Configuration对象实例的引用。这个Configuration里面最重要的有2个东西:
- mappedStatements:存放mapper对应的sql信息
- mybatisMapperRegistry.knownMappers:存放mapper接口对应的代理类
这2个东西贯穿了mybatis的接口到sql的执行逻辑。
- 接口的调用:我们接口调用的其实是代理的包装类,这个类也是上图mybatisMapperRegistry.knownMappers里面展示的MybatisMapperProxyFactory(mybatis是MapperProxyFactory)的getObject返回的代理MybatisMapperProxy对象。这个代理类里面的主要逻辑就是拿着该类的全限定类名,指定某个方法的时候,去Configuration的mappedStatements里面去找到对应的sql。
所以知道了mybatis的大概逻辑了,我们可以猜到:在Configuration加载的时候,一定有地方将我们BaseMapper的默认方法对应的SQL的信息给装载到mappedStatements这个map里面去。我们就需要去跟踪,在哪里我们将这些默认的基础方法的MappedStatement对象进行构建,并插入到configuration中的。
debug跟踪可以发现:
第一步,肯定是自动配置要加载SqlSessionFactory,这个方法主要是构建MybatisSqlSessionFactoryBean对象,然后调用getObject方法,我们跟进MybatisSqlSessionFactoryBean.getObject()
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
// 这里才是开始构建SqlSessionFactory的
this.sqlSessionFactory = buildSqlSessionFactory();
}
可以看到,最终会执行到buildSqlSessionFactory()。这块方法的主要逻辑就是解析XML配置来创建Configuration对象。我们可以在最下面发现解析我们mapper.xml文件的逻辑:
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
// 对每一个mapper.xml文件进行解析
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
初步猜测:sql信息就是在这里面被装载进去的,重点看看xmlMapperBuilder.parse();
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// debug发现,Configuration中mappedStatements在执行该方法之后,mapper方法数量就变多了。
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
而bindMapperForNamespace
里面,是在执行configuration.addMapper(boundType);
之后方法变多的。这个方法最终调用的是MybatisMapperRegistry.addMapper()
,这个方法里面最终会转去调用MybatisMapperAnnotationBuilder.parse()
方法,将mapper的方法加入到mappedStatements中。
@Override
public void parse() {
......
try {
// https://github.com/baomidou/mybatis-plus/issues/3038
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
// 执行该步骤之后,新增了mappestatment
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
parsePendingMethods();
}
parserInjector方法如下:
void parserInjector() {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
//GlobalConfigUtils.getSqlInjector
public static ISqlInjector getSqlInjector(Configuration configuration) {
return getGlobalConfig(configuration).getSqlInjector();
}
//getSqlInjector()
private ISqlInjector sqlInjector = new DefaultSqlInjector();
//MybatisPlusAutoConfiguration.sqlSessionFactory#sqlInjector
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
我们可以看到,通过一连串的方法拿到ISqlInjector实现类。默认是DefaultSqlInjector,但是如果Spring中被手动注入了该实现类的话,就会在自动配置的时候,修改为我们自定义的SqlInjector。
这里就会转到我们自定义的逻辑了,但是我们这里CustomSqlInjector是extends DefaultSqlInjector的,所以逻辑还是在DefaultSqlInjector里面。
//DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
//....
).collect(toList());
}
//AbstractSqlInjector
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
// 可以看到这里拿取我们CustomSqlInjector返回的AbstractMethod list,然后循环调用inject
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
if (CollectionUtils.isNotEmpty(methodList)) {
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
//AbstractMethod
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
这里就很清楚了,这里面一系列的模板方法模式的类,预留了钩子让子类去实现。
DefaultSqlInjector
这个类只是为了提供哪些方法需要被注入到mappedStatements中,这个list将被抽象类AbstractSqlInjector
钩子调用。
AbstractSqlInjector
主要就是循环getMethodList
返回的AbstractMethod组成的List,然后调用inject方法。
AbstractMethod
的inject
也就是我们自己定义的逻辑。
SaveBatch
在构建好一个MappedStatement对象需要的元素后,调用addInsertMappedStatement
将插入到Configuration的mappedStatements中。
分析完毕。
番外
mybatis-plus除了BaseMapper之外,还有一些公共的方法,是放在一个 ServiceImpl
的类中的,很多人在service层继承这个类来获取这些功能,我一直很不喜欢这种方式:
- 在Service层继承这个东西,感觉像是把Dao的功能迁移到了Service层面了,层次结构有点不舒服(当然实际使用没什么影响)。
- 这个ServiceImpl里面很多方法被强制加了是事务注解,我们都无法改变!这个比较糟糕,多数据源的时候这些事务注解会导致数据源切换失败。
我的想法是这些方法能不能再落到BaseMapper层?经过这次分析发现,确实不太合适:BaseMapper里面的基础方法一般都对应这一条SQL,这条SQL是能被完整构建的。
但是在ServiceImpl里面的方法,很多其实都是打包多条SQL然后统一提交进行flush操作的,甚至一写比如saveOrUpdate方法都是执行查询,然后处理后再更新或者插入。这些都不是单个SQL能完成的任务,因此mybatis-plus将这些逻辑只能放在ServiceImpl中。
如果想将这些方法,还是放在BaseMapper中,那么可能就需要去特别的修改MapperProxy代理类了。相较而言,放在ServiceImpl还是不错的选择。