blinkfox / fenix

This is an extension library to the Spring Data JPA complex or dynamic SQL query. 这是一个比 MyBatis 更加强大的 Spring Data JPA 扩展库,为解决复杂动态 JPQL (或 SQL) 而生。https://blinkfox.github.io/fenix
https://blinkfox.github.io/fenix
Apache License 2.0
345 stars 72 forks source link

sql改写扩展点 #9

Closed hexian closed 4 years ago

hexian commented 4 years ago

您好, 在实际工作中我们的表一般都是下划线隔开多个单词,而hibernate的投影或者自定义的ResultTransformer一般使用Transformers.aliasToBean(Clazz), 根据数据库列的别名和返回类型的名称相同才能正确赋值, 我觉得可以在您的 com.blinkfox.fenix.jpa.FenixJpaQuery.doCreateQuery(Object[]) 这个方法里面的 this.querySql = this.sqlInfo.getSql(); 对这个sql进行再次处理( 这里 ), 使用durid或者jsqlparse等库对sql进行改写,自动增加下划线转别名的功能, 或者支持传入一个策略类, 可以自定义列明到Java对象字段的转换, 这样就实现了类似MyBatis中的 resultMap 的功能,下面是我参考durid的一个写法:

import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern;

import com.alibaba.druid.sql.SQLUtils; import com.alibaba.druid.sql.ast.SQLExpr; import com.alibaba.druid.sql.ast.SQLStatement; import com.alibaba.druid.sql.ast.expr.SQLPropertyExpr; import com.alibaba.druid.sql.ast.statement.SQLExprTableSource; import com.alibaba.druid.sql.ast.statement.SQLSelectItem; import com.alibaba.druid.sql.ast.statement.SQLTableSource; import com.alibaba.druid.sql.visitor.SQLASTVisitorAdapter; import com.alibaba.druid.util.JdbcConstants;

public abstract class SqlUtils {

public static enum GenAliasSrategy {

    NONE("直接以列名作为别名"),
    LOWER_COLUMN_NAME("以列的小写作为别名"),
    UPPER_COLUMN_NAME("以列的小写作为别名"),
    CAMEL2UNDERLINE("驼峰转下划线"),
    UNDERLINE2CAMEL("下划线转驼峰");

    private String srategyRemark;

    private GenAliasSrategy(String srategyRemark) {
        this.srategyRemark = srategyRemark;
    }

