问题描述
给出一个简单的表
create table car (
make varchar
model varchar
)
以及以下DAO代码
NamedParameterJdbcTemplate template;
String sql = "delete from car where make = :make and model in (:model)";
void batchDelete(final Map<String,Collection<String>> map) {
sqlParameterSource[] params = map.entrySet().stream()
.map(entry -> toParams(entry.getKey(),entry.getValue()))
.toArray(sqlParameterSource[]::new);
template.batchUpdate(sql,params);
}
void delete(final Map<String,Collection<String>> map) {
map.forEach((make,models) -> {
sqlParameterSource params = toParams(make,models);
template.update(sql,params);
});
}
sqlParameterSource toParams(final String make,final Collection<String> models) {
return new MapsqlParameterSource("make",make)
.addValue("model",new ArrayList<>(models));
}
当映射具有2个键,且这些键的批处理中的IN子句具有不同数量的值时,批处理删除功能将失败。假设Map.of
创建并排序了地图。
// runs fine - 2 values for each key
batchDelete(Map.of("VW",Arrays.asList("Polo","Golf"),"Toyota",Arrays.asList("Yaris","Camry")));
// fails - first key has 1 value,second key has 2 values
batchDelete(Map.of("Toyota",Arrays.asList("Yaris"),"VW","Golf")));
// runs fine - key with bigger list comes first
batchDelete(Map.of("VW",Arrays.asList("Yaris")));
// non batch delete runs fine either way
delete(Map.of("Toyota","Golf")));
Spring文档暗示了这一点
https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc-in-clause
sql标准允许基于包含变量值列表的表达式选择行。一个典型的示例是从T_ACTOR中选择*,其中id在(1、2、3)中。 JDBC标准不直接为准备好的语句支持此变量列表。您不能声明可变数量的占位符。您需要准备好所需数量的占位符的多种变体,或者一旦知道需要多少个占位符,就需要动态生成sql字符串。在NamedParameterJdbcTemplate和JdbcTemplate中提供的命名参数支持采用后一种方法。
错误消息是
The column index is out of range: 3,number of columns: 2.; nested exception is org.postgresql.util.PsqlException: The column index is out of range: 3,number of columns: 2.
NamedParameterJdbcTemplate # batchUpdate
中的以下行会发生什么:
PreparedStatementCreatorFactory pscf = getPreparedStatementCreatorFactory(parsedsql,batchArgs[0]);
delete from car where make = ? and model in (?)
因为只有1个占位符,所以具有2个模型的第二批项目将失败。
解决方案
回到简单的旧PreparedStatement
sql-使用ANY
代替IN
delete from car where make = ? and model = any (?)
道
Connection con;
PreparedStatement ps = con.prepareStatement("sql");
map.forEach((make,models) -> {
int col = 0;
ps.setString(++col,make);
ps.setArray(++col,con.createArrayOf("text",models));
ps.addBatch();
});
ps.executeBatch();
解决方法
我建议更改SQL,使其看起来像这样:
String SQL = "DELETE FROM car WHERE (make,model) IN (:ids)";
如果您这样做,则可以使用与我在此问题上给出的答案类似的方法:NamedJDBCTemplate Parameters is list of lists。这样做意味着您可以使用NamedParameterJdbcTemplate.update(String sql,Map<String,?> paramMap)
。在您的paramMap
中,键将是"ids"
,值将是Collection<Object[]>
的实例,其中集合中的每个条目都是一个包含要删除的值对的数组:>
List<Object[]> params = new ArrayList<>();//you can make this any instance of collection you want
for (Car car : cars) {
params.add(new Object[] { car.getMake(),car.getModel() });
//this is just to provide an example of what I mean,obviously this will probably be different in your app.
}