web-dev-qa-db-fra.com

Restauration de transaction sur SQLException à l'aide du nouveau bloc try-with-resources

J'ai un problème avec try-with-resources et je demande juste pour être sûr. Puis-je l'utiliser, si j'ai besoin de réagir sur exception, et j'ai toujours besoin de la ressource dans le bloc catch? Voici un exemple donné:

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
}

Je crains que je ne sois toujours condamné à utiliser l'ancien try-catch-finalement dans ce cas, même selon la documentation Oracle - "catch et enfin les blocs dans une instruction try-with-resources, tout catch ou finalement le bloc est exécuté après les ressources déclarées ont été fermées. "

42
Jan Hruby

Selon les spécifications linguistiques, la connexion sera fermée avant l'exécution de la clause catch ( http://docs.Oracle.com/javase/specs/jls/se7/html/jls-14.html#jls- 14.20.3.2 ).

Une solution possible consiste à imbriquer les instructions 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);
}

J'espère que cela illustre le point. Cela devrait être amélioré un peu si vous prévoyez de l'utiliser dans le code de production.

Par exemple, si vous utilisez un pool de connexions, vous devez renvoyer la connexion telle que vous l'avez obtenue, donc con.setAutoCommit (true); devrait être fait dans une clause finale. Cela signifierait que le try-with-resources externe devrait être un try-catch traditionnel finalement.

Modifier (2018)

Je vois encore des gens commenter cela, alors j'ai pensé lui donner une réponse en 2018. Je ne travaille plus en Java plus, je travaille principalement en Scala, Clojure et Kotlin, et ce code n'a pas été testé, veuillez donc le traiter comme un autre exemple. Cependant, depuis Java a des lambdas, je pense que l'approche suivante est bien meilleure. Et j'ai fait des choses similaires dans le code de production dans ces autres langages.

Dans cette approche, il existe une fonction inTransaction qui gère tous les trucs de transaction désagréables. Mais l'utilisation est assez simple.

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

J'espère qu'il y a des bibliothèques testées au combat qui font ce genre de choses pour vous, il y en a au moins dans ces autres langues.

Je vois qu'il y a plusieurs commentaires concernant l'autocommit et les pools de connexions. Les exemples ci-dessus doivent être indépendants de l'origine de la connexion, d'un pool ou non, c'est-à-dire de la remettre à true si c'est sa valeur initiale. Donc, si d'une piscine c'est faux, il ne faut pas le toucher.

Un dernier mot sur les essais avec les ressources. Je ne pense pas que ce soit une très bonne abstraction, donc je serais prudent de l'utiliser dans des scénarios plus complexes.

48
Alf

Dans votre code, vous capturez "SQLException" pour effectuer la réinitialisation autoCommit. N'importe quel type d'exception d'exécution (comme une exception de pointeur nul) sortira de votre code sans réinitialiser la validation automatique.

La syntaxe try-with-resource oblige le compilateur à générer du code merveilleux pour couvrir tous les chemins d'exécution et suivre toutes les exceptions supprimées à travers les fermetures. Avec quelques classes d'assistance, vous pouvez insérer commit/rollback et reset-auto-commit dans le processus de génération de code:

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

}

Vous pouvez maintenant contrôler la restauration et la validation automatique avec la syntaxe "essayer avec la ressource" comme ceci:

    try(Connection conn = getConnection(),
        AutoSetAutoCommit a = new AutoSetAutoCommit(conn,false),
        AutoRollback tm = new AutoRollback(conn)) 
    {

        // Do stuff

        tm.commit();
    } 
19
ChrisCantrell
    //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
        }
    }
8
Jaskey

Dans l'exemple ci-dessus, je pense qu'il vaut mieux mettre con.commit() à l'intérieur imbriqué try-catch car il peut également lancer 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);
        }
    }

Nous avons eu un tel problème dans notre environnement de production avec des sessions non fermées.

5
shambalaxx

Vous pouvez affecter la référence du bloc d'essai "con" à une autre référence de méthode "connexion".

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