扩展预编译 SQL 值处理器

Rabbit SQL 默认内置支持的值类型有限,而不同框架还会有各自的特殊值类型。如果每次都手动转换会比较麻烦,可以通过实现接口 com.github.chengyuxing.sql.plugins.StatementValueHandler 并配置到 BakiDao 来扩展:

public class MyStatementValueHandler implements StatementValueHandler {
    @Override
    public void handle(@NotNull PreparedStatement ps, @Range(from = 1, to = Integer.MAX_VALUE) int index, @Nullable Object value, @NotNull DatabaseInfo info) throws SQLException {
      // ...
      // 这是内部兜底实现,除非你完全自己处理参数,否则建议调用它
      JdbcUtils.setStatementValue(ps, index, value);
    }
}

例如处理 Spring Boot 的 MultipartFile。如果还有文件服务器或其他文件存储中间件,可以拦截所有文件类型,把文件保存到文件服务器,数据库只保存文件路径:

if (value instanceof MultipartFile) {
   try {
        ps.setBytes(index, ((MultipartFile) value).getBytes());
        return;
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

如果传入值是 Map 或 List,大多数数据库没有对应的原生类型,此时默认序列化为 JSON 比较合理:

if (value instanceof Map<?, ?> || value instanceof List<?>) {
   ps.setString(index, Jackson.toJson(value));
   return;
}

还可以根据不同的数据库来针对性的处理值:

if(info.getName().equals("postgresql")){
  // ...
}

在信创场景中,需要面对随时更换、种类繁多的国产数据库,这个扩展点会显得尤为重要。

二进制参数的资源管理

默认 JdbcUtils.setStatementValue(...) 对 File / Path 使用 Files.readAllBytes 后以 setBytes 绑定,文件内容会全部进入内存。上面的 MultipartFile#getBytes() 示例同样适合可接受内存占用的文件。

大文件可将已经打开的 InputStream 作为参数交给默认处理器,并在 SQL 执行结束后由调用方关闭:

try (InputStream content = Files.newInputStream(path)) {
    baki.execute("insert into files(content) values (:content)",
        Args.of("content", content));
}

不要在值处理器里打开流却丢失关闭责任,也不要在绑定参数后、执行 SQL 前关闭流。自定义处理器若打开流,需要由外层持有并管理到执行结束;批处理中的流也必须保持可用直到对应分批执行完成。