【问题标题】:Transaction rollback on SQLException using new try-with-resources block使用新的 try-with-resources 块在 SQLException 上回滚事务
【发布时间】:2013-03-23 14:39:02
【问题描述】:

我对 try-with-resources 有疑问,我只是想确定一下。如果我需要对异常做出反应,并且我仍然需要 catch 块中的资源,我可以使用它吗?给出的例子是这样的:

try (java.sql.Connection con = createConnection())
{
    con.setAutoCommit(false);
    Statement stm = con.createStatement();
    stm.execute(someQuery); // causes SQLException
}
catch(SQLException ex)
{
    con.rollback();
    // do other stuff
}

我担心在这种情况下我仍然注定要使用旧的 try-catch-finally,即使根据 oracle 文档 - “catch and finally blocks in a try-with-resources statement, any catch or finally block is run在声明的资源关闭之后。”

【问题讨论】:

  • 在这种情况下,如果连接本身失败,则没有回滚的意义。 con 的范围仅限于尝试阻止。
  • 这个问题也可能有帮助。 stackoverflow.com/questions/9260159/…
  • 在所有有趣的选项中,我还是更喜欢原来的try-catch-finally

标签: java try-with-resources


【解决方案1】:

根据语言规范,连接将在 catch 子句执行之前关闭 (http://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.20.3.2)。

一种可能的解决方案是嵌套 try-with-resources 语句:

try (java.sql.Connection con = createConnection())
{
    con.setAutoCommit(false);
    try (Statement stm = con.createStatement())
    {
        stm.execute(someQuery); // causes SQLException
    }
    catch(SQLException ex)
    {
        con.rollback();
        con.setAutoCommit(true);
        throw ex;
    }
    con.commit();
    con.setAutoCommit(true);
}

希望这能说明这一点。如果您打算在生产代码中使用它,这应该会有所改进。

例如,如果您正在使用连接池,那么您必须在获得连接时返回连接,因此 con.setAutoCommit(true);应该在 finally 子句中完成。这意味着外部的 try-with-resources 应该是传统的 try-catch-finally。

编辑(2018 年)

我仍然看到人们对此发表评论,所以我想我会在 2018 年回复它。我不再使用Java了,主要是使用Scala、Clojure和Kotlin,并且这段代码没有经过测试,所以请把这当作另一个例子。但是,由于 Java 有 lambda,我认为以下方法要好得多。我在其他语言的生产代码中也做过类似的事情。

在这种方法中,有一个 inTransaction 函数来处理所有令人讨厌的事务。但是用法很简单。

public class Foo {

    interface ConnectionProvider {
        Connection get() throws SQLException;
    }

    public static <A> A doInTransation(ConnectionProvider connectionProvider, Function<Connection, A> f) throws SQLException {
        Connection connection = null;
        A returnValue;
        boolean initialAutocommit = false;
        try {
            connection = connectionProvider.get();
            initialAutocommit = connection.getAutoCommit();
            connection.setAutoCommit(false);
            returnValue = f.apply(connection);
            connection.commit();
            return returnValue;
        } catch (Throwable throwable) {
            // You may not want to handle all throwables, but you should with most, e.g.
            // Scala has examples: https://github.com/scala/scala/blob/v2.9.3/src/library/scala/util/control/NonFatal.scala#L1
            if (connection != null) {
                connection.rollback();
            }
            throw throwable;
        } finally {
            if (connection != null) {
                try {
                    if(initialAutocommit){
                        connection.setAutoCommit(true);
                    }
                    connection.close();
                } catch (Throwable e) {
                    // Use your own logger here. And again, maybe not catch throwable,
                    // but then again, you should never throw from a finally ;)
                    StringWriter out = new StringWriter();
                    e.printStackTrace(new PrintWriter(out));
                    System.err.println("Could not close connection " + out.toString());
                }
            }
        }
    }

    public static void main(String[] args) throws SQLException {
        DataSource ds = null;

        // Usage example:
        doInTransation(ds::getConnection, (Connection c) -> {
            // Do whatever you want in a transaction
            return 1;
        });
    }
}

我希望有一些经过实战考验的库可以为你做这些事情,至少在这些其他语言中是这样的。

我看到有几个关于自动提交和连接池的 cmets。上面的例子应该不知道连接来自哪里,一个池与否,即只有当它是初始值时才将它设置回 true。因此,如果从池中它是错误的,则不应触摸它。

关于 try-with-resources 的最后一句话。我不认为这是一个很好的抽象,所以我会在更复杂的场景中小心使用它。

【讨论】:

  • 请注意,调用 setAutoCommit 会在任何待处理事务上调用 COMMIT。所以在更复杂的代码中要小心。
  • "这意味着外部的 try-with-resources 应该是传统的 try-catch-finally。" - 所以实际上“使用资源尝试”模式对于带有连接池的 JDBC 连接完全没用......?
  • “例如,如果您使用的是连接池,那么您必须在获得时返回连接,因此 con.setAutoCommit(true); 应该在 finally 子句中完成。” — 如果连接池遵循 JDBC 规范,那就不对了。根据规范,新签出的连接必须始终启用自动提交。即使在禁用自动提交的情况下签入连接,大多数连接池(如 c3p0)也会确保这一点。
  • @Alf 在最后一行,如果您刚刚提交并且之后立即关闭连接,为什么需要将自动提交设置为“true”?
  • 但你只能捕捉到SQLException。如果发生非检查异常,例如NullPointerException,该怎么办?你的交易永远不会回滚吗?
【解决方案2】:

在上面的示例中,我认为最好将con.commit() 放在嵌套的try-catch 中,因为它也可以抛出SQLException

 try (java.sql.Connection con = createConnection())
    {
        con.setAutoCommit(false);
        try (Statement stm = con.createStatement())
        {
            stm.execute(someQuery); // causes SQLException
            con.commit();           // also causes SQLException!
        }
        catch(SQLException ex)
        {
            con.rollback();
            throw ex;
        }finally{
            con.setAutoCommit(true);
        }
    }

我们在生产环境中遇到了这样的问题,会话未关闭。

【讨论】:

  • 您应该使用 finally 语句将自动提交设置回 true。这将阻止您两次使用此语句。
【解决方案3】:
    //try with resources
    try(Connection conn = this.connectionProvider.getConnection()){//auto close BEFORE reach this , catch block, so we need a inner try block for statement
        boolean oldAutoCommit=conn.getAutoCommit();
        conn.setAutoCommit(false);//auto commit to false
        try(
            Statement stm = con.createStatement()
        ){
            stm.execute(someQuery); // causes SQLException
            conn.commit();//commit
        }
        catch (SQLException ex){
            conn.rollback();//error, rollback
            throw ex;//If you need to throw the exception to the caller
        }
        finally {
            conn.setAutoCommit(oldAutoCommit);//reset auto commit
        }
    }

【讨论】:

    【解决方案4】:

    在您的代码中,您正在捕获“SQLException”来执行自动提交重置。任何类型的运行时异常(如空指针异常)都会从您的代码中冒泡,而无需重置自动提交。

    try-with-resource 语法使编译器生成一些精彩的代码来覆盖所有执行路径,并通过关闭来跟上所有被抑制的异常。使用几个帮助类,您可以在代码生成过程中插入提交/回滚和重置自动提交:

    import java.sql.SQLException;
    import java.sql.Connection;
    
    public class AutoRollback implements AutoCloseable {
    
        private Connection conn;
        private boolean committed;
    
        public AutoRollback(Connection conn) throws SQLException {
            this.conn = conn;        
        }
    
        public void commit() throws SQLException {
            conn.commit();
            committed = true;
        }
    
        @Override
        public void close() throws SQLException {
            if(!committed) {
                conn.rollback();
            }
        }
    
    }
    
    public class AutoSetAutoCommit implements AutoCloseable {
    
        private Connection conn;
        private boolean originalAutoCommit;
    
        public AutoSetAutoCommit(Connection conn, boolean autoCommit) throws SQLException {
            this.conn = conn;
            originalAutoCommit = conn.getAutoCommit();
            conn.setAutoCommit(autoCommit);
        }
    
        @Override
        public void close() throws SQLException {
            conn.setAutoCommit(originalAutoCommit);
        }
    
    }
    

    现在您可以使用“try with resource”语法来控制回滚和自动提交,如下所示:

        try(Connection conn = getConnection(),
            AutoSetAutoCommit a = new AutoSetAutoCommit(conn,false),
            AutoRollback tm = new AutoRollback(conn)) 
        {
    
            // Do stuff
    
            tm.commit();
        } 
    

    【讨论】:

    • 有趣的代码,我没有花时间尝试 AutoCloseable 接口。这似乎是替换 finally 子句的好方法。在生产中,代码会简单得多,只需一个 try-with-resource。不错!
    • 1.为什么不在 close() 方法中声明“抛出 SQLException”?为什么你只包装为一个例外? 2. 为什么不在一个 try 块中创建所有资源? AFAIK 他们会以相反的顺序关闭,你已经宣布了他们。
    • 非常好的点!我将更改关闭以抛出 SQL 异常并按照您和 AxelH 的建议将它们组合在一起。感谢您的反馈。
    • @Chris - 抱歉,我误解了您的答案,我认为您的重点是在我的示例中缺少异常处理,但我现在看到它是在自动提交上。无论如何,最初的问题与异常处理无关,而只是关于在 try 块的标题中定义的资源范围
    • 需要将 try-resource 中的 "," 替换为 ";"。至少这是我想要的。
    猜你喜欢
    • 2020-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-20
    • 2017-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多