Java 有没有更干净的方法来使用try with resource和PreparedStatement?

Java 有没有更干净的方法来使用try with resource和PreparedStatement?,java,jdbc,lambda,java-8,try-with-resources,Java,Jdbc,Lambda,Java 8,Try With Resources,下面是Main.java: package foo.sandbox.db; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(String[] args) { final String SQL

下面是
Main.java

package foo.sandbox.db;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Main {
    public static void main(String[] args) {
        final String SQL = "select * from NVPAIR where name=?";
        try (
                Connection connection = DatabaseManager.getConnection();
                PreparedStatement stmt = connection.prepareStatement(SQL);
                DatabaseManager.PreparedStatementSetter<PreparedStatement> ignored = new DatabaseManager.PreparedStatementSetter<PreparedStatement>(stmt) {
                    @Override
                    public void init(PreparedStatement ps) throws SQLException {
                        ps.setString(1, "foo");
                    }
                };
                ResultSet rs = stmt.executeQuery()
        ) {
            while (rs.next()) {
                System.out.println(rs.getString("name") + "=" + rs.getString("value"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
为了简单起见,我使用H2数据库,因为它是一个基于文件的数据库,易于创建和测试

因此,一切正常,资源得到了预期的清理,但是我觉得可能有一种更干净的方法可以从try with resources块内部设置
PreparedStatement
参数(我不想使用嵌套的try/catch块,因为它们看起来“笨拙”)。也许JDBC中已经存在一个帮助器类来实现这一点,但我还没有找到一个


最好使用lambda函数来初始化
PreparedStatement
,但它仍然需要分配一个
AutoCloseable
对象,以便它可以在try with resources中使用。

首先,您的
PreparedStatementSetter
类很笨拙:

  • 它是一个类型化类,但未使用该类型
  • 构造函数正在显式调用可重写的方法
请考虑以下接口(灵感来源于同名接口)

此接口定义了
PreparedStatementSetter
应该做什么的约定:设置
PreparedStatement
的值,仅此而已

然后,最好在单个方法中创建和初始化
PreparedStatement
。考虑在<代码>数据库管理员> <代码>类:

中的这个添加
public static PreparedStatement prepareStatement(Connection connection, String sql, PreparedStatementSetter setter) throws SQLException {
    PreparedStatement ps = connection.prepareStatement(sql);
    setter.setValues(ps);
    return ps;
}
使用此静态方法,您可以编写:

try (
    Connection connection = DatabaseManager.getConnection();
    PreparedStatement stmt = DatabaseManager.prepareStatement(connection, SQL, ps -> ps.setString(1, "foo"));
    ResultSet rs = stmt.executeQuery()
) {
    // rest of code
}

请注意,
PreparedStatementSetter
是如何在这里使用lambda表达式编写的。这是使用接口而不是抽象类的优点之一:在本例中,它实际上是一个函数接口(因为只有一个抽象方法),因此可以编写为lambda。

从@Tunaki的答案扩展,还可以考虑使用资源和
rs.executeQuery()进行尝试
这样
DatabaseManager
就可以为您处理所有这些,并且只需要SQL、一个
PreparedStatementSetter
和一个
ResultSet
处理程序

这样可以避免在您进行查询的任何地方重复此操作。然而,实际的API将取决于您的使用情况–例如,您是否会使用相同的连接进行多个查询

如果你愿意,我提议如下:

public class DatabaseManager implements AutoCloseable {

    /* Use local file for database */
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL";

    static {
        try {
            Class.forName("org.h2.Driver");  // Init H2 DB driver
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private final Connection connection;

    private DatabaseManager() throws SQLException {
        this.connection = getConnection();
    }

    @Override
    public void close() throws SQLException {
        connection.close();
    }

    public interface PreparedStatementSetter {
        void setValues(PreparedStatement ps) throws SQLException;
    }

    public interface Work {
        void doWork(DatabaseManager manager) throws SQLException;
    }

    public interface ResultSetHandler {
        void process(ResultSet resultSet) throws SQLException;
    }

    /**
     * @return Database connection
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_CONNECTION, "su", "");
    }

    private PreparedStatement prepareStatement(String sql, PreparedStatementSetter setter) throws SQLException {
        PreparedStatement ps = connection.prepareStatement(sql);
        setter.setValues(ps);
        return ps;
    }

    public static void executeWork(Work work) throws SQLException {
        try (DatabaseManager dm = new DatabaseManager()) {
            work.doWork(dm);
        }
    }

    public void executeQuery(String sql, PreparedStatementSetter setter, ResultSetHandler handler) throws SQLException {
        try (PreparedStatement ps = prepareStatement(sql, setter);
            ResultSet rs = ps.executeQuery()) {
            handler.process(rs);
        }
    }
}
它将连接包装为
DatabaseManager
的一个实例字段,由于实现了
AutoCloseable
,该实例字段将处理连接的生命周期

它还定义了2个新的功能接口(除了@Tunaki的
PreparedStatementSetter
):

  • Work
    通过
    executeWork
    静态方法定义一些与
    DatabaseManager
    相关的工作
  • ResultSetHandler
    定义通过新的
    executeQuery
    实例方法执行查询时必须如何处理
    ResultSet
它可以按如下方式使用:

    final String SQL = "select * from NVPAIR where name=?";
    try {
        DatabaseManager.executeWork(dm -> {
            dm.executeQuery(SQL, ps -> ps.setString(1, "foo"), rs -> {
                while (rs.next()) {
                    System.out.println(rs.getString("name") + "=" + rs.getString("value"));
                }
            });
            // other queries are possible here
        });
    } catch (Exception e) {
        e.printStackTrace();
    }
如您所见,您不必担心任何资源处理问题 更多

我将
SQLException
处理放在api之外,因为您可能希望让它传播


此解决方案的灵感来自。

我找到了另一种可能对人们有用的方法:

PreparedStatementExecutor.java:

/**
 * Execute PreparedStatement to generate ResultSet
 */
public interface PreparedStatementExecutor {
    ResultSet execute(PreparedStatement pstmt) throws SQLException;
}
PreparedStatementSetter.java:

/**
 * Lambda interface to help initialize PreparedStatement
 */
public interface PreparedStatementSetter {
    void prepare(PreparedStatement pstmt) throws SQLException;
}
jdbctree.java:

/**
 * Contains DB objects that close when done
 */
public class JdbcTriple implements AutoCloseable {
    Connection connection;
    PreparedStatement preparedStatement;
    ResultSet resultSet;

    /**
     * Create Connection/PreparedStatement/ResultSet
     *
     * @param sql String SQL
     * @param setter Setter for PreparedStatement
     * @return JdbcTriple
     * @throws SQLException
     */
    public static JdbcTriple create(String sql, PreparedStatementSetter setter) throws SQLException {
        JdbcTriple triple = new JdbcTriple();
        triple.connection = DatabaseManager.getConnection();
        triple.preparedStatement = DatabaseManager.prepareStatement(triple.connection, sql, setter);
        triple.resultSet = triple.preparedStatement.executeQuery();
        return triple;
    }

    public Connection getConnection() {
        return connection;
    }

    public PreparedStatement getPreparedStatement() {
        return preparedStatement;
    }

    public ResultSet getResultSet() {
        return resultSet;
    }

    @Override
    public void close() throws Exception {
        if (resultSet != null)
            resultSet.close();
        if (preparedStatement != null)
            preparedStatement.close();
        if (connection != null)
            connection.close();
    }
}
DatabaseManager.java:

/**
 * Initialize script
 * -----
 * CREATE TABLE NVPAIR;
 * ALTER TABLE PUBLIC.NVPAIR ADD value VARCHAR2 NULL;
 * ALTER TABLE PUBLIC.NVPAIR ADD id int NOT NULL AUTO_INCREMENT;
 * CREATE UNIQUE INDEX NVPAIR_id_uindex ON PUBLIC.NVPAIR (id);
 * ALTER TABLE PUBLIC.NVPAIR ADD name VARCHAR2 NOT NULL;
 * ALTER TABLE PUBLIC.NVPAIR ADD CONSTRAINT NVPAIR_name_pk PRIMARY KEY (name);
 *
 * INSERT INTO NVPAIR(name, value) VALUES('foo', 'foo-value');
 * INSERT INTO NVPAIR(name, value) VALUES('bar', 'bar-value');
 */
public class DatabaseManager {
    /* Use local file for database */
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL";

    static {
        try {
            Class.forName("org.h2.Driver");  // Init H2 DB driver
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @return Database connection
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_CONNECTION, "su", "");
    }

    /** Prepare statement */
    public static PreparedStatement prepareStatement(Connection conn, String SQL, PreparedStatementSetter setter) throws SQLException {
        PreparedStatement pstmt = conn.prepareStatement(SQL);
        setter.prepare(pstmt);
        return pstmt;
    }

    /** Execute statement */
    public static ResultSet executeStatement(PreparedStatement pstmt, PreparedStatementExecutor executor) throws SQLException {
        return executor.execute(pstmt);
    }
}
Main.java:

public class Main {
    public static void main(String[] args) {
        final String SQL = "select * from NVPAIR where name=?";
        try (
            JdbcTriple triple = JdbcTriple.create(SQL, pstmt -> { pstmt.setString(1, "foo"); })
        ){
            while (triple.getResultSet().next()) {
                System.out.println(triple.getResultSet().getString("name") + "=" + triple.getResultSet().getString("value"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虽然这不能处理您可能需要从insert或事务返回ID的情况,但它确实提供了一种快速运行查询、设置参数和获取结果集的方法,在我的例子中,结果集是数据库代码的大部分。

可能重复的我希望找到一种方法,使用lambda而不是类实例来执行init for PreparedStatement,几乎就像将参数流式传输到PreparedStatement中一样。我相信您的代码具有@Trejkaz在这里提到的相同缺陷:-setter.setValues(ps)引发的异常绕过了本地构造的PreparedStatement的返回,因此它没有关闭。
/**
 * Initialize script
 * -----
 * CREATE TABLE NVPAIR;
 * ALTER TABLE PUBLIC.NVPAIR ADD value VARCHAR2 NULL;
 * ALTER TABLE PUBLIC.NVPAIR ADD id int NOT NULL AUTO_INCREMENT;
 * CREATE UNIQUE INDEX NVPAIR_id_uindex ON PUBLIC.NVPAIR (id);
 * ALTER TABLE PUBLIC.NVPAIR ADD name VARCHAR2 NOT NULL;
 * ALTER TABLE PUBLIC.NVPAIR ADD CONSTRAINT NVPAIR_name_pk PRIMARY KEY (name);
 *
 * INSERT INTO NVPAIR(name, value) VALUES('foo', 'foo-value');
 * INSERT INTO NVPAIR(name, value) VALUES('bar', 'bar-value');
 */
public class DatabaseManager {
    /* Use local file for database */
    private static final String JDBC_CONNECTION = "jdbc:h2:file:./db/sandbox_h2.db;MODE=PostgreSQL";

    static {
        try {
            Class.forName("org.h2.Driver");  // Init H2 DB driver
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @return Database connection
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(JDBC_CONNECTION, "su", "");
    }

    /** Prepare statement */
    public static PreparedStatement prepareStatement(Connection conn, String SQL, PreparedStatementSetter setter) throws SQLException {
        PreparedStatement pstmt = conn.prepareStatement(SQL);
        setter.prepare(pstmt);
        return pstmt;
    }

    /** Execute statement */
    public static ResultSet executeStatement(PreparedStatement pstmt, PreparedStatementExecutor executor) throws SQLException {
        return executor.execute(pstmt);
    }
}
public class Main {
    public static void main(String[] args) {
        final String SQL = "select * from NVPAIR where name=?";
        try (
            JdbcTriple triple = JdbcTriple.create(SQL, pstmt -> { pstmt.setString(1, "foo"); })
        ){
            while (triple.getResultSet().next()) {
                System.out.println(triple.getResultSet().getString("name") + "=" + triple.getResultSet().getString("value"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}