Comment utiliser try-with-resources avec JDBC?

148

J'ai une méthode pour obtenir des utilisateurs d'une base de données avec 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;
}

Comment utiliser Java 7 try-with-resources pour améliorer ce code?

J'ai essayé avec le code ci-dessous, mais il utilise de nombreux tryblocs et n'améliore pas beaucoup la lisibilité . Dois-je utiliser try-with-resourcesd'une autre manière?

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;
}
Jonas
la source
5
Dans votre deuxième exemple, vous n'avez pas besoin de l'intérieur try (ResultSet rs = ps.executeQuery()) {car un objet ResultSet est automatiquement fermé par l'objet Statement qui l'a généré
Alexander Farber
2
@AlexanderFarber Malheureusement, il y a eu des problèmes notoires avec les pilotes qui n'ont pas réussi à fermer les ressources par eux-mêmes. L'École de Hard Knocks nous apprend à toujours fermer toutes les ressources JDBC explicitement, plus facile à l' aide essayer avec-ressources autour Connection, PreparedStatementet ResultSetaussi. Aucune raison de ne pas vraiment le faire, car l'essai avec des ressources le rend si facile et rend notre code plus auto-documenté quant à nos intentions.
Basil Bourque le

Réponses:

85

Il n'y a pas besoin d'essayer externe dans votre exemple, donc vous pouvez au moins passer de 3 à 2, et vous n'avez pas non plus besoin de fermer ;à la fin de la liste des ressources. L'avantage d'utiliser deux blocs try est que tout votre code est présent à l'avance, vous n'avez donc pas à vous référer à une méthode distincte:

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;
}
bpgergo
la source
5
Comment appelez-vous Connection::setAutoCommit? Un tel appel n'est pas autorisé tryentre le con = et le ps =. Lors de l'obtention d'une connexion à partir d'un DataSource qui peut être sauvegardé avec un pool de connexions, nous ne pouvons pas supposer comment autoCommit est défini.
Basil Bourque
1
vous injecteriez généralement la connexion dans la méthode (contrairement à l'approche ad-hoc montrée dans la question d'OP), vous pourriez utiliser une classe de gestion de connexion qui sera appelée pour fournir ou fermer une connexion (qu'elle soit groupée ou non). dans ce gestionnaire, vous pouvez spécifier votre comportement de connexion
svarog
@BasilBourque vous pouvez passer DriverManager.getConnection(myConnectionURL)à une méthode qui définit également l'indicateur autoCommit et retourne la connexion (ou la définir dans l'équivalent de la createPreparedStatementméthode de l'exemple précédent ...)
rogerdpack
@rogerdpack Oui, cela a du sens. Ayez votre propre implémentation de l' DataSourceendroit où la getConnectionméthode fait ce que vous dites, obtenez la connexion et configurez-la si nécessaire, puis transmettez la connexion.
Basil Bourque
1
@rogerdpack merci pour la clarification dans la réponse. J'ai mis à jour ceci avec la réponse sélectionnée.
Jonas
187

Je me rends compte que cela a été répondu il y a longtemps, mais je veux suggérer une approche supplémentaire qui évite le double bloc try-with-resources imbriqué.

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;
}
Jeanne Boyarsky
la source
24
Non, c'est couvert, le problème est que le code ci-dessus appelle prepareStatement depuis une méthode qui ne déclare pas lancer SQLException. En outre, le code ci-dessus a au moins un chemin où il peut échouer sans fermer l'instruction préparée (si une exception SQLException se produit lors de l'appel de setInt.)
Trejkaz
1
@Trejkaz bon point sur la possibilité de ne pas fermer le PreparedStatement. Je n'y ai pas pensé, mais tu as raison!
Jeanne Boyarsky
2
@ArturoTena oui - la commande est garantie
Jeanne Boyarsky
2
@JeanneBoyarsky y a-t-il une autre façon de faire ça? Sinon, je devrais créer une méthode createPreparedStatement spécifique pour chaque phrase sql
John Alexander Betts
1
En ce qui concerne le commentaire de Trejkaz, il createPreparedStatementest dangereux quelle que soit la façon dont vous l'utilisez. Pour résoudre ce problème, vous devez ajouter un try-catch autour de setInt (...), en attraper SQLException, et quand cela se produit, appelez ps.close () et relancez l'exception. Mais cela aboutirait à un code presque aussi long et peu élégant que le code que l'OP voulait améliorer.
Florian F
4

Voici une manière concise d'utiliser lambdas et le fournisseur JDK 8 pour tout adapter à l'essai extérieur:

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()) {
}
inder
la source
5
C'est plus concis que "l'approche classique" décrite par @bpgergo? Je ne pense pas et le code est plus difficile à comprendre. Veuillez donc expliquer l'avantage de cette approche.
rmuller
Je ne pense pas, dans ce cas, que vous deviez intercepter explicitement l'exception SQLException. C'est en fait "facultatif" sur un essai avec des ressources. Aucune autre réponse ne le mentionne. Donc, vous pouvez probablement simplifier cela davantage.
djangofan
et si DriverManager.getConnection (JDBC_URL, prop); renvoie null?
gaurav
2

