2014-12-05 65 views
1

我在Spring中爲項目創建REST API。春季地圖可選查詢參數到SQL準備語句

我面臨的問題是如何優雅地創建一個帶有可變數量參數的PreparedStatement。

例如,我有一個產品的收集和我有很多的查詢參數

/賬戶?的categoryId =不便&爲了= ASC &價格= 上限= 10 &偏移= 300

問題是,這些參數可能會或可能不會設置。

目前,我有一些看起來像這樣,但我還沒有開始消毒用戶輸入

控制器

@RequestMapping(method = RequestMethod.GET) 
public List<Address> getAll(@RequestParam Map<String, String> parameters) { 
     return addressRepository.getAll(parameters); 
} 

@Override 
public List<Address> getAll(Map<String, String> parameters) { 
    StringBuilder conditions = new StringBuilder(); 
    List<Object> parameterValues = new ArrayList<Object>(); 
    for(String key : parameters.keySet()) { 
     if(allowedParameters.containsKey(key) && !key.equals("limit") && !key.equals("offset")) { 
      conditions.append(allowedParameters.get(key)); 
      parameterValues.add(parameters.get(key)); 
     } 
    } 
    int limit = Pagination.DEFAULT_LIMIT_INT; 
    int offset = Pagination.DEFAULT_OFFSET_INT; 
    if(parameters.containsKey("limit")) 
     limit = Pagination.sanitizeLimit(Integer.parseInt(parameters.get("limit"))); 
    if(parameters.containsKey("offset")) 
     offset = Pagination.sanitizeOffset(Integer.parseInt(parameters.get("offset"))); 
    if(conditions.length() != 0) { 
     conditions.insert(0, "WHERE "); 
     int index = conditions.indexOf("? "); 
     int lastIndex = conditions.lastIndexOf("? "); 
     while(index != lastIndex) { 
      conditions.insert(index + 2, "AND "); 
      index = conditions.indexOf("? ", index + 1); 
      lastIndex = conditions.lastIndexOf("? "); 
     } 
    } 
    parameterValues.add(limit); 
    parameterValues.add(offset); 
    String base = "SELECT * FROM ADDRESSES INNER JOIN (SELECT ID FROM ADDRESSES " + conditions.toString() + "LIMIT ? OFFSET ?) AS RESULTS USING (ID)"; 
    System.out.println(base); 
    return jdbc.query(base, parameterValues.toArray(), new AddressRowMapper()); 
} 

我能提高嗎?或者,還有更好的方法?

回答

2

我發現上面的代碼很難維護,因爲它有複雜的邏輯來構建where子句。 Spring的NamedParameterJdbcTemplate可以用來簡化邏輯。按照this鏈接,在一個基本的例子上是NamedParameterJdbcTemplate

在看看

下面是新的代碼看起來應該像

public List<Address> getAll(Map<String, String> parameters) { 
     Map<String, Object> namedParameters = new HashMap<>(); 
     for(String key : parameters.keySet()) { 
      if(allowedParameters.contains(key)) { 
       namedParameters.put(key, parameters.get(key)); 
      } 
     } 

     String sqlQuery = buildQuery(namedParameters); 

     NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(null /* your data source object */); 
     return template.query(sqlQuery, namedParameters, new AddressRowMapper()); 
    } 

    private String buildQuery(Map<String, Object> namedParameters) { 
     String selectQuery = "SELECT * FROM ADDRESSES INNER JOIN (SELECT ID FROM ADDRESSES "; 
     if(!(namedParameters.isEmpty())) { 
      String whereClause = "WHERE "; 
      for (Map.Entry<String, Object> param : namedParameters.entrySet()) { 
       whereClause += param.getKey() + " = :" + param.getValue(); 
      } 

      selectQuery += whereClause; 
     } 
     return selectQuery + ") AS RESULTS USING (ID)"; 
    } 
+0

