升到 Spring Framework 7.1(随 Boot 4.2.0-RC1)后,JdbcTemplate 带 batchSize 的多批 batchUpdate 被标成 deprecated。JdbcClient 补了 entry-based 批量 DSL,一次 update() 返回扁平的 int[]。老项目如果按「每批再套一层」读 int[][],改完先吃编译警告,循环结构也会挂。

这和早年 JdbcClient 与 JdbcTemplate 对照 里「批量还得掉回 template」的结论不一样了。Framework 7.1.0-RC1 发行说明写明:Introduce entry-based DSL for JdbcClient batch updates,同时 Deprecate multi-batch batchUpdate variant in JdbcTemplate。Boot 4.2 与 Framework 7.1 同列车,见 Patch Thursday 快讯。

版本:JDK 21.0.12.1、Spring Boot 4.2.0-RC1、Spring Framework 7.1.0-RC1、H2 2.5.252。依赖 spring-boot-starter-jdbc + 嵌入式 H2,表:

CREATE TABLE product (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(64) NOT NULL,
  qty INT NOT NULL
)

旧写法:多批 batchUpdate,返回 int[][]

很多老代码用这个签名把大集合按固定大小切开,返回值是「批的数组」:

int[][] multi = jdbcTemplate.batchUpdate(
    "INSERT INTO product (name, qty) VALUES (?, ?)",
    products,          // 5 条
    2,                 // batchSize
    (ps, p) -> {
        ps.setString(1, p.name());
        ps.setInt(2, p.qty());
    });

Framework 7.1 起该方法 @Deprecated(since = "7.1")。Javadoc 写:in favor of single batchUpdate calls which execute the entire batch。编译期原样警告:

WARNING] BatchDemoApplication.java:[55,53] <T>batchUpdate(java.lang.String,java.util.Collection<T>,int,org.springframework.jdbc.core.ParameterizedPreparedStatementSetter<T>) in org.springframework.jdbc.core.JdbcTemplate has been deprecated

同库实测,5 条、batchSize=2,日志和返回值:

Executing SQL batch update [INSERT INTO product (name, qty) VALUES (?, ?)] with a batch size of 2
old.multiBatch.batchSize=2 inputSize=5 batches=3
old.multiBatch.batch[0]=[1, 1]
old.multiBatch.batch[1]=[1, 1]
old.multiBatch.batch[2]=[1]
old.multiBatch.rowCount=5

三层批:前两批各 2 行,最后一批 1 行。调用方如果写 for (int[] batch : multi) 再进内层,依赖的就是这个嵌套形状。

新写法:JdbcClient entry DSL,返回扁平 int[]

同一张表换成 JdbcClient。命名参数用 entry(Consumer) 或 entry(Map),位置参数用 entry(List),一批对象可用 entries(list):

int[] namedRows = jdbcClient.sql(
        "INSERT INTO product (name, qty) VALUES (:name, :qty)")
    .batch()
    .entry(e -> e.param("name", "alpha").param("qty", 1))
    .entry(e -> e.param("name", "bravo").param("qty", 2))
    .entry(Map.of("name", "charlie", "qty", 3))
    .update();

int[] posRows = jdbcClient.sql(
        "INSERT INTO product (name, qty) VALUES (?, ?)")
    .batch()
    .entry(List.of("delta", 4))
    .entry(List.of("echo", 5))
    .update();

int[] entriesRows = jdbcClient.sql(
        "INSERT INTO product (name, qty) VALUES (:name, :qty)")
    .batch()
    .entries(products)   // record / bean / Map
    .update();
new.named.entry.update=[1, 1, 1]
new.named.rowCount=3
new.positional.entry.update=[1, 1]
new.entries(list).update=[1, 1, 1, 1, 1]
new.entries(list).len=5

没有 batchSize 参数。一次 update() 就是一整批 JDBC batch,返回值和条数对齐的 int[]。DEBUG 里只剩一句 Executing SQL batch update [...],不再出现 with a batch size of N。

迁移时会挂的几处

1. 返回值形状。 旧代码 int[][] 外层按「切出来的批」迭代;新 API 是扁平 int[]。把 multi.length 当「批次数」用的逻辑,改完后变成「行次数」。5 条旧多批是 batches=3;同 5 条 entries 是 len=5。

2. 切批要自己做。 Framework 不打算在 JdbcClient 里复刻多批变体。需要限流时,调用方切 list 再多次 .batch().entries(chunk).update():

List<Product> chunk = products.subList(0, 2);
int[] chunkRows = jdbcClient.sql(
        "INSERT INTO product (name, qty) VALUES (:name, :qty)")
    .batch()
    .entries(chunk)
    .update();
// migrate.callerChunk.size=2 update=[1, 1]

3. 命名和位置不能混。 同一 batch 里先 entry(e -> e.param(...)) 再 entry(List.of(...)):

new.mixed.batch.ex=java.lang.IllegalStateException: Configure either named or indexed parameters for all batch entries, not both

4. 空 entry 会抛;空 batch 不会。 回调里一个参数都不绑:

new.empty.entry.ex=java.lang.IllegalStateException: Configure at least one parameter for each batch entry

完全没有 entry 就 update(),这次跑通返回空数组:

new.empty.batch.update=[] len=0

生成主键这边 API 支持。H2 上 update(KeyHolder, "id") 实测拿到键:

new.keys.update=[1, 1]
new.keys.list=[{ID=16}, {ID=17}]
new.keys.size=2

对照结论就一句:升 Framework 7.1 / Boot 4.2 后,带 batchSize 的多批 batchUpdate 先告警;迁到 JdbcClient entry DSL 时,把嵌套 int[][] 和框架内切批一起清掉,改成扁平 int[],切批留在业务侧。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。