16

我最近和我的教授讨论了如何处理基本的 jdbc 连接方案。假设我们要执行两个查询,这是他提出的

public void doQueries() throws MyException{
    Connection con = null;
    try {
        con = DriverManager.getConnection(dataSource);
        PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
        PreparedStatement s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

        rs.close();
        s2.close();
        s1.close();
    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't really do anything
        }
    }
}

我不喜欢这种方法,对此我有两个问题:

1.A)我认为,如果在我们做“其他事情”的地方抛出任何异常,或者在该行中,或者在rs.close()方法结束s2.close()s1不会被关闭。我说得对吗?

1.B)教授一直要求我明确关闭结果集(即使声明文档明确表示它将关闭结果集)她说 Sun 推荐它。有什么理由这样做吗?

现在这是我认为是同一件事的正确代码:

public void doQueries() throws MyException{
    Connection con = null;
    PreparedStatement s1 = null;
    PreparedStatement s2 = null;
    try {
        con = DriverManager.getConnection(dataSource);
        s1 = con.prepareStatement(updateSqlQuery);
        s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (s2 != null) {
                s2.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (s1 != null) {
                s1.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't do nothing
        }
    }
}

2.A)这段代码正确吗?(是否保证方法结束时全部关闭?)

2.B)这是非常大且冗长的(如果有更多的语句,它会变得更糟)是否有任何更短或更优雅的方法可以在不使用 try-with-resources 的情况下做到这一点?

最后这是我最喜欢的代码

public void doQueries() throws MyException{
    try (Connection con = DriverManager.getConnection(dataSource);
         PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
         PreparedStatement s2 = con.prepareStatement(selectSqlQuery))
    {

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    }
}

3)这段代码正确吗?我认为我的教授不喜欢这种方式,因为没有明确关闭 ResultSet,但她告诉我,只要在文档中清楚地表明所有内容都已关闭,她就可以接受。您能否提供任何带有类似示例的官方文档链接,或者根据文档显示此代码没有问题?

4

6 回答 6

21

tl;博士

  • 理论上关闭语句会关闭结果集。
  • 在实践中,众所周知,一些错误的 JDBC 驱动程序实现未能做到这一点。因此,她从硬敲门学校学到的教练的建议。除非您熟悉可能为您的应用程序部署的每个JDBC 驱动程序的每个实现,否则请使用try-with-resources自动关闭您的JDBC工作的每个级别,例如语句和结果集。

使用 try-with-resources 语法

您的代码都没有完全使用try-with-resources。在 try-with-resources 语法中,您在大括号之前声明并实例化您的Connection, PreparedStatement, 和ResultSet括号。请参阅Oracle 教程

虽然您ResultSet在上一个代码示例中没有显式关闭,但它应该在其语句关闭时间接关闭。但如下所述,它可能由于 JDBC 驱动程序错误而无法关闭

AutoCloseable

任何实现的此类对象 都将自动调用AutoCloseable其方法。close所以不需要那些finally条款。

对于阅读本文的人文学科专业的学生来说,是的,Java 团队拼错了“可关闭”。

你怎么知道哪些对象是可自动关闭的,哪些不是?查看他们的类文档,看看它是否声明AutoCloseable为超级接口。相反,请参阅JavaDoc 页面以AutoCloseable获取所有捆绑的子接口和实现类的列表(实际上有几十个)。

例如,对于 SQL 工作,我们看到 、ConnectionStatementPreparedStatementResultSet都是RowSet可自动关闭的,但DataSource不是。这是有道理的,因为DataSource存储有关潜在资源(数据库连接)的数据,但它本身不是资源。ADataSource永远不会“打开”,因此无需关闭。

请参阅 Oracle 教程,try-with-resources 语句

代码示例

您的最后一个代码示例已经接近良好,但应该包含ResultSet在 try-with-resources 语句中以自动关闭。

引用ResultSetJavaDoc:

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

正如您的老师所建议的那样,一些 JDBC 驱动程序存在严重缺陷,未能兑现 JDBC 规范的承诺,即ResultSet在其Statement或关闭时PreparedStatement关闭。如此多的程序员养成了ResultSet显式关闭每个对象的习惯。

现在使用 try-with-resources 语法可以更轻松地完成这项额外任务。在实际工作中,您可能会对所有AutoCloseable对象进行尝试,例如ResultSet无论如何。所以我自己的意见是:为什么不把它变成一个try-with-resources + else呢?没有伤害,使您的代码更能自我记录您的意图,如果您的代码遇到这些错误的 JDBC 驱动程序之一,它可能会有所帮助。唯一的成本是一对 parens,假设无论如何你都会有一个 try-catch-else 。