這種方法的另一個問題是,我失去的參數類型安全。作爲收集資源,我必須分頁。每次查詢參數映射時,我都必須檢查它是否爲LIMIT或OFFSET,並將參數值轉換爲long。我必須爲每個允許的參數附加一組規則。我是否讓這個過於複雜? – 2014-12-06 07:57:35

+1

是的,我認爲你讓它變得複雜:)。您原來的方法中也丟失了類型安全性,主要是因爲您收到一個Map 作爲參數。爲此,您可以嘗試@QueryParam,但namedParameters貼圖仍然必須是Map 。如果你想使地圖類型安全,那麼在編寫適用於所有參數的通用代碼時就會失敗。 – 2014-12-06 10:39:37

0

經過一番思考,我決定類型安全是非常重要的,我決定在整個使用下面的風格API。

@RequestMapping(method = RequestMethod.GET) 
public List<Address> getAll(@RequestParam(value = "cityId", required = false) Long cityId, 
          @RequestParam(value = "accountId", required = false) Long accountId, 
          @RequestParam(value = "zipCode", required = false) String zipCode, 
          @RequestParam(value = "limit", defaultValue = Pagination.DEFAULT_LIMIT_STRING) Integer limit, 
          @RequestParam(value = "offset", defaultValue = Pagination.DEFAULT_OFFSET_STRING) Integer offset) { 
    Map<String, Object> sanitizedParameters = AddressParameterSanitizer.sanitize(accountId, cityId, zipCode, limit, offset); 
    return addressRepository.getAll(sanitizedParameters); 
} 

參數衛生

public static Map<String, Object> sanitize(Long accountId, Long cityId, String zipCode, Integer limit, Integer offset) { 
    Map<String, Object> sanitizedParameters = new LinkedHashMap<String, Object>(5); 

    if(accountId != null) { 
     if (accountId < 1) throw new InvalidQueryParameterValueException(ExceptionMessage.INVALID_ID); 
     else sanitizedParameters.put("ACCOUNT_ID = ? ", accountId); 
    } 

    if(cityId != null) { 
     if (cityId < 1) throw new InvalidQueryParameterValueException(ExceptionMessage.INVALID_ID); 
     else sanitizedParameters.put("CITY_ID = ? ", cityId); 
    } 

    if(zipCode != null) { 
     if (!zipCode.matches("[0-9]+")) throw new InvalidQueryParameterValueException(ExceptionMessage.INVALID_ZIP_CODE); 
     else sanitizedParameters.put("ZIP_CODE = ? ", zipCode); 
    } 

    if (limit < 1 || limit > Pagination.MAX_LIMIT_INT) throw new InvalidQueryParameterValueException(ExceptionMessage.INVALID_LIMIT); 
    else sanitizedParameters.put("LIMIT ? ", Pagination.sanitizeLimit(limit)); 

    if(offset < 0) throw new InvalidQueryParameterValueException(ExceptionMessage.INVALID_OFFSET); 
    else sanitizedParameters.put("OFFSET ?", Pagination.sanitizeOffset(offset)); 

    return sanitizedParameters; 
} 

SQL查詢字符串構建

public static String buildQuery(Tables table, Map<String, Object> sanitizedParameters) { 
    String tableName = table.name(); 
    String baseQuery = "SELECT * FROM " + tableName + " INNER JOIN (SELECT ID FROM " + tableName; 
    String whereClause = " "; 
    if(sanitizedParameters.size() > 2) { 
     whereClause += "WHERE "; 
    } 
    if(!sanitizedParameters.isEmpty()) { 
     for(String key : sanitizedParameters.keySet()) { 
      whereClause += key; 
     } 
     baseQuery += whereClause; 
    } 
    return baseQuery + ") AS RESULTS USING (ID)"; 
} 

庫:

@Override 
public List<Address> getAll(Map<String, Object> sanitizedParameters) { 
    String sqlQuery = CollectionQueryBuilder.buildQuery(Tables.ADDRESSES, sanitizedParameters); 
    return jdbc.query(sqlQuery, sanitizedParameters.values().toArray(), new AddressRowMapper()); 
}