背景
Mybatis Plus QueryWrapper
的lambda
用起来很便利,比如Wrappers.
。
但是在需要对SQL做一些特殊处理时,比如distinct
、sum
时,无法用到lambda
,只能硬编码字段的数据库字段名,例如Wrappers.
,这种在代码里硬编码数据库字段名,给人感觉非常的不规范,那是否可以像lambdaQuery
那样,不硬编码也可以获取到数据库字段名呢?类似这样子:
String columName = columnResolver.getColumn(Order::getOrderCode);
Wrappers.query().select("distinct " + columName);
思路
Mybatis Plus
的lambda
既然在常规的SQL下既然可以做到获取数据库字段名,那是否可以复用它的代码呢?
这就需要先看下Mybatis Plus
是怎么实现的。
原理
初始化
Mybatis
在项目启动时,会生成每个Mapper
对应的Bean
,Mybatis Plus
在解析Mapper
时,会解析实体类信息,生成TableInfo
,解析入口是TableInfoHelper.initTableInfo
。
public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class> clazz) {
TableInfo targetTableInfo = TABLE_INFO_CACHE.get(clazz);
final Configuration configuration = builderAssistant.getConfiguration();
if (targetTableInfo != null) {
Configuration oldConfiguration = targetTableInfo.getConfiguration();
if (!oldConfiguration.equals(configuration)) {
// 不是同一个 Configuration,进行重新初始化
targetTableInfo = initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
TABLE_INFO_CACHE.put(clazz, targetTableInfo);
}
return targetTableInfo;
}
return TABLE_INFO_CACHE.computeIfAbsent(clazz, key -> initTableInfo(configuration, builderAssistant.getCurrentNamespace(), key));
}
先从缓存TABLE_INFO_CACHE
里获取信息,如果没有,就通过initTableInfo
解析信息并放入缓存。
private synchronized static TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class> clazz) {
/* 没有获取到缓存信息,则初始化 */
TableInfo tableInfo = new TableInfo(clazz);
tableInfo.setCurrentNamespace(currentNamespace);
tableInfo.setConfiguration(configuration);
GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
/* 初始化表名相关 */
final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo);
List excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList();
/* 初始化字段相关 */
initTableFields(clazz, globalConfig, tableInfo, excludePropertyList);
/* 自动构建 resultMap */
tableInfo.initResultMapIfNeed();
/* 缓存 lambda */
LambdaUtils.installCache(tableInfo);
return tableInfo;
}
通过initTableName
方法初始化表名信息,再通过initTableFields
方法初始化字段相关信息,最后放入缓存中。
private static void initTableFields(Class> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List excludeProperty) {
/* 数据库全局配置 */
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory();
//TODO @咩咩 有空一起来撸完这反射模块.
Reflector reflector = reflectorFactory.findForClass(clazz);
List list = getAllFields(clazz);
// 标记是否读取到主键
boolean isReadPK = false;
// 是否存在 @TableId 注解
boolean existTableId = isExistTableId(list);
// 是否存在 @TableLogic 注解
boolean existTableLogic = isExistTableLogic(list);
List fieldList = new ArrayList<>(list.size());
for (Field field : list) {
if (excludeProperty.contains(field.getName())) {
continue;
}
/* 主键ID 初始化 */
if (existTableId) {
TableId tableId = field.getAnnotation(TableId.class);
if (tableId != null) {
if (isReadPK) {
throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName());
} else {
initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId, reflector);
isReadPK = true;
continue;
}
}
} else if (!isReadPK) {
isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, reflector);
if (isReadPK) {
continue;
}
}
final TableField tableField = field.getAnnotation(TableField.class);
/* 有 @TableField 注解的字段初始化 */
if (tableField != null) {
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, tableField, reflector, existTableLogic));
continue;
}
/* 无 @TableField 注解的字段初始化 */
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic));
}
/* 字段列表 */
tableInfo.setFieldList(fieldList);
/* 未发现主键注解,提示警告信息 */
if (!isReadPK) {
logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
}
}
initTableFields
方法通过获取注解TableId
、TableField
配置来生成字段相关信息,TableField
解析信息逻辑在TableFieldInfo
构造函数内。
public TableFieldInfo(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field, TableField tableField,
Reflector reflector, boolean existTableLogic) {
field.setAccessible(true);
this.field = field;
this.version = field.getAnnotation(Version.class) != null;
this.property = field.getName();
this.propertyType = reflector.getGetterType(this.property);
this.isPrimitive = this.propertyType.isPrimitive();
this.isCharSequence = StringUtils.isCharSequence(this.propertyType);
this.fieldFill = tableField.fill();
this.withInsertFill = this.fieldFill == FieldFill.INSERT || this.fieldFill == FieldFill.INSERT_UPDATE;
this.withUpdateFill = this.fieldFill == FieldFill.UPDATE || this.fieldFill == FieldFill.INSERT_UPDATE;
this.update = tableField.update();
JdbcType jdbcType = tableField.jdbcType();
final Class extends TypeHandler> typeHandler = tableField.typeHandler();
final String numericScale = tableField.numericScale();
String el = this.property;
if (JdbcType.UNDEFINED != jdbcType) {
this.jdbcType = jdbcType;
el += (COMMA + "jdbcType=" + jdbcType.name());
}
if (UnknownTypeHandler.class != typeHandler) {
this.typeHandler = (Class extends TypeHandler>>) typeHandler;
if (tableField.javaType()) {
String javaType = null;
TypeAliasRegistry registry = tableInfo.getConfiguration().getTypeAliasRegistry();
Map> typeAliases = registry.getTypeAliases();
for (Map.Entry> entry : typeAliases.entrySet()) {
if (entry.getValue().equals(propertyType)) {
javaType = entry.getKey();
break;
}
}
if (javaType == null) {
javaType = propertyType.getName();
registry.registerAlias(javaType, propertyType);
}
el += (COMMA + "javaType=" + javaType);
}
el += (COMMA + "typeHandler=" + typeHandler.getName());
}
if (StringUtils.isNotBlank(numericScale)) {
el += (COMMA + "numericScale=" + numericScale);
}
this.el = el;
this.initLogicDelete(dbConfig, field, existTableLogic);
String column = tableField.value();
if (StringUtils.isBlank(column)) {
column = this.property;
if (tableInfo.isUnderCamel()) {
/* 开启字段下划线申明 */
column = StringUtils.camelToUnderline(column);
}
if (dbConfig.isCapitalMode()) {
/* 开启字段全大写申明 */
column = column.toUpperCase();
}
}
String columnFormat = dbConfig.getColumnFormat();
if (StringUtils.isNotBlank(columnFormat) && tableField.keepGlobalFormat()) {
column = String.format(columnFormat, column);
}
this.column = column;
this.sqlSelect = column;
if (tableInfo.getResultMap() == null && !tableInfo.isAutoInitResultMap() &&
TableInfoHelper.checkRelated(tableInfo.isUnderCamel(), this.property, this.column)) {
/* 未设置 resultMap 也未开启自动构建 resultMap, 字段规则又不符合 mybatis 的自动封装规则 */
String propertyFormat = dbConfig.getPropertyFormat();
String asProperty = this.property;
if (StringUtils.isNotBlank(propertyFormat)) {
asProperty = String.format(propertyFormat, this.property);
}
this.sqlSelect += (" AS " + asProperty);
}
this.insertStrategy = this.chooseFieldStrategy(tableField.insertStrategy(), dbConfig.getInsertStrategy());
this.updateStrategy = this.chooseFieldStrategy(tableField.updateStrategy(), dbConfig.getUpdateStrategy());
this.whereStrategy = this.chooseFieldStrategy(tableField.whereStrategy(), dbConfig.getSelectStrategy());
if (StringUtils.isNotBlank(tableField.condition())) {
// 细粒度条件控制
this.condition = tableField.condition();
}
// 字段是否注入查询
this.select = tableField.select();
}
由于MybatisConfiguration
默认开启驼峰转下划线
模式:this.mapUnderscoreToCamelCase = true
,即@TableField
如果没有配置value
属性,则数据库字段名默认识别为下划线格式
。
在初始化配置信息,将信息放入缓存之后,后续的查询就可以用到这些信息了。
数据执行流程
Wrappers.
中的Order::getOrderCode
传入的其实是SFunction
对象,通过AbstractLambdaWrapper.columnsToString
方法将SFunction
转为column
名称。
protected String columnToString(SFunction column, boolean onlyColumn) {
return getColumn(LambdaUtils.resolve(column), onlyColumn);
}
LambdaUtils
是解析的核心代码类。
public final class LambdaUtils {
public static SerializedLambda resolve(SFunction func) {
Class> clazz = func.getClass();
String name = clazz.getName();
return Optional.ofNullable(FUNC_CACHE.get(name))
.map(WeakReference::get)
.orElseGet(() -> {
SerializedLambda lambda = SerializedLambda.resolve(func);
FUNC_CACHE.put(name, new WeakReference<>(lambda));
return lambda;
});
}
}
public class SerializedLambda implements Serializable {
/**
* 通过反序列化转换 lambda 表达式,该方法只能序列化 lambda 表达式,不能序列化接口实现或者正常非 lambda 写法的对象
*
* @param lambda lambda对象
* @return 返回解析后的 SerializedLambda
*/
public static SerializedLambda resolve(SFunction, ?> lambda) {
if (!lambda.getClass().isSynthetic()) {
throw ExceptionUtils.mpe("该方法仅能传入 lambda 表达式产生的合成类");
}
try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {
@Override
protected Class> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
Class> clazz;
try {
clazz = ClassUtils.toClassConfident(objectStreamClass.getName());
} catch (Exception ex) {
clazz = super.resolveClass(objectStreamClass);
}
return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;
}
}) {
return (SerializedLambda) objIn.readObject();
} catch (ClassNotFoundException | IOException e) {
throw ExceptionUtils.mpe("This is impossible to happen", e);
}
}
}
public class SerializationUtils {
/**
* Serialize the given object to a byte array.
*
* @param object the object to serialize
* @return an array of bytes representing the object in a portable fashion
*/
public static byte[] serialize(Object object) {
if (object == null) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
oos.flush();
} catch (IOException ex) {
throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), ex);
}
return baos.toByteArray();
}
}
- 先听过
SerializationUtils.serialize
把SFunction
序列化,获取类字节。 - 再通过重组成
SerializedLambda.resolve
对象,通过lambda.getClass().isSynthetic()
确保SFunction
一定是个lambda表达式
。 - 最后放入
FUNC_CACHE
缓存。
学到了一手 ^_^
public abstract class AbstractLambdaWrapper>
private String getColumn(SerializedLambda lambda, boolean onlyColumn) {
Class> aClass = lambda.getInstantiatedType();
tryInitCache(aClass);
String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName());
ColumnCache columnCache = getColumnCache(fieldName, aClass);
return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect();
}
}
- 从初始化时创建的缓存里,获取
对象缓存列表
。 - 调用
PropertyNamer.methodToProperty(lambda.getImplMethodName())
从方法中提取属性名,例如从getCodeName
提取到codeName
。 - 根据
属性名
从缓存中获取对应的列信息。
以上就是数据执行流程,通过LambdaWrapper.columnToString
可用获取到属性的数据库字段名,但是columnToString
方法是protected
,无法直接使用,可以通过继承的方式来扩展 。
实现
- 扩展类继承
AbstractLambdaWrapper
,重写columnToString
方法, 但是内容是调用父类的方法,只是把方法从protected
改为public
。 - 因为
mybatis-plus
会在项目启动时初始化信息,也就是扩展类必须是在相关的类初始化完成之后才能用,所以也要把扩展类加入到spring容器中。
@Component
public class MybatisPlusColumnResolver {
public ColumnResolver create() {
return new ColumnResolver<>();
}
/**
* @author
* @date
*/
public static class ColumnResolver extends AbstractLambdaWrapper> {
@Override
protected ColumnResolver instance() {
return null;
}
@Override
public String columnsToString(SFunction... columns) {
return super.columnsToString(columns);
}
@Override
public String columnsToString(boolean onlyColumn, SFunction... columns) {
return super.columnsToString(onlyColumn, columns);
}
@Override
public String columnToString(SFunction column) {
return super.columnToString(column);
}
@Override
public String columnToString(SFunction column, boolean onlyColumn) {
return super.columnToString(column, onlyColumn);
}
}
}
使用
@Autowired
private MybatisPlusColumnResolver lambdaColumnResolvers;
ColumnResolver columnResolver = lambdaColumnResolvers.create();
Wrappers.query().select("distinct " + columnResolver.columnToString(OmsOrder::getCodeName));