SQLException 上的事务回滚使用新的“尝试资源”块

2022-08-31 17:15:46

我在尝试资源方面遇到了问题,我要求只是为了确定。如果我需要对异常做出反应,并且我仍然需要 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-final,即使根据oracle文档 - “在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);应该在最后的条款中完成。这意味着外部的 try-with-resources 应该是一个传统的 try-catch-finally。

编辑 (2018)

我看到人们仍然对此发表评论,所以我想我会给它一个2018年的回复。我不再在Java中工作,主要是在Scala,Clojure和Kotlin中工作,并且此代码尚未经过测试,因此请将其视为另一个示例。但是,由于Java具有lambdas,我认为以下方法要好得多。我在这些其他语言的生产代码中也做过类似的事情。

在这种方法中,有一个 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;
        });
    }
}

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

我看到有几条关于自动提交和连接池的评论。上面的示例应该与连接的来源无关,无论是否是池,即只有在它是初始值时才将其设置回true。因此,如果从池中它是假的,则不应触摸它。

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


答案 2

在代码中,您正在捕获“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(Connection conn = getConnection(),
        AutoSetAutoCommit a = new AutoSetAutoCommit(conn,false),
        AutoRollback tm = new AutoRollback(conn)) 
    {

        // Do stuff

        tm.commit();
    } 

推荐