Is there a cleaner way to use try-with-resource an

2020-06-03 04:59发布

Here is the 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();
        }
    }
}

And here is 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", "");
    }
}

I am using H2 database for simplicity since it's a file based one that is easy to create and test on.

So everything works and resources get cleaned up as expected, however I just feel there may be a cleaner way to set the PreparedStatement parameters from inside the try-with-resources block (and I don't want to use nested try/catch blocks as those look 'awkward'). Maybe there already exists a helper class in JDBC that does just this, but I have not been able to find one.

Preferably with a lambda function to initialize the PreparedStatement but it would still require allocating an AutoCloseable object so it can be inside the try-with-resources.

3条回答
劳资没心,怎么记你
2楼-- · 2020-06-03 05:43

I found another way of doing this which may be helpful to people:

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

While this doesn't handle cases where you may need to return an ID from insert or transactions, it does offer a quick way to run a query, set parameters and get a ResultSet, which in my case is bulk of the DB code.

查看更多
做自己的国王
3楼-- · 2020-06-03 05:49

First off, your PreparedStatementSetter class is awkward:

  • it is a typed class but the type is not used.
  • the constructor is explicitly calling an overridable method, which is a bad practice.

Consider the following interface instead (inspired from the Spring interface of the same name).

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

This interface defines a contract of what a PreparedStatementSetter is supposed to do: set values of a PreparedStatement, nothing more.

Then, it would be better to do the creation and initialization of the PreparedStatement inside a single method. Consider this addition inside your DatabaseManager class:

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

With this static method, you can then write:

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

Notice how the PreparedStatementSetter was written here with a lambda expression. That's one of the advantage of using an interface instead of an abstract class: it actually is a functional interface in this case (because there is a single abstract method) and so can be written as a lambda.

查看更多
Emotional °昔
4楼-- · 2020-06-03 05:51

Extending from @Tunaki's answer, it's also possible to factor-in the try-with-resources and rs.executeQuery() such that the DatabaseManager handles all of this for you and only asks for the SQL, a PreparedStatementSetter and a ResultSet handler.

This would avoid repeating this everywhere you make a query. Actual API will depend on your usage however – e.g. will you make several queries with the same connection?

Supposing you will, I propose the following:

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

It wraps the connection as an instance field of DatabaseManager, which will handle the life-cycle of the connection, thanks to its implementation of AutoCloseable.

It also defines 2 new functional interfaces (additionally to @Tunaki's PreparedStatementSetter) :

  • Work defines some work to do with a DatabaseManager via the executeWork static method
  • ResultSetHandler defines how the ResultSet must be handled when executing a query via the new executeQuery instance method.

It can be used as follows:

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

As you see, you don't have to worry about resource handling any more.

I left SQLException handling outside the api since you might want to let it propagate.

This solution was inspired by Design Patterns in the Light of Lambda Expressions by Subramaniam.

查看更多
登录 后发表回答