最近在做项目时,发现前端调用查询接口,发现接口返回数据不对。我通过日志发现前端查询字段值为空时,竟然被当作一个条件,因为后端采用的mybatis-plus,通过官网我找到了一个配置
mybatis-plus:
global-config:
db-config:
select-strategy: not_empty
然后再测试一遍发现好使了。
我决定看一下mybatis-plus的底层时怎么实现的。
mybatis-plus 为我们提供了许多默认的方法,通过继承BaseMapper就可以实现,无需配置xml,具体的方法可以参考mybatis-plus的官方网站:
mybatis-plus
mybatis 在启动的时候,会根据mapper方法,生成一个MappedStatement,一个mapper的方法会对应一个MappedStatement.
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
由此可以想到,mybatis-plus 启动的时候会生成一些默认的方法的 MappedStatement我们先看MybatisConfiguration 的addMapper 方法
@Override
public void addMapper(Class type) {
mybatisMapperRegistry.addMapper(type);
}
跳转到MybatisMapperRegistry 类中
@Override
public void addMapper(Class type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// TODO 这里就不抛异常了
// throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
重点看 parser.parse() 我们在点进去看
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
final String typeName = type.getName();
assistant.setCurrentNamespace(typeName);
parseCache();
parseCacheRef();
SqlParserHelper.initSqlParserInfoCache(type);
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
SqlParserHelper.initSqlParserInfoCache(typeName, method);
}
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
}
parsePendingMethods();
}
我们看inspectInject方法:
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class> mapperClass) {
Class> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
List 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);
}
}
}
然后再看 m.inject方法:
/**
* 注入自定义方法
*/
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);
}
终于看到增加MappedStatement了,injectMappedStatement有很多实现类
@Override
public MappedStatement injectMappedStatement(Class> mapperClass, Class> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(), sqlSelectColumns(tableInfo, true),
tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, sqlMethod.getMethod(), sqlSource, tableInfo);
}
因为是查询条件,我们看sqlWhereEntityWrapper 方法 此方法会封装一些sql的条件语句:,最后来到TableFieldInfo的getSqlWhere方法:
public String getSqlWhere(final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
// 默认: AND column=#{prefix + el}
String sqlScript = " AND " + String.format(condition, column, newPrefix + el);
// 查询的时候只判非空
return convertIf(sqlScript, newPrefix + property, whereStrategy);
}
然后进入convertIf方法:
private String convertIf(final String sqlScript, final String property, final FieldStrategy fieldStrategy) {
if (fieldStrategy == FieldStrategy.NEVER) {
return null;
}
if (fieldStrategy == FieldStrategy.IGNORED) {
return sqlScript;
}
if (fieldStrategy == FieldStrategy.NOT_EMPTY && isCharSequence) {
return SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and %s != ''", property, property),
false);
}
return SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", property), false);
}
通过代码可以看到生成的sql条件对空判断和策略有关,那我们就去找TableFieldInfo创建的时候是怎么赋值的,我们回到AbstractSqlInjector中
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class> mapperClass) {
Class> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
List 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);
}
}
}
TableFieldInfo 是TableInfo的属性,我们看initTableInfo方法:
/* 初始化字段相关 */
initTableFields(clazz, globalConfig, tableInfo);
此方法就是创建TableFieldInfo:
/* 无 @TableField 注解的字段初始化 */
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field));
然后看TableFieldInfo的构造方法:
public TableFieldInfo(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field) {
this.version = field.getAnnotation(Version.class) != null;
this.property = field.getName();
this.propertyType = field.getType();
this.isCharSequence = StringUtils.isCharSequence(this.propertyType);
this.el = this.property;
this.insertStrategy = dbConfig.getInsertStrategy();
this.updateStrategy = dbConfig.getUpdateStrategy();
this.whereStrategy = dbConfig.getSelectStrategy();
tableInfo.setLogicDelete(this.initLogicDelete(dbConfig, field));
可以看到this.whereStrategy = dbConfig.getSelectStrategy();
就是说这个策略可以配置来改变