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经过一番思考,我认为类型安全很重要,因此我决定在整个 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 准备语句的主要内容,如果未能解决你的问题,请参考以下文章