可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答2:
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.
回答3:
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.