当前位置:Java -> 如何在Java脚本中有效地使用JDBC工作

如何在Java脚本中有效地使用JDBC工作

我想向你介绍一个名为SqlExecutor的Java类,代码行数不到170行,用于通过JDBC API调用SQL查询以简化工作。这个解决方案有什么特别之处呢?这个类可以嵌入到Java版本17的脚本中。

使用Java脚本

Java脚本的优势在于以文本格式轻松可移植,并且可以在无需事先编译的情况下运行,同时在运行时可以利用语言标准库中丰富的资源。使用脚本适用于各种原型,甚至可以解决更加复杂的数据导出或数据转换问题(连接到数据库之后)。脚本在任何我们不希望(或者不能)将实现内容放入标准Java项目的地方都非常有用。

不过,使用脚本也有一些限制。例如,代码必须写在一个单独的文件中。我们可以在运行脚本时包含所有必要的库,但这些库可能有额外的依赖,而只是在命令行中列出它们可能会很令人沮丧。分发这样的脚本可能会带来复杂性。基于上述原因,我认为最好避免在脚本中使用外部库。如果我们仍然希望选择脚本方式,最好选择纯JDBC。我们可以优势地使用多行文本字面量编写SQL查询,并且可以自动关闭像PreparedStatement(实现AutoCloseable接口)这样的对象。那问题出在哪里呢?

映射SQL参数值

出于安全考虑,建议将SQL参数值映射到问号。我认为JDBC的主要障碍是使用问号(从1开始)的序列号码映射参数。第一版参数映射到SQL脚本通常会很顺利,但随着参数数量和额外SQL修改的增加,错误的风险也会增加。需要提醒的是,在第一个位置插入新参数时,后续行必须重新编号。

另一个复杂性在于使用IN运算符,因为对于枚举的每个值,在SQL模板中必须写一个问号,这必须映射到一个单独的参数。如果参数列表是动态的,SQL模板中问号的列表也必须是动态的。调试更多更复杂的SQL可能会耗费大量时间。

通过使用String模板插入SQL参数,我们需要更长时间等待。但是,可以通过简单封装接口PreparedStatement来简化插入SQL参数。在调用SQL语句之前,这个封装器将使用JPA风格的命名标签(以冒号开头的字母文本)附加参数。如果封装器还允许将所需方法链接成单个语句,最好具有返回类型Stream

SqlParamBuilder类

对SQL命令及其参数的可视化有时对调试或记录SQL查询是有用的。我向你介绍类SqlParamBuilder。实际的实现优先采用了一个单一Java类,代码极简化。编程接口灵感来自库JDBI。示例使用H2数据库的内存模式。但是,连接数据库驱动是必要的。

void mainStart(Connection dbConnection) throws Exception {
    try (var builder = new SqlParamBuilder(dbConnection)) {
        System.out.println("# CREATE TABLE");
        builder.sql("""
                        CREATE TABLE employee
                        ( id INTEGER PRIMARY KEY
                        , name VARCHAR(256) DEFAULT 'test'
                        , code VARCHAR(1)
                        , created DATE NOT NULL )
                        """)
                .execute();

        System.out.println("# SINGLE INSERT");
        builder.sql("""
                        INSERT INTO employee
                        ( id, code, created ) VALUES
                        ( :id, :code, :created )
                        """)
                .bind("id", 1)
                .bind("code", "T")
                .bind("created", someDate)
                .execute();

        System.out.println("# MULTI INSERT");
        builder.sql("""
                        INSERT INTO employee
                        (id,code,created) VALUES
                        (:id1,:code,:created),
                        (:id2,:code,:created)
                        """)
                .bind("id1", 2)
                .bind("id2", 3)
                .bind("code", "T")
                .bind("created", someDate.plusDays(7))
                .execute();
        builder.bind("id1", 11)
                .bind("id2", 12)
                .bind("code", "V")
                .execute();

        System.out.println("# SELECT");
        List<Employee> employees = builder.sql("""
                        SELECT t.id, t.name, t.created
                        FROM employee t
                        WHERE t.id < :id
                          AND t.code IN (:code)
                        ORDER BY t.id
                        """)
                .bind("id", 10)
                .bind("code", "T", "V")
                .streamMap(rs -> new Employee(
                        rs.getInt("id"),
                        rs.getString("name"),
                        rs.getObject("created", LocalDate.class)))
                .toList();

        System.out.printf("# PRINT RESULT OF: %s%n", builder.toStringLine());
        employees.stream()
                 .forEach((Employee employee) -> System.out.println(employee));
        assertEquals(3, employees.size());
        assertEquals(1, employees.get(0).id);
        assertEquals("test", employees.get(0).name);
        assertEquals(someDate, employees.get(0).created);
    }
}

record Employee (int id, String name, LocalDate created) {}
static class SqlParamBuilder {…}


使用注意事项和最终思考

对类型SqlParamBuilder的实例可以重复使用于多个SQL语句。调用命令后,参数可以被更改,然后再次运行命令。参数会分配给最后使用的Object PreparedStatement。

  • 方法sql()会自动关闭内部对象PrepradedStatement(如果之前有一个已打开)。
  • 如果我们更改参数组(通常是对于IN运算符),需要为相同PreparedStatement发送相同数量。否则,需要使用方法againsql()。
  • 在最后一个命令执行后需要一个对象显式关闭SqlParamBuilder。然而,由于实现了接口AutoCloseable,只需将整个块置于try块中。关闭不会影响包含的数据库连接。
  • 在Bash shell中,可以使用脚本SqlExecutor.sh运行示例,这个脚本可以下载必要的JDBC驱动(这里是H2数据库的驱动)。
  • 如果我们偏爱Kotlin,可以尝试Bash脚本SqlExecutorKt.sh,它将准备好的Kotlin代码迁移到一个脚本并运行它。
  • 不用被这个类存储在Maven类型项目中所困惑。其中一个原因是方便运行JUnit测试。
  • 这个类是根据Apache License,Version 2.0许可的。

创建自己的实现最快的方法可能就是下载示例脚本,重新设计方法mainRun(),并将连接参数修改为自己的数据库。使用自己的JDBC驱动运行。

推荐阅读: 百度面经(19)

本文链接: 如何在Java脚本中有效地使用JDBC工作