mybatis-plus不好用?那就自己写!
ShiJh Lv3

mybatisplus在设计上略有欠缺,导致整个框架有些笨重,因此决定抽出精华部分自己写

MyBatis更加强大的动态SQL

为什么不用MyBatis-Plus

MP(mybatis-plus)是一个很便捷的框架,功能也很多,在大部分人眼中是一个超级好用的框架,事实上确实如此,只要不去在意其设计上的缺陷,但我偏偏就是有强迫症,就对MP的设计抱有疑问。

  1. MP本身不支持多表联查

    MP最吸引人的东西就是QueryWrapper,以及通过实体类的注解来自动的插入、更新数据,对于插入和更新没有问题,但是查询却只能在实体类上注解的TableName一张表上进行查询,让人有些遗憾。

  2. MP绑定的IService破坏了三层架构

    这就是我最疑惑的地方,在MP中只要不进行多表联查的话是不需要写Mapper的,因此对于条件查询使用的Wrapper是在Service层中进行的,这就必须涉及到在Service中写数据库的字段名,匹配哪些字段来进行查询——这个功能本身是由持久层完成的,不应该放在业务层中,如果数据库字段名出现更改,那么业务层中使用的方法,由于相同的查询可能写多次而导致修改的工作量变大,事实上对于一次查询的所有条件都包含在DTO内,因此只需要根据DTO内的值按规则进行查询即可,而规则对于一个DTO来说只有一个,也只需要编写一次。

以上,可能由于我的无知造成误解

实现动态SQLBuilder

可以依赖于Mybatis自带的SQL进行二次封装。因此实现的Builder是用于Mybatis注解配置SQL的场景,在@XXXXProvider中使用。对框架没有任何本质增强,仅仅是简化动态生成SQL的代码量。

自定义注解

注解 作用
@UpdateField 重命名实体对应表的名字,允许更新/插入
@PrimaryField 标注实体对应表的主键,可重命名
/**
 * 带有此注解的 Field 即允许更新(包括插入),与数据库不同名可指定value值,默认同名不用赋值
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UpdateField {
    String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrimaryField {
    String value() default "";
    /*是否自增*/
    boolean auto() default true;
}

InsertSQL

对于插入语句的复杂场景无非就是批量插入,因此实现一个批量插入语句的构造器,依赖于自定义注解来保持规则。

public class InsertSQL extends BaseUpInSQL<InsertSQL> {
    private final List<?> data;

    public InsertSQL(List<?> batchData, String tableName) {
        this.data = batchData;
        this.tableName = tableName;
    }

    private String[] getIntoValueArray(String[] nameArray, int index) {
        String[] intoValueNames = new String[nameArray.length];
        for (int i = 0; i<intoValueNames.length; i++) {
            String cur = nameArray[i];
            if (cur.matches(".*\\$")) {
                nameArray[i] = Str.removeEnd(cur);
                //mybatis内部根据是否有L转化成长整形 此处无安全性风险可直接添加
                intoValueNames[i] = String.valueOf(SqlBuilderConfig.snowFlake.generate());
            } else {
                intoValueNames[i] = Str.f("#{list[{}].{}}",index, cur);
            }
        }
        return intoValueNames;
    }

    private String getFieldColumn(Field field, Annotation annotation) {
        String alias = field.getName();
        if (annotation instanceof UpdateField) {
            UpdateField anno = (UpdateField) annotation;
            alias = anno.value().isEmpty() ? alias : anno.value();
        }
        else if (annotation instanceof PrimaryField) {
            PrimaryField anno = (PrimaryField) annotation;
            alias = anno.value().isEmpty() ? alias : anno.value();
        }
        if(isCamelToUnderscore()) alias = Str.toUnderscore(alias);
        return alias;
    }

    @Override
    public String toString() {
        INSERT_INTO(tableName);
        if (data.size() == 0) return null;
        Class<?> dataClass = data.get(0).getClass();
        Field[] fields = dataClass.getDeclaredFields();
        List<String> fieldNames = new ArrayList<>(fields.length);
        boolean hasFoundId = false;
        for (Field field : fields) {
            //找主键 如果主键自增则跳过不插入主键
            if (!hasFoundId) {
                PrimaryField anno = field.getDeclaredAnnotation(PrimaryField.class);
                if (anno != null) {
                    hasFoundId = true;
                    if (anno.snowFlake()) {
                        fieldNames.add(getFieldColumn(field,anno) + "$");
                        continue;
                    } else if (anno.auto()) {
                        continue;
                    } else {
                        fieldNames.add(getFieldColumn(field,anno));
                        continue;
                    }
                }
            }
            //2 判断是否允许插入
            UpdateField upAnno = field.getDeclaredAnnotation(UpdateField.class);
            if (upAnno != null) {
                fieldNames.add(getFieldColumn(field, upAnno));
            }
        }
        String[] names = fieldNames.toArray(Cl.emptyArr(String.class));
        for (int i = 0; i < data.size(); i++) {
            INTO_VALUES(getIntoValueArray(names, i));
        }
        //雪花算法检测规则可能造成对 ID 列名的一些修改 需要处理后在INTO_COLUMNS 看起来不太优雅 但是性能比清晰的划分功能更高
        INTO_COLUMNS(names);
        return super.toString();
    }
}

UpdateSQL

更新语句主要在平时写的过程中主要时重复了“判空更新”的步骤,使用构造器简化代码。

/**
 * 默认使用 {@link PrimaryField} 注解的属性作为 where 条件 <br/>
 * 指定 <b>keyFieldName</b> 则使用该值作为 where 条件 <br/>
 * 也可使用方法 <b>where()</b> 指定自己编写的 where 子句
 */