Oracle 教程中所述,一起声明的多个AutoCloseable对象将以相反的顺序关闭,就像我们想要的那样。

提示:try-with-resources 语法允许在最后声明的资源项上使用可选的分号。我将分号作为一种习惯包括在内,因为它读起来很好,是一致的,并且便于剪切和粘贴编辑。我把它包括在你的PreparedStatement s2线上。

public void doQueries() throws MyException{
    // First try-with-resources.
    try ( Connection con = DriverManager.getConnection( dataSource ) ;
          PreparedStatement s1 = con.prepareStatement( updateSqlQuery ) ;
          PreparedStatement s2 = con.prepareStatement( selectSqlQuery ) ;
    ) {

        … Set parameters of PreparedStatements, etc.

        s1.executeUpdate() ;

        // Second try-with-resources, nested within first.
        try (
            ResultSet rs = s2.executeQuery() ;
        ) {
            … process ResultSet
        } catch ( SQLException e2 ) {  
            … handle exception related to ResultSet.
        }

    } catch ( SQLException e ) {  
        … handle exception related to Connection or PreparedStatements.
    }
}

我想这种工作可能会在未来的编程语言中发明一种更优雅的语法。但是现在,我们有 try-with-resources,我很乐意使用它。虽然 try-with-resources 并不完美,但它比旧语法有了很大的改进。

顺便说一句,Oracle 建议使用一种DataSource实现来获取连接,而不是DriverManager在您的代码中看到的方法。DataSource在整个代码中使用可以更轻松地切换驱动程序或切换到连接池。查看您的 JDBC 驱动程序是否提供了DataSource.

更新:Java 9

现在在 Java 9 中,您可以在 try-with-resources之前初始化资源。这篇文章。这种灵活性在某些情况下可能很有用。

于 2017-07-16T22:12:56.583 回答
7

JDBC 代码的有趣之处在于,您编写的规范并不总是很清楚您的实现是否符合规范。有很多不同的数据库和驱动程序,有些驱动程序比其他驱动程序表现更好。这往往使人们在谨慎方面犯错,建议明确关闭所有内容。您可以只关闭此处的连接。为了安全起见而关闭结果集是很难争论的。您没有在此处指明您使用的是什么数据库或驱动程序,我不想硬编码对某些实现可能无效的驱动程序的假设。

按顺序关闭事物确实会让您面临可能引发异常并导致跳过某些关闭的问题。你担心这一点是对的。

请注意,这是一个玩具示例。大多数实际代码使用连接池,其中调用 close 方法实际上并没有关闭连接,而是将连接返回到池中。因此,一旦您使用池,资源可能不会关闭。如果您想更改此代码以使用连接池,那么您至少必须返回并关闭语句。

此外,如果您反对这种冗长,那么答案就是将代码隐藏在使用策略、结果集映射器、准备好的语句设置器等的可重用实用程序中。当然,这一切都已经完成了;您将走上重塑 Spring JDBC 的道路。

说到这一点:Spring JDBC 显式地关闭了所有内容(可能是因为它需要与尽可能多的驱动程序一起工作,并且不希望由于某些驱动程序的表现不佳而引起问题)。

于 2014-03-26T20:17:12.300 回答
2

这是我发现处理 JDBC 等资源的最佳解决方案。此方法提供了一个不可变的功能,通过利用最终变量,并且仅在需要时声明和分配这些变量,它的 CPU 效率非常高,并保证在所有情况下,无论状态如何,分配和打开的所有资源都是关闭的的例外。如果不仔细实施以解决所有情况,您正在使用的技术会留下可能导致资源泄漏的空白。如果始终遵循以下模式,此技术不允许资源泄漏:
1) 分配资源
2) 尝试
3) 使用资源
4) 最后关闭资源

public void doQueries() throws MyException {
   try {
      final Connection con = DriverManager.getConnection(dataSource);
      try {
         final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
         try {

            // Set the parameters of the PreparedStatements and maybe do other things

            s1.executeUpdate();

         } finally {
            try { s1.close(); } catch (SQLException e) {}
         }

         final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
         try {

            // Set the parameters of the PreparedStatements and maybe do other things

            final ResultSet rs = s2.executeQuery();
            try {

               // Do something with rs

            } finally {
               try { rs.close(); } catch (SQLException e) {}
            }
         } finally {
            try { s2.close(); } catch (SQLException e) {}
         }
      } finally {
         try { con.close(); } catch (SQLException e) {}
      }
   } catch (SQLException e) {
      throw new MyException(e);
   }
}

