spring 将可选查询参数映射到 sql 准备语句

Posted

技术标签:

【中文标题】spring 将可选查询参数映射到 sql 准备语句【英文标题】:spring map optional query parameters to sql prepared statement 【发布时间】:2014-12-05 20:29:23 【问题描述】:

我正在 Spring 中为一个项目创建一个 REST API。

我面临的问题是如何优雅地创建具有可变数量参数的 PreparedStatement。

例如。我有一个产品集合,我有很多查询参数

/accounts?categoryId=smth&order=asc&price=

问题是这些参数可以设置也可以不设置。

目前我有一些看起来像这样的东西,但我什至还没有开始清理用户输入

控制器

@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());

我可以改进吗?还是有更好的办法?

【问题讨论】:

【参考方案1】:

我发现上面的代码很难维护,因为它有复杂的逻辑来构建 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)";
    

【讨论】:

这种方法的另一个问题是我失去了参数的类型安全性。作为收藏资源,我必须分页。每次我查询参数映射时,我都必须检查它是 LIMIT 还是 OFFSET 并将参数值转换为 long。我必须为每个允许的参数附加一组规则。我是不是把这个弄得太复杂了? 是的,我认为你让它变得复杂了:)。您在原始方法中也失去了类型安全,这主要是因为您收到一个 Map 作为参数。为此,您可以尝试@QueryParam,但 namedParameters 映射仍然必须是 Map。如果您想使地图类型安全,那么您将失去编写适用于所有参数的通用代码。【参考方案2】:

经过一番思考,我认为类型安全很重要,因此我决定在整个 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());

【讨论】:

以上是关于spring 将可选查询参数映射到 sql 准备语句的主要内容,如果未能解决你的问题,请参考以下文章

python如何将可选参数或关键字参数从一个函数传递到另一个函数?

getopt 不将可选参数解析为参数

如何将可选参数传递给typescript中的回调函数

是否有必要在将可选参数传递给另一个可选参数之前检查它?

定义后无法将可选参数传递给express

如何将可选参数传递给 C++ 中的方法?