Есть ли более чистый способ использовать try-with-resource и PreparedStatement?

здесь 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();
        }
    }
}

а вот DatabaseManager.java

package foo.sandbox.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * 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 {
    /**
     * Class to allow PreparedStatement to initialize parmaters inside try-with-resource
     * @param <T> extends Statement
     */
    public static abstract class PreparedStatementSetter<T extends Statement> implements AutoCloseable {
        public PreparedStatementSetter(PreparedStatement pstmt) throws SQLException {
            init(pstmt);
        }

        @Override
        public void close() throws Exception {
        }

        public abstract void init(PreparedStatement pstmt) throws SQLException;
    }

    /* 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", "");
    }
}

я использую базу данных H2 для простоты, так как это файл, основанный на том, что легко создать и протестировать.

так что все работает и ресурсы очищаются, как ожидалось, однако я просто чувствую, что там может быть чище способ установить PreparedStatement параметры внутри блока try-with-resources (и я не хочу использовать вложенные блоки try/catch, как они выглядят "неловко"). Возможно, в JDBC уже существует вспомогательный класс, который делает именно это, но я не смог его найти.

предпочтительно с лямбда-функцией для инициализации PreparedStatement но это все равно потребует выделения AutoCloseable объект, поэтому он может быть внутри try-with-resources.

3 ответов


во-первых,PreparedStatementSetter класс-то неудобно:

вместо этого рассмотрим следующий интерфейс (вдохновленный Весна интерфейса с тем же именем).

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

этот интерфейс определяет контракт того, что PreparedStatementSetter предполагается сделать: установить значения PreparedStatement, больше ничего.

тогда было бы лучше сделать создание и инициализацию PreparedStatement внутри одного метода. Рассмотрим это дополнение внутри вашего DatabaseManager класс:

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 было написано здесь с лямбда-выражением. Это одно из преимуществ использования интерфейса вместо абстрактного класса: на самом деле это функциональный интерфейс в этот случай (потому что есть один абстрактный метод) и поэтому может быть записан как лямбда.


расширяясь от ответа @Tunaki, также можно учесть-в try-with-resources и 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 определяет некоторую работу, чтобы сделать с DatabaseManager через executeWork статический метод
  • ResultSetHandler определяется как ResultSet должен обрабатываться при выполнении запроса через новый executeQuery метод экземпляра.

его можно использовать следующим образом:

    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;
}

JdbcTriple.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 или транзакций, он предлагает быстрый способ запуска запроса, установки параметров и получите ResultSet, который в моем случае является основной частью кода DB.