有没有更简洁的方法来使用 try-with-resource 和 PreparedStatement?
Is there a cleaner way to use try-with-resource and 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 数据库,因为它是一个基于文件的数据库,易于创建和测试。
所以一切正常,资源按预期清理,但我只是觉得可能有一种更简洁的方法可以从 try-with-resources 块内部设置 PreparedStatement
参数(我不想要使用嵌套的 try/catch 块,因为它们看起来 'awkward')。也许 JDBC 中已经存在一个帮手 class 可以做到这一点,但我找不到。
最好使用 lambda 函数来初始化 PreparedStatement
,但它仍然需要分配一个 AutoCloseable
对象,以便它可以在 try-with-resources 中。
首先,你的 PreparedStatementSetter
class 很尴尬:
- 它是类型class但未使用该类型。
- 构造函数正在显式调用可覆盖方法,which is a bad practice。
改为考虑以下界面(灵感来自同名的 Spring interface)。
public interface PreparedStatementSetter {
void setValues(PreparedStatement ps) throws SQLException;
}
此接口定义了 PreparedStatementSetter
应该做什么的约定:设置 PreparedStatement
的值,仅此而已。
那么,PreparedStatement
的创建和初始化最好在一个方法中完成。考虑在 DatabaseManager
class:
中加入这个
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 表达式写的。这是使用接口而不是抽象的优点之一 class:在这种情况下它实际上是一个功能接口(因为只有一个抽象方法),因此可以写成 lambda。
从@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
通过 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 之外,因为您可能想让它传播。
此解决方案的灵感来自 Design Patterns in the Light of Lambda Expressions by Subramaniam。
我发现了另一种可能对人们有帮助的方法:
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();
}
}
}
虽然这不能处理您可能需要 return 来自插入或事务的 ID 的情况,但它确实提供了一种快速的方法来 运行 查询、设置参数和获取结果集,在我的例子中是大部分的数据库代码。
一个更简洁的解决方案就是嵌套您的 try-with-resource 调用
try (Connection connection = dataSource.getConnection())
{
try (PreparedStatement stmt = connection.prepareStatement(query))
{
stmt.setString(1, value);
try (ResultSet resultSet = stmt.executeQuery()) {
// tickle your data here...
}
}
}
catch (Exception e) {
log.error("Oopsie daisy!", e);
}
这里是 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 数据库,因为它是一个基于文件的数据库,易于创建和测试。
所以一切正常,资源按预期清理,但我只是觉得可能有一种更简洁的方法可以从 try-with-resources 块内部设置 PreparedStatement
参数(我不想要使用嵌套的 try/catch 块,因为它们看起来 'awkward')。也许 JDBC 中已经存在一个帮手 class 可以做到这一点,但我找不到。
最好使用 lambda 函数来初始化 PreparedStatement
,但它仍然需要分配一个 AutoCloseable
对象,以便它可以在 try-with-resources 中。
首先,你的 PreparedStatementSetter
class 很尴尬:
- 它是类型class但未使用该类型。
- 构造函数正在显式调用可覆盖方法,which is a bad practice。
改为考虑以下界面(灵感来自同名的 Spring interface)。
public interface PreparedStatementSetter {
void setValues(PreparedStatement ps) throws SQLException;
}
此接口定义了 PreparedStatementSetter
应该做什么的约定:设置 PreparedStatement
的值,仅此而已。
那么,PreparedStatement
的创建和初始化最好在一个方法中完成。考虑在 DatabaseManager
class:
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 表达式写的。这是使用接口而不是抽象的优点之一 class:在这种情况下它实际上是一个功能接口(因为只有一个抽象方法),因此可以写成 lambda。
从@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
通过executeWork
静态方法 定义了一些与 ResultSetHandler
定义在通过新的executeQuery
实例方法执行查询时必须如何处理ResultSet
。
DatabaseManager
相关的工作
可以这样使用:
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 之外,因为您可能想让它传播。
此解决方案的灵感来自 Design Patterns in the Light of Lambda Expressions by Subramaniam。
我发现了另一种可能对人们有帮助的方法:
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();
}
}
}
虽然这不能处理您可能需要 return 来自插入或事务的 ID 的情况,但它确实提供了一种快速的方法来 运行 查询、设置参数和获取结果集,在我的例子中是大部分的数据库代码。
一个更简洁的解决方案就是嵌套您的 try-with-resource 调用
try (Connection connection = dataSource.getConnection())
{
try (PreparedStatement stmt = connection.prepareStatement(query))
{
stmt.setString(1, value);
try (ResultSet resultSet = stmt.executeQuery()) {
// tickle your data here...
}
}
}
catch (Exception e) {
log.error("Oopsie daisy!", e);
}