    public String getSrategyRemark() {
        return srategyRemark;
    } 

}

/**
 * <pre>
 * 将传入的sql语句进行处理, 如果这个sql语句的列没有别名, 则根据传入的策略自动加上别名, 如果列本身有别名则不处理有别名的列(依赖 druid 来进行解析, 需要引入依赖包), 
 * 因为是静态解析sql,所以 * 这个需要写程序来展开实际的列, 注: 应该也可以用与查询列的jpql
 * 
 * 例如: (策略为下划线转驼峰)
 *   传入的sql: 
 *       SELECT d.dept_name deptNameVo, e.emp_id, e.gender FROM t_employee e left join department d ON e.emp_id=d.dept_id+200
 *       
 *   处理和sql: 
 *       SELECT d.dept_name AS deptNameVo, e.emp_id AS empId, e.gender AS gender FROM t_employee e LEFT JOIN department d ON e.emp_id=d.dept_id+200
 *   
 * </pre>
 * @return 返回改写后的sql语句
 */
public static String autoPopulatesColumnAliases(String sql, String dbType, GenAliasSrategy strategy) {
    if(strategy == null) {
       strategy = GenAliasSrategy.UNDERLINE2CAMEL;
    }

    List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
    // 只能处理一个sql语句
    SQLStatement sqlStatement = stmtList.get(0);

    // 设置访问者
    ExportTableAliasVisitor visitor = new ExportTableAliasVisitor();
    sqlStatement.accept(visitor);

    // 获取所有的表(可以改写表名,仅对NativeSql有效)
    Map<String, SQLTableSource> tableaAliasMap = visitor.getTableaAliasMap();

    // 获取所有的列
    Map<String, SQLSelectItem> columnItems = visitor.getColumnItems();
    Iterator<Entry<String, SQLSelectItem>> iterator = columnItems.entrySet().iterator();
    while(iterator.hasNext()) {
        Entry<String, SQLSelectItem> entry = iterator.next();
        // 数据库列名称
        String name = entry.getKey(); 
        SQLSelectItem item = entry.getValue();
        String alias = item.getAlias();
        // 如果没有别名则自动添加别名
        if(alias==null || "".equals(alias.trim())) {
           switch(strategy) {
              case NONE : {
                 item.setAlias(name);
                 break;
              }
              case LOWER_COLUMN_NAME : {
                 item.setAlias(name.toLowerCase());

                 break;
              }
              case UPPER_COLUMN_NAME : {
                 item.setAlias(name.toUpperCase());

                 break;
              }
              case CAMEL2UNDERLINE : {
                  String underLine = camel2UnderLine(name);
                  item.setAlias(underLine);

                  break;
              }
              case UNDERLINE2CAMEL : {
                 String camel = underLine2Camel(name);
                 item.setAlias(camel);

                 break;
              }
           }
        }
    }

    // 获取处理后的sql语句 
    String newSql = sqlStatement.toString();

    return newSql;
}

/**
 * 下划线转驼峰
 */
public static String underLine2Camel(String underline){
    Pattern pattern = Pattern.compile("[_]\\w");
    String camel = underline.toLowerCase();
    Matcher matcher = pattern.matcher(camel);
    while(matcher.find()){
         String w = matcher.group().trim();
         camel = camel.replace(w, w.toUpperCase().replace("_", ""));
    }

    return camel;
}

/**
 * 驼峰转下划线
 */
public static String camel2UnderLine(String camel) {
    // 转成小写
    String underline = camel.replaceAll("\\B([A-Z])", "_$1").toLowerCase();

    return underline;
}

/**
 * 仅内部使用
 *
 */
 private static class ExportTableAliasVisitor extends SQLASTVisitorAdapter /* MySqlASTVisitorAdapter */ {

    // 保存数据库的表的别名与表信息的映射
    private Map<String, SQLTableSource> tableaAliasMap = new LinkedHashMap<String, SQLTableSource>();

    // 保存列的别名与列信息的映射
    private Map<String, SQLSelectItem> columnItems = new LinkedHashMap<String, SQLSelectItem>();

    @Override
    public boolean visit(SQLExprTableSource x) {
        String alias = x.getAlias();
        tableaAliasMap.put(alias, x);
        return true;
    }

    @Override
    public boolean visit(SQLSelectItem item) {
        SQLExpr expr = item.getExpr();
        if(expr instanceof SQLPropertyExpr) {
           SQLPropertyExpr propExpr = (SQLPropertyExpr) expr;
           String name = propExpr.getName();
           columnItems.put(name, item);
        }

        return true;
    }

    public Map<String, SQLTableSource> getTableaAliasMap() {
        return tableaAliasMap;
    }

    public Map<String, SQLSelectItem> getColumnItems() {
        return columnItems;
    }
}

public static void main(String[] args) {
    // 测试自动增加别名性能与功能
    String select = "SELECT " 
                    + " d.dept_name deptNameVo, " // 有别名的不会自己转换
                    + " e.emp_id, "               // emp_id ==> empId
                    + " e.gender "                // gender ==> gender
                    + " FROM t_employee e "
                    + " left join department d "
                    + " ON e.emp_id=d.dept_id+200 ";
    long start = System.currentTimeMillis();
    String newSql = autoPopulatesColumnAliases(select, JdbcConstants.MYSQL, GenAliasSrategy.UNDERLINE2CAMEL);
    System.out.println(newSql);
    long end = System.currentTimeMillis();
    System.out.println("自动增加别名耗时: "+(end-start)+" 毫秒" );

}

}

