带有IN子句的Spring batchUpdate 解决方案

问题描述

给出一个简单的表

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]);

将在第一批arg长度之外创建一个动态sql

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