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

166

我有一个使用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;
}

9
在你的第二个示例中,不需要内部的try (ResultSet rs = ps.executeQuery()) {,因为A ResultSet对象由生成它的Statement对象自动关闭 - Alexander Farber
6
很遗憾,存在一些臭名昭著的问题,即驱动程序未能自行关闭资源。摸爬滚打的经验告诉我们,总是要显式地关闭所有 JDBC 资源,并使用 try-with-resources 简化对 Connection、PreparedStatement 和 ResultSet 的操作。实际上没有理由不这样做,因为 try-with-resources 很容易使用,可以使我们的代码更加自我说明我们的意图。 - Basil Bourque
5个回答

205

我知道这个问题在很久以前就被回答了,但我想提出一种额外的方法,可以避免嵌套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;
}

29
不,它是有覆盖的,问题在于上面的代码在一个没有声明抛出 SQLException 的方法中调用了 prepareStatement。此外,上述代码至少存在一条路径,在该路径中,如果在调用 setInt 时出现 SQLException,则无法关闭准备好的语句。 - Hakanai
1
@Trejkaz,你提到的PreparedStatement可能没有关闭的可能性很有道理。我没有想到这一点,但你是对的! - Jeanne Boyarsky
2
@ArturoTena 是的 - 订单是有保证的。 - Jeanne Boyarsky
2
@JeanneBoyarsky 还有其他的方法吗?如果没有,我需要为每个 SQL 语句创建一个特定的 createPreparedStatement 方法。 - John Alexander Betts
1
关于Trejkaz的评论,“createPreparedStatement”无论如何使用都是不安全的。要修复它,您必须在setInt(...)周围添加try-catch,捕获任何SQLException,并在发生异常时调用ps.close()并重新抛出异常。但这将导致代码几乎与OP想要改进的代码一样冗长和不优雅。 - Florian F
显示剩余11条评论

104

在你的示例中,外部try是不必要的,因此你可以将其从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;
}

7
你如何称呼 Connection::setAutoCommit?在 con =ps = 之间的 try 块内不允许进行此调用。从可能由连接池支持的数据源获取连接时,我们无法假设自动提交设置为何值。 - Basil Bourque
1
通常情况下,您会将连接注入到方法中(与OP问题中显示的临时方法不同),您可以使用一个连接管理类来提供或关闭连接(无论是池化还是非池化)。在该管理器中,您可以指定连接的行为。 - svarog
1
@BasilBourque 您可以将 DriverManager.getConnection(myConnectionURL) 移入一个方法中,该方法还设置了 autoCommit 标志并返回连接(或在前面示例中的 createPreparedStatement 方法的等效位置中设置它...) - rogerdpack
@rogerdpack 是的,那很有道理。您可以拥有自己的DataSource实现,在getConnection方法中执行您所说的操作:获取连接并根据需要进行配置,然后传递连接。 - Basil Bourque
1
@rogerdpack 感谢您在回答中的澄清。我已将其更新为所选答案。 - Jonas
显示剩余3条评论

10

正如他人所述,您的代码基本上是正确的,只是外部的try是不必要的。以下是一些更多的想法。

DataSource

其他答案都是正确且好的,例如bpgergo的被接受的答案。但是其中没有一个显示使用DataSource,在现代Java中通常建议使用DriverManager的使用。

因此,为了完整起见,这里是一个完整的示例,从数据库服务器获取当前日期。此处使用的数据库是Postgres。任何其他数据库都可以类似地工作。您将使用org.postgresql.ds.PGSimpleDataSource来替换适合您的数据库的DataSource的实现。如果您走这条路,您的特定驱动程序或连接池可能会提供实现。

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

DataSource的目的是将数据库连接信息外部化。如果在源代码中硬编码用户名、密码等信息,则更改数据库服务器配置意味着必须重新编译和部署代码,这并不好玩。相反,这些数据库配置细节应该存储在源代码外部,然后在运行时检索。您可以通过JNDI从命名和目录服务器(如LDAP)检索配置详细信息。或者您可以从运行应用程序的Servlet容器Jakarta EE服务器检索。

嵌套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中提取值,然后通过调用PreparedStatement::set…方法将这些值传递以满足SQL中的?占位符。

语法说明

分号

请注意,在try-with-resources括号内的最后一个资源语句后面的分号是可选的。我在自己的工作中包含它有两个原因:一致性和看起来完整,这使得复制和粘贴混合行变得更容易,而无需担心行尾分号。您的IDE可能会将最后一个分号标记为多余的,但保留它没有任何危害。

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

Java 9新特性是try-with-resources语法的增强。现在我们可以在try语句的括号外声明和填充资源。我还没有发现它对JDBC资源有用,但要记住在您自己的工作中使用。

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

在理想情况下,ResultSet会像文档承诺的那样自动关闭:

当生成ResultSet对象的Statement对象关闭、重新执行或用于检索多个结果序列中的下一个结果时,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." );
    }
}

如果内部的代码抛出了异常,但是没有在内部进行捕获,那么它能被外部的代码块捕获吗? - Guilherme Taffarel Bergamin
2
@GuilhermeTaffarelBergamin 是的,这就是Java中异常的工作方式。如果本地代码未捕获它们,它们会“冒泡”到外部调用代码。冒泡继续通过所有被调用的方法,直到最终逃离您的应用程序并到达JVM以进行处理。 - Basil Bourque
谢谢。我的问题是因为您不需要为具有资源的尝试声明catch,它将执行隐式finally,这意味着此尝试具有其自己的特殊性。在我看来,它可能会忽略异常,因为没有catch,但有一个finally(通常发生在catch之后)。无论如何,感谢您的解释。 - Guilherme Taffarel Bergamin

5
创建一个额外的包装类怎么样?
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();
}


2
以上的评论从未说过它不是。 - Hakanai

3

这是使用Lambda表达式和JDK 8的Supplier来简洁地实现外部try语句中的所有内容的方法:

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

5
这种方法比@bpgergo描述的“传统方法”更为简洁吗?我不这么认为,而且代码更难以理解。因此,请解释一下这种方法的优点。 - rmuller
在这种情况下,我认为你不需要显式地捕获SQLException。在try-with-resources中,它实际上是“可选的”。没有其他答案提到这一点。因此,你可能可以进一步简化这个过程。 - djangofan
如果 DriverManager.getConnection(JDBC_URL, prop); 返回 null 呢? - Gaurav
1
  1. 这并不更简洁,但更加混乱。
  2. 它也存在“提取方法答案”的问题,即当“s.setInt”调用出现异常时,“PreparedStatement s”资源仍然泄漏且未关闭。
- Sebsen36

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接