乐趣区

关于java:MybatisPlus结合lambda表达式获取entity的数据库字段名

背景

Mybatis Plus QueryWrapperlambda 用起来很便当,比方 Wrappers.<Order>lambdaQuery().eq(Order::getOrderCode, 'test')
然而在须要对 SQL 做一些非凡解决时,比方 distinctsum 时,无奈用到 lambda,只能硬编码字段的数据库字段名,例如Wrappers.<Order>query().select("distinct order_code"),这种在代码里硬编码数据库字段名,给人感觉十分的不标准,那是否能够像lambdaQuery 那样,不硬编码也能够获取到数据库字段名呢?相似这样子:

String columName = columnResolver.getColumn(Order::getOrderCode);
Wrappers.<Order>query().select("distinct" + columName);

思路

Mybatis Pluslambda 既然在惯例的 SQL 下既然能够做到获取数据库字段名,那是否能够复用它的代码呢?
这就须要先看下 Mybatis Plus 是怎么实现的。

原理

初始化

Mybatis在我的项目启动时,会生成每个 Mapper 对应的 BeanMybatis 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<String> 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<String> excludeProperty) {
    /* 数据库全局配置 */
    GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
    ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory();
    //TODO @咩咩 有空一起来撸完这反射模块.
    Reflector reflector = reflectorFactory.findForClass(clazz);
    List<Field> list = getAllFields(clazz);
    // 标记是否读取到主键
    boolean isReadPK = false;
    // 是否存在 @TableId 注解
    boolean existTableId = isExistTableId(list);
    // 是否存在 @TableLogic 注解
    boolean existTableLogic = isExistTableLogic(list);

    List<TableFieldInfo> 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办法通过获取注解 TableIdTableField 配置来生成字段相干信息,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<String, Class<?>> typeAliases = registry.getTypeAliases();
            for (Map.Entry<String, Class<?>> 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>lambdaQuery().eq(Order::getOrderCode, 'test')中的 Order::getOrderCode 传入的其实是 SFunction 对象,通过 AbstractLambdaWrapper.columnsToString 办法将 SFunction 转为 column 名称。

protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {return getColumn(LambdaUtils.resolve(column), onlyColumn);
}

LambdaUtils是解析的外围代码类。

public final class LambdaUtils {public static <T> SerializedLambda resolve(SFunction<T, ?> 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();} 
}       
  1. 先听过 SerializationUtils.serializeSFunction序列化,获取类字节。
  2. 再通过重组成 SerializedLambda.resolve 对象,通过 lambda.getClass().isSynthetic() 确保 SFunction 肯定是个lambda 表达式
  3. 最初放入 FUNC_CACHE 缓存。

学到了一手 ^_^

解析进去的 lambda 对象信息是这样的:

public abstract class AbstractLambdaWrapper<T, Children extends AbstractLambdaWrapper<T, Children>>
    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();
    }    
}
  1. 从初始化时创立的缓存里,获取 对象缓存列表
  2. 调用 PropertyNamer.methodToProperty(lambda.getImplMethodName()) 从办法中提取属性名,例如从 getCodeName 提取到codeName
  3. 依据 属性名 从缓存中获取对应的列信息。

以上就是数据执行流程,通过 LambdaWrapper.columnToString 可用获取到属性的数据库字段名,然而 columnToString 办法是protected,无奈间接应用,能够通过继承的形式来扩大。

实现

  1. 扩大类继承 AbstractLambdaWrapper,重写columnToString 办法,然而内容是调用父类的办法,只是把办法从 protected 改为public
  2. 因为 mybatis-plus 会在我的项目启动时初始化信息,也就是扩大类必须是在相干的类初始化实现之后能力用,所以也要把扩大类退出到 spring 容器中。
@Component
public class MybatisPlusColumnResolver {public <T> ColumnResolver<T> create() {return new ColumnResolver<>();
    }

    /**
     * @author 
     * @date 
     */
    public static class ColumnResolver<T> extends AbstractLambdaWrapper<T, ColumnResolver<T>> {

        @Override
        protected ColumnResolver<T> instance() {return null;}

        @Override 
        public String columnsToString(SFunction<T, ?>... columns) {return super.columnsToString(columns);
        }

        @Override 
        public String columnsToString(boolean onlyColumn, SFunction<T, ?>... columns) {return super.columnsToString(onlyColumn, columns);
        }

        @Override 
        public String columnToString(SFunction<T, ?> column) {return super.columnToString(column);
        }

        @Override 
        public String columnToString(SFunction<T, ?> column, boolean onlyColumn) {return super.columnToString(column, onlyColumn);
        }
    }
}

应用

    @Autowired
    private MybatisPlusColumnResolver lambdaColumnResolvers;
    
    ColumnResolver<Order> columnResolver = lambdaColumnResolvers.create(); 
    Wrappers.<OmsOrder>query().select("distinct" + columnResolver.columnToString(OmsOrder::getCodeName));    
退出移动版