zoukankan      html  css  js  c++  java
  • 分享公司DAO层动态SQL的一些封装

    主题

      公司在DAO层使用的框架是Spring Data JPA,这个框架很好用,基本不需要自己写SQL或者HQL就能完成大部分事情,但是偶尔有一些复杂的查询还是需要自己手写原生的Native SQL或者HQL.同时公司前端界面使用的是jquery miniui框架,并且自己进行了一些封装.

      当界面上查询条件比较多的时候,需要动态拼接查询条件,使用JPA的话可以通过CriteriaQuery进行面向对象的方式进行查询,但是偶尔有时候又要用到HQL或者SQL,毕竟比CriteriaQuery简单很多.而两者似乎不能很好的结合(我还没见过将两者混用的,不过可能是我CriteriaQuery使用的比较少的原因).

      这也是我写这篇文章的原因,记录分享一下公司的解决办法(公司的策略也不是完美的,只能适用于简单的查询,复杂的情况还是不支持的,但是也不失为一种解决办法)

    原理

      公司动态查询的原理是使用HQL或者SQL,在这个基本select语句上动态拼接where条件...公司认为用户在界面上动态选择条件的时候,select 的表基本是不会变化的,有变化的是where字句里的条件.所以需要对用户的输入和where子句里的条件进行封装.

      DefaultPage:这个类是公司对前端界面用户输入的查询条件,比如分页信息等等进行的封装.(当然还有其他很多类..只是这个是最主要的,怎么封装的不是这篇文章的主题)

      SearchCriteria:这个是公司对查询条件where子句的封装,它可以通过一些方法转化成where子句里SQL字符串..

      最主要的就是以上2个类,核心思想就是封装用户输入的查询信息到DefaultPage,从DefaultPage中取得SearchCriteria,将SearchCriteria转化成字符串形式,拼接在基础的SQL(HQL)之上,形成动态的SQL来查询数据.

    主要实现

    SearchCriteria

    SearchCriteria是对where子句的封装,SearchCriteria中包含很多个小的SubCriteria,SubCriteria是对where子句里每个条件的封装.

    比如有个where子句是:

    where 1=1 and user.username = 'user1' and user.state in ('0','1');

    那整个where语句是1个SearchCriteria  

    and user.username = 'user1'是第一个SubCriteria 

    and user.state in ('0','1')是第二个SubCriteria

    1=1是为了拼接方便,就算没有查询条件也拼接where条件而增加的一个拼接默认条件

    1     public SubCriteria(String attName, EOperator operator, Object value, ERelation relation, String tabelAlias) {
    2         this.attName = attName;
    3         this.operator = operator;
    4         this.attValue = value;
    5         this.relation = relation;
    6         this.tableAliasName = tabelAlias;
    7     }

    从SubCriteria的构造方法中可以看出,如果一个SubCriteria对应的是and user.username = 'user1'

    那attName就是username

    operator就是=

    value就是'user1'

    relation就是and

    tableAlias就是user

    1个SearchCriteria中肯定会含有N个SubCriteria

    List<SubCriteria> list = new ArrayList<SubCriteria>();

    查询的数据库结果的方法如下:

    1     @Override
    2     public <D> List<D> executeDynamicQuerySql(String sql, SearchCriteria criteria, Class<? extends D> targetClass,
    3             Map<String, Object> customParams) {
    4         Map<String, Object> paramMap = new HashMap<String, Object>();
    5         String handledSql = calculateDynamicSql(sql, criteria, paramMap);
    6         mergeCustomParams(paramMap, customParams);
    7         return NativeSqlExecutor.executeQuerySql(getEntityManager(), handledSql, paramMap, targetClass);
    8     }

    sql的格式就是类似于 select * from user user %Where_Clause% and user.yxbz = :yxbz     (yxbz是有效标志的意思)

    criteria就是界面上动态选择的查询条件和值的封装

    targetClass不重要,只是为了把结果封装成对象时候,指定要封装到哪个类型的对象里.(数据库返回结果封装到对象也是公司自己封装的代码)

    customParams 里存是sql里一些占位符参数的键值对,比如key=yxbz,value='Y'

    第5行代码calculateDynamicSql将Criteria转化成的SQL拼接到传入的sql中,替换掉%Where_Clause%,在把Criteria中的占位符键值对放到paramMap中

    第6行,将paramMap与传入的customParams合并,得到一个合并的map,即是把customParams的键值对放到paramMap中.

    第7行就是常规的调用JPA的方法,把生成的动态的SQL与参数Map传给JPA去执行,根据targetClass将返回的结果封装成对象.

    calcilateDynamicSql具体步骤如下:

        private String calculateDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) {
            String searchSql = calculateSearchDynamicSql(sql, criteria, paramMap);
            return calculateOrderDynamicSql(searchSql, criteria);
        }
    
    private String calculateSearchDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) {
            StringBuilder whereClause = new StringBuilder(" WHERE 1=1 ");
            int index = paramMap.keySet().size() + 1;
            for (SubCriteria subCriteria : criteria.getCreteriaList()) {
                whereClause.append(subCriteria.getRelation().getCode());
                whereClause.append(StringUtils.isEmpty(subCriteria.getTableAliasName()) ? "" : subCriteria
                        .getTableAliasName() + ".");
                whereClause.append(subCriteria.getAttName());
                whereClause.append(" ");
                whereClause.append(subCriteria.getOperator().getCode());
                whereClause.append(" ");
                if (EOperator.IN == subCriteria.getOperator()) {
    
                    @SuppressWarnings("unchecked")
                    List<Object> paramValues = (List<Object>) subCriteria.getAttValue();
                    whereClause.append("(");
                    for (int i = 0; i < paramValues.size(); i++) {
                        whereClause.append(PLACEHOLDER);
                        String key = PARAM_PREFIX + (index++);
                        whereClause.append(key);
                        if (i != paramValues.size() - 1) {
                            whereClause.append(",");
                        }
                        paramMap.put(key, paramValues.get(i));
                    }
                    whereClause.append(")");
                } else {
                    whereClause.append(PLACEHOLDER);
                    String key = PARAM_PREFIX + (index++);
                    whereClause.append(key);
                    whereClause.append(" ");
                    paramMap.put(key, subCriteria.getAttValue());
                }
            }
            return sql.replace("%WHERE_CLAUSE%", whereClause.toString());
        }

    calculateDynamicSql中又分为2个步骤,先根据SearchCriteria计算出where字符串,再根据SearchCriteria计算order by子句...order by子句比where子句简单很多,原理也差不多...就不介绍了..主要看calculateSearchDynamicSql这个计算where子句的方法步骤主要是:

    1.先拼接where 1=1 这是为了简化问题,防止用户什么都不选的时候不用拼接where子句的问题,就算动态SQL里什么where条件都不写,也会拼接where 1=1 这个条件来简化问题.

    2.有了步骤1可以保证一定拼接了where字符串,后续只要把SubCriteria转化成字符串拼接到where子句中就OK了. 拼接方法如前面介绍SubCriteria所说,就是把SubCriteria中的属性一个一个取出来拼接.唯一有点区别的就是如果SubCriteria中EOperator是in操作符,那传过来的参数值是个list而不是一个String...

    mergeCustomParams(paramMap, customParams)方法

    拼接完了SQL,就需要把SearchCriteria中的占位符键值对与用户传入的键值对合并.

     1     private void mergeCustomParams(Map<String, Object> paramMap, Map<String, Object> customParams) {
     2         if (null == customParams || null == paramMap) {
     3             return;
     4         }
     5         for (Map.Entry<String, Object> entry : customParams.entrySet()) {
     6             if (null != entry.getKey()) {
     7                 if (paramMap.containsKey(entry.getKey())) {
     8                     throw new IllegalArgumentException("动态SQL不允许自定义占位符以 'P_' 开始,该类占位符用于 searchCriteria动态生成。");
     9                 }
    10                 paramMap.put(entry.getKey(), entry.getValue());
    11             }
    12         }
    13     }

    当然其中键值对可能会有同名的...那就报错...

    executeQuerySql方法

     1     public static <D> List<D> executeQuerySql(EntityManager entityManager, String sql, Map<String, Object> paramMap,
     2             Class<? extends D> targetClass) {
     3 
     4         LOGGER.debug("Execute native query sql {} with parameters {} for target {}", sql, paramMap, targetClass);
     5         Query query = entityManager.createNativeQuery(sql);
     6         if (DbColumnMapper.isNamedMapping(targetClass)) {
     7             query.unwrap(SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
     8         }
     9         if (paramMap != null) {
    10             for (Entry<String, Object> entry : paramMap.entrySet()) {
    11                 query.setParameter(entry.getKey(), entry.getValue());
    12             }
    13         }
    14         return DbColumnMapper.resultMapping(query.getResultList(), targetClass);
    15     }

    核心就是:

    1.query.setParameter(entry.getKey(), entry.getValue());根据传入的键值对设置到占位符中

    2.query.getResultList()查询结果..其他很多代码是用于把query.getResultList()的结果封装成对象用的..主要方法是在targetClass的类的Field上使用注解标注.

    DefaultPage

    公司对前后台请求与参数都进行了封装,前面写过一篇文章,介绍了公司的基本思路(当然实现肯定不一样)

    http://www.cnblogs.com/abcwt112/p/5169250.html

    所以这里不再详细介绍前台用户选择的那些查询条件怎么封装到DefaultPage里了...

    我们来看看DefaultPage如何生成SearchCriteria的:

     1 private SearchCriteria buildSearchCriteria(String buildSearchType) {
     2         SearchCriteria search = new SearchCriteria();
     3         operationMap = this.getOperationMap();
     4         try {
     5             for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
     6                 String paramKey = entry.getKey();
     7                 Object value = parameterMap.get(paramKey);
     8                 boolean isString = value instanceof String;
     9 
    10                 if (value != null) {
    11                     if (isString) {
    12                         if (StringUtils.isNotEmpty((String) value)) {
    13                             search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
    14                                     operationMap.get(paramKey));
    15                             this.parameterValues.add(value);
    16                         }
    17                     } else {
    18                         search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
    19                                 operationMap.get(paramKey));
    20                         this.parameterValues.add(value);
    21                     }
    22 
    23                 }
    24             }
    25             for (SearchOrder order : orderBy) {
    26                 if (StringUtils.isNotEmpty(order.getOrderName())) {
    27                     SearchOrder realOrder = new SearchOrder(this.getColumnName(order.getOrderName(), buildSearchType),
    28                             order.isAsc());
    29                     search.getOrderByList().add(realOrder);
    30                 }
    31             }
    32 
    33         } catch (Exception ex) {
    34             throw new SystemException(SystemException.REQUEST_EXCEPTION, ex, ex.getMessage());
    35         }
    36         return search;
    37     }

    buildSearchType有2种,1种最常用的就是生成我们这里一般的SQL查询的searchCriteria,还有一种适用于SpringDataJpa,用于生成适用于Specification接口的SearchCriteria用的...

    生成SearchCriteria主要是为了生成SubCriteria,通过调用SearchCriteria的add方法直接在生成一个SubCriteria并放到SearchCriteria的List<SubCriteria>成员域中.

    1                             search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey),
    2                                     operationMap.get(paramKey));
    1     public void add(String attribute, Object value, EOperator operator) {
    2         if (attribute == null || operator == null) {
    3             return;
    4         }
    5         list.add(new SubCriteria(attribute, operator, value));
    6     }

    search.add的第一个参数是attribute就是where user.username = :username中的username

    getColumn方法如下:

     1     private String getColumnName(String paramKey, String buildType) throws Exception {// NOSONAR
     2         if (paramKey.indexOf(':') > 0) {
     3             String[] keys = paramKey.split(":");
     4             if (null != keys && keys.length == 2 && cmpClass != null) {
     5                 Field field = cmpClass.getDeclaredField(keys[0]);
     6                 if (null != field) {
     7                     StringBuilder sb = new StringBuilder();
     8                     sb.append(keys[0]);
     9                     sb.append('.');
    10                     if (BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) {
    11                         sb.append(getColumnName(keys[1], buildType, field.getClass()));
    12                     } else {
    13                         sb.append(keys[1]);
    14                     }
    15                     return sb.toString();
    16                 }
    17             }
    18         }
    19         return getColumnName(paramKey, buildType, cmpClass);
    20     }

    大多数情况下是直接调用19行的getColumnName

     1     private String getColumnName(String paramKey, String buildType, Class<?> clazz) throws Exception {// NOSONAR
     2         String columnName = paramKey;
     3         if (paramKey.endsWith("_start")) {
     4             columnName = paramKey.replaceAll("_start", "");
     5         }
     6         if (paramKey.endsWith("_end")) {
     7             columnName = paramKey.replaceAll("_end", "");
     8         }
     9         if (clazz != null && BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) {
    10             Field field = clazz.getDeclaredField(columnName);
    11             Column column = field.getAnnotation(Column.class);
    12             if (column != null) {
    13                 columnName = column.name();
    14             }
    15         }
    16         return columnName;
    17     }

    clazz是前台数据传过来肯定会封装到一个接受对象上,如果那个对象的field上用了注解@Column,那就取注解里写的name作为attribute的name,否则就取前台传过来的参数值作为attribute.

    这是因为jpa的注解@column可以让实体类里的字段映射到数据库中表的字段,但是两者的名字可以不同,将SearchCriteria转化成SQL的时候用要使用数据库中的字段名而不是实体类中的属性名.

    这样就能构造出一个SearchCriteria了.

    以上便是公司对DAO层动态SQL的主要封装逻辑..在查询条件不复杂的情况下还算好用...

    但是查询条件比较复杂的话就有点力不从心了..因为SearchCriteria里只有一个SubCriteria的list,而SubCriteria中不能包含SubCriteria...所以像 where (a.b = '1' or a.c = '2') and ....

    这样的查询就做不出来...

  • 相关阅读:
    linux下mysql基于mycat做主从复制和读写分离之基础篇
    HttpClient之基本使用
    查阅资料学习的书签(补充中.......)
    java架构《Socket网络编程基础篇》
    Redis常见配置文件详解
    redis学习教程五《管道、分区》
    redis学习教程四《管理、备份、客户端连接》
    js将json数组转成tree对象
    小程序转发事件生命周期
    mpvue开发小程序添加页面
  • 原文地址:https://www.cnblogs.com/abcwt112/p/5874401.html
Copyright © 2011-2022 走看看