我写了另一个半成品,就是使用jpa的@Column注解, 但是name是作为数据库的列名称(最好还是自定义一个注解比较好), 然后写了一个 ResultTransformer

返回的vo定义: import javax.persistence.Column;

import com.ducha.repositories.convert.impl.DateDataConvert; import com.ducha.repositories.dao.annotation.ResultConvertAndTransformer; import com.ducha.repositories.dao.annotation.TypeConvert;

// 标记该类中可能出现需要类型转换的字段 @ResultTransformer public class UserVo {

@Column(name = "id")
private Integer id;

@Column(name = "username")
private String username;

@Column(name = "password")
private String password;

@Column(name = "birthDay")
@TypeConvert(converter=DateDataConvert.class, parameter = {"yyyy/MM/dd HH:mm:ss"}) // @TypeConvert 自定义注解
private String birthDay;

@Column(name = "entryDate")
@TypeConvert(converter=DateDataConvert.class, parameter = {"yyyy-MM-dd HH:mm:ss"})
private String entryDate;

public Integer getId() {
    return id;
}

public void setId(Integer id) {
    this.id = id;
}

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

public String getBirthDay() {
    return birthDay;
}

public void setBirthDay(String birthDay) {
    this.birthDay = birthDay;
}

public String getEntryDate() {
    return entryDate;
}

public void setEntryDate(String entryDate) {
    this.entryDate = entryDate;
}

}

ResultTransformer定义: import java.lang.reflect.Field; import java.util.LinkedHashMap; import java.util.List; import java.util.Map;

import javax.persistence.Column;

import org.hibernate.transform.ResultTransformer;

public class AnnotationBeanResultTransforme implements ResultTransformer {

private static final long serialVersionUID = 1638820969102403186L;

private final Class<?> resultClass;

private Map<String, Field> columnFieldMapping = new LinkedHashMap<String, Field>();

public AnnotationBeanResultTransforme(Class<?> resultClass) {
    this.resultClass = resultClass;

    try {
        init();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
public Object transformTuple(Object[] tuple, String[] aliases) {
    Object result = null;
    try {
        result = resultClass.newInstance();
        for ( int i = 0; i < aliases.length; i++ ) {
            String alias= aliases[i];
            Field field = columnFieldMapping.get(alias);
            if(field!=null) {
                Object columnValue = tuple[i];
                field.set(result, columnValue);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } 

    return result;
}

// 首先进入 transformTuple(Object[], String[]) 进行转换, 然后再进入 transformList(List), 此时集合已经是实体对象了, 直接返回即可
@SuppressWarnings("rawtypes")
@Override
public List transformList(List list) {
    return list;
}

private void init() throws Exception {
    Class<?> clazz = resultClass;
    while (clazz != null) {
          // 获取所有字段,public和protected和private,但是不包括父类字段
          Field[] declaredFields = clazz.getDeclaredFields();
          for(int idx=0; idx<declaredFields.length; idx++) {
              Field f = declaredFields[idx];
              Column column = f.getAnnotation(Column.class);
              if(column!=null) {
                  // 数据库列名称
                  String name = column.name();
                  if("".equals(name)) {
                     // 如果没有值则取字段的值
                     name = f.getName(); 
                  }
                  if(!columnFieldMapping.containsKey(name)) {
                      f.setAccessible(true);
                      columnFieldMapping.put(name, f);
                  }
              }
          }

          // 访问父类
          clazz = clazz.getSuperclass(); 
    }
}

}

这样在 @QueryFenix 新增一个属性或者新增一个新的注解来告诉框架改写sql的时候的策略。

blinkfox commented 4 years ago

比较独立可行的方案应该是传入一个转换器,在注解或者 XML 中配置下就行,但是感觉这个功能有些鸡肋。而且不同的公司规范,表设计存在较大差异。