Mybatis-Plus结合lambda表达式获取entity的数据库字段名

背景

Mybatis Plus QueryWrapperlambda用起来很便利,比如Wrappers.lambdaQuery().eq(Order::getOrderCode, 'test')
但是在需要对SQL做一些特殊处理时,比如distinctsum时,无法用到lambda,只能硬编码字段的数据库字段名,例如Wrappers.query().select("distinct order_code"),这种在代码里硬编码数据库字段名,给人感觉非常的不规范,那是否可以像lambdaQuery那样,不硬编码也可以获取到数据库字段名呢?类似这样子:

String columName = columnResolver.getColumn(Order::getOrderCode);
Wrappers.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 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方法通过获取注解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 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>) 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.lambdaQuery().eq(Order::getOrderCode, 'test')中的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();
    } 
}       
  1. 先听过SerializationUtils.serializeSFunction序列化,获取类字节。
  2. 再通过重组成SerializedLambda.resolve对象,通过lambda.getClass().isSynthetic()确保SFunction一定是个lambda表达式
  3. 最后放入FUNC_CACHE缓存。
学到了一手 ^_^

解析出来的lambda对象信息是这样的:
Mybatis-Plus结合lambda表达式获取entity的数据库字段名_第1张图片

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();
    }    
}
  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  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));    

你可能感兴趣的:(Mybatis-Plus结合lambda表达式获取entity的数据库字段名)