在 Java 7 中,您可以利用新的 try -with-resources 来进一步简化这一过程:新的 try -with-resources 遵循上述逻辑流程,因为它将保证所有资源都包含在分配 get 的 with resources 块中关闭。在 with resources 块中抛出的任何异常都将被抛出,但那些分配的资源仍将被关闭。这段代码非常简化,看起来像这样:

public void doQueries() throws MyException {
   try (
      final Connection con = DriverManager.getConnection(dataSource);
      final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
      final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
      final ResultSet rs = s2.executeQuery()) {

      s1.executeUpdate();

         // Do something with rs

   } catch (SQLException e) {
      throw new MyException(e);
   }
}

[编辑]:将 rs 分配移动到资源块中以显示最简单的实现。在实践中,这个简单的解决方案并没有真正起作用,因为它效率不高。应该重用连接,因为建立连接是一项非常昂贵的操作。此外,这个简单的示例没有将查询参数分配给准备好的语句。应该小心处理这些场景,因为资源块应该只包含赋值语句。为了描述这一点,我还添加了另一个示例

   public void doQueries() throws MyException {

      final String updateSqlQuery = "select @@servername";
      final String selecSqlQuery  = "select * from mytable where col1 = ? and col2 > ?";
      final Object[] queryParams  = {"somevalue", 1};

      try (final Connection con = DriverManager.getConnection(dataSource);
         final PreparedStatement s1 = newPreparedStatement(con, updateSqlQuery);
         final PreparedStatement s2 = newPreparedStatement(con, selectSqlQuery, queryParams);
         final ResultSet rs = s2.executeQuery()) {

         s1.executeUpdate();

         while (!rs.next()) {
            // do something with the db record.
         }
      } catch (SQLException e) {
         throw new MyException(e);
      }
   }

   private static PreparedStatement newPreparedStatement(Connection con, String sql, Object... args) throws SQLException
   {
      final PreparedStatement stmt = con.prepareStatement(sql);
      for (int i = 0; i < args.length; i++)
         stmt.setObject(i, args[i]);
      return stmt;
   }
于 2015-10-22T17:22:47.397 回答
1

这确实是 try-with-resources 的主要动机。请参阅 Java教程作为参考。你的教授已经过时了。如果您想处理结果集问题,您始终可以将其包含在另一个 try-with-resources 语句中。

于 2014-03-26T20:05:13.857 回答
0

您可以创建一个 util 类来处理这些资源的关闭。即仅供参考,我只是在尝试关闭 util 类中的资源时忽略了 SQLExceptions,但是您也可以在完成关闭集合中的资源后根据您的需要记录或收集并抛出它们

public class DBUtil {
public static void closeConnections(Connection ...connections){
    if(connections != null ){
        for(Connection conn : connections){
            if(conn != null){
                try {
                    conn.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

public static void closeResultSets(ResultSet ...resultSets){
    if(resultSets != null ){
        for(ResultSet rs: resultSets){
            if(rs != null){
                try {
                    rs.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

public static void closeStatements(Statement ...statements){
    if(statements != null){
        for(Statement statement : statements){
            if(statement != null){
                try {
                    statement.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

}

然后从你的方法中调用它:

    public void doQueries() throws MyException {
    Connection con = null;
    try {
        con = DriverManager.getConnection(dataSource);
        PreparedStatement s1 = null;
        PreparedStatement s2 = null;
        try {
            s1 = con.prepareStatement(updateSqlQuery);
            s2 = con.prepareStatement(selectSqlQuery);

            // Set the parameters of the PreparedStatements and maybe do other things
            s1.executeUpdate();
            ResultSet rs = null;
            try {
                rs = s2.executeQuery();
            } finally {
                DBUtil.closeResultSets(rs);
            }
        } finally {
            DBUtil.closeStatements(s2, s1);
        }

    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        DBUtil.closeConnections(con);
    }
}
于 2014-03-26T20:34:57.417 回答
0

我更喜欢让 Java 自动关闭。所以当我必须为 ResultSet 设置值时,我会这样做。

try (Connection conn = DB.getConn();
     PreparedStatement ps = conn.prepareStatement("SELECT * FROM x WHERE y = ?")
) {
    ps.setString(1, "yValue");
   
    try(ResultSet rs = ps.executeQuery()) {
        while(rs.next()) {
            ...
        }
    }
} catch (SQLException e) {
    e.printStackTrace(e);
    ...
}
于 2021-03-24T19:17:45.523 回答