分享公司DAO層動態SQL的一些封裝

 

主題

  公司在DAO層使用的框架是Spring Data JPA,這個框架很好用,基本不須要本身寫SQL或者HQL就能完成大部分事情,可是偶爾有一些複雜的查詢仍是須要本身手寫原生的Native SQL或者HQL.同時公司前端界面使用的是jquery miniui框架,而且本身進行了一些封裝.html

  當界面上查詢條件比較多的時候,須要動態拼接查詢條件,使用JPA的話能夠經過CriteriaQuery進行面向對象的方式進行查詢,可是偶爾有時候又要用到HQL或者SQL,畢竟比CriteriaQuery簡單不少.而二者彷佛不能很好的結合(我還沒見過將二者混用的,不過多是我CriteriaQuery使用的比較少的緣由).前端

  這也是我寫這篇文章的緣由,記錄分享一下公司的解決辦法(公司的策略也不是完美的,只能適用於簡單的查詢,複雜的狀況仍是不支持的,可是也不失爲一種解決辦法)jquery

 

原理

  公司動態查詢的原理是使用HQL或者SQL,在這個基本select語句上動態拼接where條件...公司認爲用戶在界面上動態選擇條件的時候,select 的表基本是不會變化的,有變化的是where字句裏的條件.因此須要對用戶的輸入和where子句裏的條件進行封裝.sql

  DefaultPage:這個類是公司對前端界面用戶輸入的查詢條件,好比分頁信息等等進行的封裝.(固然還有其餘不少類..只是這個是最主要的,怎麼封裝的不是這篇文章的主題)數據庫

  SearchCriteria:這個是公司對查詢條件where子句的封裝,它能夠經過一些方法轉化成where子句裏SQL字符串..app

  最主要的就是以上2個類,核心思想就是封裝用戶輸入的查詢信息到DefaultPage,從DefaultPage中取得SearchCriteria,將SearchCriteria轉化成字符串形式,拼接在基礎的SQL(HQL)之上,造成動態的SQL來查詢數據.框架

 

主要實現

SearchCriteria

SearchCriteria是對where子句的封裝,SearchCriteria中包含不少個小的SubCriteria,SubCriteria是對where子句裏每一個條件的封裝.ide

好比有個where子句是:ui

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

那整個where語句是1個SearchCriteria  this

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 ....

這樣的查詢就作不出來...

相關文章
相關標籤/搜索