@Slf4j
public class UpdateSQL extends SQL {
    private final Object data;
    private final String tableName;
    private String keyFieldName;
    private String whereSql;

    public UpdateSQL(Object data, String tableName) {
        this.data = data;
        this.tableName = tableName;
    }

    public UpdateSQL(Object data, String tableName, String keyFieldName) {
        this.data = data;
        this.tableName = tableName;
        this.keyFieldName = keyFieldName;
    }

    public UpdateSQL where(String whereSql) {
        this.whereSql = whereSql;
        return this;
    }

    @Override
    public String toString() {
        UPDATE(tableName);
        Field[] fields = data.getClass().getDeclaredFields();
        String name = "";
        String idFieldName = "";
        String tableId = "";
        String whereBy = "";
        boolean hasFoundId = false;
        try {
            for (Field field : fields) {
                name = field.getName();
                //1 尝试找主键 主键不能更新
                if (!hasFoundId) {
                    PrimaryField pAnno = field.getDeclaredAnnotation(PrimaryField.class);
                    if (pAnno != null) {
                        hasFoundId = true;
                        idFieldName = name;
                        tableId = pAnno.value().isEmpty() ? name : pAnno.value();
                        continue;
                    }
                }
                UpdateField annotation = field.getDeclaredAnnotation(UpdateField.class);
                //2 判断是否使用自定义的 where 条件
                if (!Str.empty(keyFieldName) && keyFieldName.equals(name)) {
                    whereBy = annotation == null || annotation.value().isEmpty() ? name : annotation.value();
                }
                //3 判断是否需要更新
                if (annotation == null) continue;
                field.setAccessible(true);
                String alias = annotation.value().isEmpty() ? name : annotation.value();
                if (field.get(data) != null) {
                    SET(Str.f("{}=#{{}}", name, alias));
                }
            }
            if(Str.empty(whereSql)) {
                if (Str.empty(keyFieldName) || Str.empty(whereBy)) {
                    keyFieldName = idFieldName;
                    whereBy = tableId;
                }
                WHERE(Str.f("{}=#{{}}", whereBy, keyFieldName));
            } else {
                WHERE(whereSql);
            }
        } catch (IllegalAccessException e) {
            log.error("update sql build failed, couldn't access {} of {}", name, data.getClass().getSimpleName());
            return null;
        }
        return super.toString();
    }
}

QuerySQL

实现只写“字段名”来构造查询Sql,仅仅只填写字段然后获得语句,内部还是使用mybatis的SQL,判断DTO字段对应值是否为空来决定是否应用where语句,使用者无需进行判断。

@SuppressWarnings("unchecked")
public abstract class BaseQuerySQL<T> {
    private final Object dto;
    private final SQL sql;

    public BaseQuerySQL(Object dto, String tableName) {
        this.dto = dto;
        this.sql = new SQL();
        sql.FROM(tableName);
    }

    public T join(String... joinSql) {
        sql.JOIN(joinSql);
        return (T)this;
    }

    public T select(String... columns) {
        sql.SELECT(columns);
        return (T)this;
    }

    public T group(String... columns) {
        sql.GROUP_BY(columns);
        return (T)this;
    }

    public T having(String havingSql) {
        sql.HAVING(havingSql);
        return (T)this;
    }

    private void where(String whereSql, String column, String fieldName) {
        if (fieldName == null) fieldName = column;
        if (isNotNULL(fieldName)) {
            sql.WHERE(Str.f(whereSql, column, fieldName));
        }
    }
    
    protected T unsafeWhere(String whereSql) {
        sql.WHERE(whereSql);
        return (T)this;
    }
    
    //通过此方法复合一组条件实现 xxx AND (xx OR xx) 这种逻辑表达式
    public T conditions(Condition... conditions) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("(");
        boolean isFst = true;
        for (Condition condition : conditions) {
            if (isNotNULL(condition.getFieldName())) {
                if (isFst) {
                    condition.removeLogic();
                    isFst = false;
                }
                stringBuilder.append(" ").append(condition.toString());
            }
        }
        stringBuilder.append(")");
        String s = stringBuilder.toString();
        if (s.equals("()")) {
            return (T) this;
        }
        return unsafeWhere(s);
    }

    private boolean isNotNULL(String fieldName) {
        try {
            Field field = dto.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(dto) != null;
        } catch (Exception e) {
            return false;
        }
    }

    public T eq(String column, String fieldName) {
        where("{} = #{{}}", column, fieldName);
        return (T)this;
    }

    public T neq(String column, String fieldName) {
        where("{} != #{{}}", column, fieldName);
        return (T)this;
    }

    public T le(String column, String fieldName) {
        where("{} <= #{{}}", column, fieldName);
        return (T)this;
    }

    public T lt(String column, String fieldName) {
        where("{} < #{{}}", column, fieldName);
        return (T)this;
    }

    public T ge(String column, String fieldName) {
        where("{} >= #{{}}", column, fieldName);
        return (T)this;
    }

    public T gt(String column, String fieldName) {
        where("{} > #{{}}", column, fieldName);
        return (T)this;
    }

    public T or() {
        sql.OR();
        return (T)this;
    }

    public T like(String column, String fieldName, LikeType type) {
        switch (type) {
            case L: fieldName = "%" + fieldName;
            case R: fieldName = fieldName + "%";
            case LR: fieldName = "%" + fieldName + "%";
        }
        where("{} like #{{}}", column, fieldName);
        return (T)this;
    }

    public T regexp(String column, String fieldName) {
        where("{} regexp #{{}}", column, fieldName);
        return (T)this;
    }

    @Override
    public String toString() {
        return sql.toString();
    }
}
 评论