【问题标题】:How should I use try-with-resources with JDBC?我应该如何在 JDBC 中使用 try-with-resources?
【发布时间】:2011-12-25 08:59:23
【问题描述】:

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

【问题讨论】:

  • 在你的第二个例子中,你不需要内部的try (ResultSet rs = ps.executeQuery()) {,因为A ResultSet object is automatically closed by the Statement object that generated it
  • @AlexanderFarber 不幸的是,驱动程序无法自行关闭资源存在臭名昭著的问题。 Hard Knocks 教我们始终明确关闭所有 JDBC 资源,使用 ConnectionPreparedStatementResultSet 周围的 try-with-resources 也更容易。没有理由不这样做,因为 try-with-resources 使它变得如此简单,并使我们的代码更加自我记录我们的意图。

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


【解决方案1】:

我意识到这个问题很久以前就得到了回答,但我想提出一种额外的方法来避免嵌套的 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;
}

【讨论】:

  • 不,它被覆盖了,问题是上面的代码是从一个没有声明抛出 SQLException 的方法内部调用 prepareStatement。此外,上面的代码至少有一个路径,它可以在不关闭准备好的语句的情况下失败(如果在调用 setInt 时发生 SQLException。)
  • @Trejkaz 关于不关闭 PreparedStatement 的可能性的好点。我没有想到,但你是对的!
  • @ArturoTena 是的 - 保证订单
  • @JeanneBoyarsky 还有其他方法吗?如果不是,我需要为每个 sql 语句创建一个特定的 createPreparedStatement 方法
  • 关于 Trejkaz 的评论,createPreparedStatement 无论您如何使用它都是不安全的。要修复它,您必须在 setInt(...) 周围添加一个 try-catch,捕获任何 SQLException,当它发生时调用 ps.close() 并重新抛出异常。但这将导致代码几乎与 OP 想要改进的代码一样长且不优雅。
【解决方案2】:

在您的示例中不需要外部尝试,因此您至少可以从 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;
}

【讨论】:

  • 你怎么称呼Connection::setAutoCommit?在con = ps = 之间的try 内不允许这样的调用。当从可能由连接池支持的 DataSource 获取连接时,我们不能假设 autoCommit 是如何设置的。
  • 您通常会将连接注入方法中(与 OP 问题中显示的临时方法不同),您可以使用将被调用以提供或关闭连接的连接管理类(无论是汇集与否)。在该管理器中,您可以指定您的连接行为
  • @BasilBourque 您可以将DriverManager.getConnection(myConnectionURL) 移动到一个方法中,该方法还设置自动提交标志并返回连接(或将其设置为与前面示例中的createPreparedStatement 方法等效...)
  • @rogerdpack 是的,这是有道理的。拥有自己的DataSource 实现,其中getConnection 方法按照您的说法执行,获取连接并根据需要进行配置,然后传递连接。
  • @rogerdpack 感谢您在答案中的澄清。我已将此更新为所选答案。
【解决方案3】:

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

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】:

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

DataSource

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

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

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

嵌套的 try-with-resources

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

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

如果从内部抛出异常,并且没有在那里捕获,ResultSet 资源将自动关闭(如果存在,则不为空)。之后,PreparedStatement 将关闭,最后Connection 将关闭。资源会按照它们在 try-with-resource 语句中声明的相反顺序自动关闭。

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

语法说明

尾随分号

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

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

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

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

【讨论】:

    【解决方案5】:

    这是使用 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()) {
    }
    

    【讨论】:

    • 这比@bpgergo 描述的“经典方法”更简洁?我不这么认为,代码更难理解。所以请解释一下这种方法的优点。
    • 我认为,在这种情况下,您不需要明确地捕获 SQLException。它实际上是 try-with-resources 上的“可选”。没有其他答案提到这一点。因此,您可能可以进一步简化。
    • 如果 DriverManager.getConnection(JDBC_URL, prop);返回 null?
    • 1.这不是更简洁,而是更令人困惑,2.它也有像“PreparedStatement s”资源仍然泄漏并且没有关闭的“提取方法答案”的问题,当“@987654323”出现异常时@" 调用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-26
    • 1970-01-01
    • 1970-01-01
    • 2021-08-03
    • 1970-01-01
    相关资源
    最近更新 更多