Qu'en est-il de la création d'une classe wrapper supplémentaire?

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


Ensuite, dans la classe appelante, vous pouvez implémenter la méthode prepareStatement comme:

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

Naveen Sisupalan
la source
2
Rien dans le commentaire ci-dessus ne dit jamais que non.
Trejkaz
2

Comme d'autres l'ont indiqué, votre code est fondamentalement correct bien que l'extérieur ne trysoit pas nécessaire. Voici quelques réflexions supplémentaires.

DataSource

D'autres réponses ici sont correctes et bonnes, comme la réponse acceptée par bpgergo. Mais aucun des exemples ne montre l'utilisation de DataSource, généralement recommandée sur l'utilisation de DriverManagerJava moderne.

Par souci d'exhaustivité, voici un exemple complet qui récupère la date actuelle sur le serveur de base de données. La base de données utilisée ici est Postgres . Toute autre base de données fonctionnerait de la même manière. Vous remplaceriez l'utilisation de org.postgresql.ds.PGSimpleDataSourcepar une implémentation DataSourceappropriée à votre base de données. Une implémentation est probablement fournie par votre pilote ou pool de connexions si vous suivez cette voie.

Une DataSourceimplémentation n'a pas besoin d' être fermée, car elle n'est jamais «ouverte». A DataSourcen'est pas une ressource, n'est pas connecté à la base de données, il ne contient donc pas de connexions réseau ni de ressources sur le serveur de base de données. A DataSourceest simplement des informations nécessaires lors de la connexion à la base de données, avec le nom ou l'adresse réseau du serveur de base de données, le nom d'utilisateur, le mot de passe de l'utilisateur et diverses options que vous souhaitez spécifier lorsqu'une connexion est finalement établie. Ainsi, votre DataSourceobjet d'implémentation ne rentre pas dans vos parenthèses try-with-resources.

Try-with-resources imbriqué

Votre code utilise correctement les instructions try-with-resources imbriquées.

Notez dans l'exemple de code ci-dessous que nous utilisons également la syntaxe try-with-resources deux fois , l'une imbriquée dans l'autre. L'extérieur trydéfinit deux ressources: Connectionet PreparedStatement. L'intérieur trydéfinit la ResultSetressource. Il s'agit d'une structure de code commune.

Si une exception est levée depuis l'intérieur, et n'y est pas interceptée, la ResultSetressource sera automatiquement fermée (si elle existe, elle n'est pas nulle). Ensuite, le PreparedStatementsera fermé, et enfin le Connectionsera fermé. Les ressources sont automatiquement fermées dans l'ordre inverse dans lequel elles ont été déclarées dans les instructions try-with-resource.

L'exemple de code ici est trop simpliste. Tel qu'il est écrit, il peut être exécuté avec une seule instruction try-with-resources. Mais dans un vrai travail, vous ferez probablement plus de travail entre la paire d' tryappels imbriqués . Par exemple, vous pouvez extraire des valeurs de votre interface utilisateur ou d'un POJO, puis les transmettre pour remplir les ?espaces réservés dans votre SQL via des appels à des PreparedStatement::set…méthodes.

Notes de syntaxe

Point-virgule de fin

Notez que le point-virgule à la fin de la dernière instruction de ressource entre les parenthèses du try-with-resources est facultatif. Je l'inclus dans mon propre travail pour deux raisons: La cohérence et il semble complet, et cela facilite le copier-coller d'un mélange de lignes sans avoir à se soucier des points-virgules de fin de ligne. Votre IDE peut marquer le dernier point-virgule comme superflu, mais il n'y a aucun mal à le laisser.

Java 9 - Utiliser les variables existantes dans try-with-resources

La nouveauté de Java 9 est une amélioration de la syntaxe try-with-resources. Nous pouvons maintenant déclarer et remplir les ressources en dehors des parenthèses de l' tryinstruction. Je n'ai pas encore trouvé cela utile pour les ressources JDBC, mais gardez cela à l'esprit dans votre propre travail.

ResultSet devrait se fermer, mais ne peut pas

Dans un monde idéal, le ResultSetse fermerait comme le promet la documentation:

Un objet ResultSet est automatiquement fermé lorsque l'objet Statement qui l'a généré est fermé, réexécuté ou utilisé pour récupérer le résultat suivant d'une séquence de résultats multiples.

Malheureusement, dans le passé, certains pilotes JDBC n'ont pas tenu cette promesse. En conséquence, de nombreux programmeurs JDBC ont appris à fermer explicitement toutes leurs ressources JDBC , y compris Connection, PreparedStatementet ResultSetaussi. La syntaxe moderne try-with-resources a rendu cela plus facile et avec un code plus compact. Notez que l'équipe Java s'est donnée la peine de marquer ResultSetcomme AutoCloseable, et je suggère que nous nous en servions. L'utilisation d'un essai avec des ressources autour de toutes vos ressources JDBC rend votre code plus auto-documenté quant à vos intentions.

Exemple de code

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." );
    }
}
Basil Bourque
la source