我应该如何在JDBC中使用try-with-resources?

时间:2011-11-09 14:31:14

标签: java jdbc java-7 try-with-resources

我有一种使用JDBC从数据库中获取用户的方法:

public List<User> getUser(int userId) {
    String sql = "SELECT id, name FROM users WHERE id = ?";
    List<User> users = new ArrayList<User>();
    try {
        Connection con = DriverManager.getConnection(myConnectionURL);
        PreparedStatement ps = con.prepareStatement(sql); 
        ps.setInt(1, userId);
        ResultSet rs = ps.executeQuery();
        while(rs.next()) {
            users.add(new User(rs.getInt("id"), rs.getString("name")));
        }
        rs.close();
        ps.close();
        con.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}

我应该如何使用Java 7 try-with-resources来改进此代码?

我已尝试使用下面的代码,但它使用了许多try块,并且没有提高可读性。我应该以另一种方式使用try-with-resources吗?

public List<User> getUser(int userId) {
    String sql = "SELECT id, name FROM users WHERE id = ?";
    List<User> users = new ArrayList<>();
    try {
        try (Connection con = DriverManager.getConnection(myConnectionURL);
             PreparedStatement ps = con.prepareStatement(sql);) {
            ps.setInt(1, userId);
            try (ResultSet rs = ps.executeQuery();) {
                while(rs.next()) {
                    users.add(new User(rs.getInt("id"), rs.getString("name")));
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}

5 个答案:

答案 0 :(得分:172)

我意识到这很久以前已经回答了,但我想建议一种额外的方法来避免嵌套的try-with-resources双重阻塞。

public List<User> getUser(int userId) {
    try (Connection con = DriverManager.getConnection(myConnectionURL);
         PreparedStatement ps = createPreparedStatement(con, userId); 
         ResultSet rs = ps.executeQuery()) {

         // process the resultset here, all resources will be cleaned up

    } catch (SQLException e) {
        e.printStackTrace();
    }
}

private PreparedStatement createPreparedStatement(Connection con, int userId) throws SQLException {
    String sql = "SELECT id, username FROM users WHERE id = ?";
    PreparedStatement ps = con.prepareStatement(sql);
    ps.setInt(1, userId);
    return ps;
}

答案 1 :(得分:67)

在您的示例中不需要外部尝试,因此您至少可以从3下降到2,并且您也不需要在资源列表的末尾关闭;。使用两个try块的优点是您的所有代码都是预先存在的,因此您不必引用单独的方法:

public List<User> getUser(int userId) {
    String sql = "SELECT id, username FROM users WHERE id = ?";
    List<User> users = new ArrayList<>();
    try (Connection con = DriverManager.getConnection(myConnectionURL);
         PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setInt(1, userId);
        try (ResultSet rs = ps.executeQuery()) {
            while(rs.next()) {
                users.add(new User(rs.getInt("id"), rs.getString("name")));
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}

答案 2 :(得分:3)

这是一个使用lambdas和JDK 8供应商的简洁方法,以适应外部尝试中的所有内容:

    try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
            PreparedStatement stmt = ((Supplier<PreparedStatement>)() -> {
                try {
                  PreparedStatement s = con.prepareStatement(
                        "SELECT userid, name, features FROM users WHERE userid = ?");
                  s.setInt(1, userid);
                  return s;
                } catch (SQLException e) { throw new RuntimeException(e); }
            }).get();
          ResultSet resultSet = stmt.executeQuery()) {
    }

答案 3 :(得分:2)

创建一个额外的包装类怎么样?

package com.naveen.research.sql;

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

public abstract class PreparedStatementWrapper implements AutoCloseable {

    protected PreparedStatement stat;

    public PreparedStatementWrapper(Connection con, String query, Object ... params) throws SQLException {
        this.stat = con.prepareStatement(query);
        this.prepareStatement(params);
    }

    protected abstract void prepareStatement(Object ... params) throws SQLException;

    public ResultSet executeQuery() throws SQLException {
        return this.stat.executeQuery();
    }

    public int executeUpdate() throws SQLException {
        return this.stat.executeUpdate();
    }

    @Override
    public void close() {
        try {
            this.stat.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


然后在调用类中,您可以将prepareStatement方法实现为:

try (Connection con = DriverManager.getConnection(JDBC_URL, prop);
    PreparedStatementWrapper stat = new PreparedStatementWrapper(con, query,
                new Object[] { 123L, "TEST" }) {
            @Override
            protected void prepareStatement(Object... params) throws SQLException {
                stat.setLong(1, Long.class.cast(params[0]));
                stat.setString(2, String.valueOf(params[1]));
            }
        };
        ResultSet rs = stat.executeQuery();) {
    while (rs.next())
        System.out.println(String.format("%s, %s", rs.getString(2), rs.getString(1)));
} catch (SQLException e) {
    e.printStackTrace();
}

答案 4 :(得分:2)

正如其他人所述,尽管不需要外部try,但是您的代码基本上是正确的。这里还有一些想法。

DataSource

此处的其他答案是正确且正确的,例如bpgergo的accepted Answer。但是没有一个显示使用DataSource的情况,在现代Java中,通常不建议使用DriverManager

因此,出于完整性考虑,下面是一个完整的示例,该示例从数据库服务器获取当前日期。此处使用的数据库为Postgres。任何其他数据库都将类似地工作。您可以将org.postgresql.ds.PGSimpleDataSource的使用替换为适合您的数据库的DataSource的实现。可能由您的特定驱动程序或连接池(如果您走那条路线)提供了一个实现。

DataSource实现无需关闭,因为它永远不会“打开”。 DataSource不是资源,没有连接到数据库,因此它既没有网络连接也没有数据库服务器上的资源。 DataSource只是与数据库建立连接时所需的信息,其中包含数据库服务器的网络名称或地址,用户名,用户密码以及最终建立连接时要指定的各种选项。因此,您的DataSource实现对象不会放入try-with-resources括号内。

嵌套的尝试资源

您的代码正确使用了嵌套的try-with-resources语句。

在下面的示例代码中请注意,我们还使用了try-with-resources语法两次,一个嵌套在另一个内部。外部try定义两个资源:ConnectionPreparedStatement。内部的try定义了ResultSet资源。这是一种常见的代码结构。

如果从内部抛出异常并且没有在其中捕获,则ResultSet资源将自动关闭(如果存在,则不为null)。之后,PreparedStatement将被关闭,最后Connection被关闭。在try-with-resource语句中以相反的顺序自动关闭资源。

此处的示例代码过于简单。如所写,可以使用单个try-with-resources语句执行。但是在实际工作中,您可能会在一对嵌套的try调用之间进行更多的工作。例如,您可能正在从用户界面或POJO中提取值,然后通过对?方法的调用将这些值传递给SQL中的PreparedStatement::set…个占位符。

语法注释

尾随分号

请注意,在try-with-resources括号内尾随最后一个resource语句的分号是可选的。我将其包含在我自己的工作中的原因有两个:一致性并且看起来很完整,并且使复制粘贴行更加容易,而不必担心行尾分号。您的IDE可能会将最后一个分号标记为多余,但保留它不会有任何危害。

Java 9 –在try-with-resources中使用现有的vars

New in Java 9是对try-with-resources语法的增强。现在,我们可以在try语句的括号之外声明和填充资源。我还没有发现这对JDBC资源有用,但是在您自己的工作中要牢记这一点。

ResultSet应该自行关闭,但可能不会关闭

在理想的世界中,ResultSet会按照文档中的说明自行关闭:

当ResultSet对象关闭,重新执行或用于从多个结果序列中检索下一个结果时,将自动关闭ResultSet对象。

不幸的是,过去臭名昭著的一些JDBC驱动程序未能实现这一承诺。结果,许多JDBC程序员学会了显式关闭其所有JDBC资源,包括ConnectionPreparedStatementResultSet。现代的try-with-resources语法使此操作变得更容易,并且代码更紧凑。注意,Java团队费心将ResultSet标记为AutoCloseable,我建议我们利用它。在所有JDBC资源周围使用try-with-resources,可以使代码根据自己的意图进行自我记录。

代码示例

package work.basil.example;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Objects;

public class App
{
    public static void main ( String[] args )
    {
        App app = new App();
        app.doIt();
    }

    private void doIt ( )
    {
        System.out.println( "Hello World!" );

        org.postgresql.ds.PGSimpleDataSource dataSource = new org.postgresql.ds.PGSimpleDataSource();

        dataSource.setServerName( "1.2.3.4" );
        dataSource.setPortNumber( 5432 );

        dataSource.setDatabaseName( "example_db_" );
        dataSource.setUser( "scott" );
        dataSource.setPassword( "tiger" );

        dataSource.setApplicationName( "ExampleApp" );

        System.out.println( "INFO - Attempting to connect to database: " );
        if ( Objects.nonNull( dataSource ) )
        {
            String sql = "SELECT CURRENT_DATE ;";
            try (
                    Connection conn = dataSource.getConnection() ;
                    PreparedStatement ps = conn.prepareStatement( sql ) ;
            )
            {
                … make `PreparedStatement::set…` calls here.
                try (
                        ResultSet rs = ps.executeQuery() ;
                )
                {
                    if ( rs.next() )
                    {
                        LocalDate ld = rs.getObject( 1 , LocalDate.class );
                        System.out.println( "INFO - date is " + ld );
                    }
                }
            }
            catch ( SQLException e )
            {
                e.printStackTrace();
            }
        }

        System.out.println( "INFO - all done." );
    }
}