最近在业务上频繁用到了 SELECT...FOR UPDATE 语法。
每个实体都需要写一份,所以想研究一下Mybatis-plus 是如何做到内置sql方法的。并新增自己的内置方法。
1. 思路 捋清楚内置sql是如何工作的 –> 添加自己新的方法 —> 构建jar —> 测试运行
2. 内置sql构建过程 解析Mapper mp提供了MybatisMapperAnnotationBuilder (Mapper注解构造器)替换了Mybatis 原本的MapperAnnotationBuilder。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void parse () { ... if (!configuration.isResourceLoaded(resource)) { ... try { if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) { parserInjector(); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new InjectorResolver (this )); } ... } ... } void parserInjector () { GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); }
在 parse() 方法中判断每一个传入的 Class<T> mapper ,如果它是 BaseMapper的子类,那么解析并注入自定义sql (parserInjector())。
检查注入sql 默认情况下,使用 DefaultSqlInjector 作为默认的注入器。
inspectInject 检查注入方法如下,其中最关键的一步调用了 getMethodList() 获取内置的sql。随后循环注入到MappedStatement中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void inspectInject (MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0 ); if (modelClass != null ) { String className = mapperClass.toString(); Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass); List<AbstractMethod> methodList = this .getMethodList(mapperClass, tableInfo); if (CollectionUtils.isEmpty(methodList)) { methodList = this .getMethodList(builderAssistant.getConfiguration(), mapperClass, tableInfo); } if (CollectionUtils.isNotEmpty(methodList)) { methodList.forEach((m) -> { m.inject(builderAssistant, mapperClass, modelClass, tableInfo); }); } else { this .logger.debug(className + ", No effective injection method was found." ); } mapperRegistryCache.add(className); } } }
DefaultSqlInjector.getMethodList() 列出了自定义的sql。 其中每一个自定义sql都是 AbstractMethod类对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class DefaultSqlInjector extends AbstractSqlInjector { @Override public List<AbstractMethod> getMethodList (Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) { GlobalConfig.DbConfig dbConfig = GlobalConfigUtils.getDbConfig(configuration); Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder() .add(new Insert (dbConfig.isInsertIgnoreAutoIncrementColumn())) .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 DeleteByIds ()) .add(new UpdateById ()) .add(new SelectById ()) .add(new SelectByIdForUpdate ()) .add(new SelectByIds ()); } else { logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method." , tableInfo.getEntityType())); } return builder.build().collect(toList()); } }
因此我们想要新增一个内置的 select...for update方法,只需要如上新增一个com.baomidou.mybatisplus.core.injector.methods.SelectByIdForUpdate类即可。
AbstractMethod是如何工作的 AbstractMethod对象是如何工作的,如何称为自定义sql?
我们都知道, SqlSource是Mybatis中sql语句的对象。 AbstractMethod 调用父类的createSqlSource创建一个SqlSource对象 ,随后添加到MappedStatement中。
下面是我的 select...for udpate 抽象方法对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class SelectByIdForUpdate extends AbstractMethod { public SelectByIdForUpdate () { this (SqlMethod.SELECT_BY_ID_FOR_UPDATE.getMethod()); } protected SelectByIdForUpdate (String methodName) { super (methodName); } @Override public MappedStatement injectMappedStatement (Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID_FOR_UPDATE; SqlSource sqlSource = super .createSqlSource( configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(tableInfo, false ), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true , true )), Object.class); return this .addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo); } }
所有的未处理sql语句都作为字符串模板存储在SqlMethod枚举类中。
定义一个自己的 SELECT_BY_ID_FOR_UPDATE枚举类 ,如下:
1 2 3 public enum SqlMethod { SELECT_BY_ID_FOR_UPDATE("selectByIdForUpdate" , "根据ID 查询一条数据获得行锁" , "SELECT %s FROM %s WHERE %s=#{%s} %s FOR UPDATE" ), }
这里就是 mp最终存储sql的地方,我们按照TableInfo 填空即可 , 随后添加到MappedStatement中, 即可被Mybatis框架使用了。
关键的 TableInfo 注意到所有的枚举类 SqlMethod都是类似于 "SELECT %s FROM %s WHERE %s=#{%s} %s FOR UPDATE" 等待填充的字符串模板。
模板填充的关键信息均来自于 关键的TableInfo 。
也就是说,如果我们新增的内置sql所需额外信息超过了 TableInfo 时,我们需要改写 TableInfo获取方法,增强额外的信息。
3. 改写过程 创建 MyBaseMapper 集成原本提供的BaseMapper :
1 2 3 4 5 package com.baomidou.mybatisplus.core.mapper;public interface MyBaseMapper <T> extends BaseMapper <T>{ public T selectByIdForUpdate (Serializable id) ; }
创建 AbstractMethod 实现类 SelectByIdForUpdate :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.baomidou.mybatisplus.core.injector.methods;import com.baomidou.mybatisplus.core.enums.SqlMethod;import com.baomidou.mybatisplus.core.injector.AbstractMethod;import com.baomidou.mybatisplus.core.metadata.TableInfo;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.mapping.SqlSource;public class SelectByIdForUpdate extends AbstractMethod { public SelectByIdForUpdate () { this (SqlMethod.SELECT_BY_ID_FOR_UPDATE.getMethod()); } protected SelectByIdForUpdate (String methodName) { super (methodName); } @Override public MappedStatement injectMappedStatement (Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID_FOR_UPDATE; SqlSource sqlSource = super .createSqlSource( configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(tableInfo, false ), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true , true )), Object.class); return this .addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo); } }
改写注入器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.baomidou.mybatisplus.core.injector;import com.baomidou.mybatisplus.core.config.GlobalConfig;import com.baomidou.mybatisplus.core.injector.methods.*;import com.baomidou.mybatisplus.core.metadata.TableInfo;import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;import org.apache.ibatis.session.Configuration;import java.util.List;import java.util.stream.Stream;import static java.util.stream.Collectors.toList;public class DefaultSqlInjector extends AbstractSqlInjector { @Override public List<AbstractMethod> getMethodList (Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) { GlobalConfig.DbConfig dbConfig = GlobalConfigUtils.getDbConfig(configuration); Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder() .add(new Insert (dbConfig.isInsertIgnoreAutoIncrementColumn())) .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 DeleteByIds ()) .add(new UpdateById ()) .add(new SelectById ()) .add(new SelectByIdForUpdate ()) .add(new SelectByIds ()); } else { logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method." , tableInfo.getEntityType())); } return builder.build().collect(toList()); } }
改写枚举类:
1 2 3 4 5 6 7 package com.baomidou.mybatisplus.core.enums;public enum SqlMethod { ... SELECT_BY_ID_FOR_UPDATE("selectByIdForUpdate" , "根据ID 查询一条数据获得行锁" , "SELECT %s FROM %s WHERE %s=#{%s} %s FOR UPDATE" ), ... }
按照上一篇文章,优雅替换三方jar中的类,打一个本地的mp包。放到测试环境中测试:
1 2 3 4 5 6 7 8 9 @SpringBootApplication public class FooApplication { public static void main (String[] args) { ConfigurableApplicationContext run = SpringApplication.run(FooApplication.class, args); OkayMapper bean = run.getBean(OkayMapper.class); Okay a = bean.selectByIdForUpdate("a" ); } }
日志输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ==> Preparing: SELECT id FROM okay WHERE id=? FOR UPDATE ==> Parameters: a(String) <== Total: 0 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@620572db] ==> Preparing: SELECT id FROM okay WHERE id=? FOR UPDATE ==> Parameters: a(String) <== Columns: id <== Row: a <== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@620572db]