DEBUG.WIKI
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
雨云;给项目一台云服务器
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →

Jdbi 批量绑定首行 NULL,下一行 LocalDate 却找不到转换器

Jdbi 3.52.0 的 PreparedBatch 可能从首行 NULL 推断出 Object,后续 LocalDate 因找不到 argument factory 而失败。为每行显式 bindByType,并核对包含正式修正的版本。

同一条批量 INSERT,先绑定 LocalDate、下一行绑定 NULL 可以执行;把 NULL 放到第一行,第二行却报:

UnableToCreateStatementException:
No argument factory registered for '2024-06-01'
of qualified type java.lang.Object

这符合 Jdbi 3.52.0 的 PreparedBatch 首行空值问题。虽然目标列是 DATE,报错里的 Object 来自客户端的参数类型推断,不表示数据库把日期列变成了 Object。

第一行的类型提示被后续行复用

PreparedBatch 会根据首行的绑定准备参数处理方式。普通 bind 收到 NULL 时,无法从值本身取得 LocalDate 类型,因而会落到未声明类型的空值处理路径。

旧的部分 argument factory 会在随后拿到非空值时,再从实际值修正 Object 类型;Java 时间类型改用另一种可预备工厂后,这个补救没有发生。于是下一行明明有 LocalDate,查找时仍带着 Object 提示,最终找不到匹配的转换器。

这也解释了为什么调整行序会改变结果。但把所有非空行移到前面不适合作为长期处理:数据可能整批为空,也可能换成其他需要明确类型的字段,排序还会改变应用约定的写入次序。

在每一行绑定时声明 LocalDate

需要兼容该入口时,可用 bindByType 给出与模型一致的类型。下面的方法接收应用现有的 Handle,SQL 中的 t 是已有测试表或业务表,列定义应与参数匹配:

import java.time.LocalDate;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.statement.PreparedBatch;

static int[] insertDates(Handle handle) {
    PreparedBatch batch = handle.prepareBatch(
        "INSERT INTO t (id, d) VALUES (:id, :d)");
    batch.bind("id", 1)
         .bindByType("d", null, LocalDate.class)
         .add();
    batch.bind("id", 2)
         .bindByType("d", LocalDate.of(2024, 6, 1), LocalDate.class)
         .add();
    return batch.execute();
}

两个 id 与日期只是说明先空后非空的样本,不能直接当作生产默认数据执行。示例不新建或关闭传入的 Handle,也不擅自提交事务;调用方继续按原来的事务入口管理整批写入。

显式类型应在空值和非空值两条路径保持一致。如果模型使用 Instant、OffsetDateTime 或自定义类型,应按那个真实类型和已注册的工厂声明,而不是为消除异常全部转换成字符串。

正式修正与回归范围

正式 Jdbi 3.54.0 的发行记录包含 PreparedBatch 首次空值绑定修正。通用的 Arguments.findFor() 在类型提示仍为 Object、值却已经非空时,会按值的实际类型查找工厂,并保留原类型限定注解;需要缓存预备转换时,也按这个修正后的类型登记。升级时应统一相关 Jdbi 模块,继续保留项目自己的数据库驱动和时间语义检查,不能只在一个独立工具中替换 core jar。

验收至少包含 NULL→LocalDate、LocalDate→NULL、全部 NULL 和连续非空行,并读回每一行的值。显式 bindByType 可以作为模型边界保留,但不应因此掩盖其他参数类型不一致。

批次抛错后,先按现有事务约定回滚或确认实际结果,再决定是否重试。不同驱动的批处理执行进度可能不同,不能因为看到的是客户端异常,就承诺数据库完全没有执行任何行。