增加MP内置sql方法

最近在业务上频繁用到了 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
//MybatisMapperAnnotationBuilder.java
public void parse() {
...
if (!configuration.isResourceLoaded(resource)) {
...
try {
//如果是Mapper.class 的子类尝试解析注入
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()) //enhanced
.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(){
//所有的未处理sql语句都作为字符串模板存储在SqlMethod枚举类中
this(SqlMethod.SELECT_BY_ID_FOR_UPDATE.getMethod());
}

/**
* @param methodName 方法名
* @since 3.5.0
*/
protected SelectByIdForUpdate(String methodName) {
super(methodName);
}

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
//定义一个自己的 SELECT_BY_ID_FOR_UPDATE枚举类
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID_FOR_UPDATE;
SqlSource sqlSource = super.createSqlSource(
configuration,
//这里是简单格式化未处理的sql
//所有关键数据都来自于 TableInfo
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;

/**
* @author SEMGHH
* @date 2025/8/22 9:57
*/
public class SelectByIdForUpdate extends AbstractMethod {

public SelectByIdForUpdate(){
this(SqlMethod.SELECT_BY_ID_FOR_UPDATE.getMethod());
}

/**
* @param methodName 方法名
* @since 3.5.0
*/
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()) //enhanced
.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);
//获得一个Mapper,调用它的selectByIdForUpdate
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]

增加MP内置sql方法
http://example.com/2025/08/22/2025-08-22-add-mybatis-plus-built-in-sql/
作者
John Doe
发布于
2025年8月22日
许可协议