Come dovrei usare try-with-resources con JDBC?


148

Ho un metodo per ottenere utenti da un database con 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;
}

Come dovrei usare Java 7 try-with-resources per migliorare questo codice?

Ho provato con il codice seguente, ma utilizza molti tryblocchi e non migliora molto la leggibilità . Dovrei usare try-with-resourcesin un altro modo?

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

5
Nel tuo secondo esempio, non hai bisogno di inner try (ResultSet rs = ps.executeQuery()) {perché un oggetto ResultSet viene automaticamente chiuso dall'oggetto Statement che lo ha generato
Alexander Farber,

2
@AlexanderFarber Sfortunatamente, ci sono stati problemi noti con i driver che non sono riusciti a chiudere le risorse da soli. La School of Hard Knocks ci insegna a sempre vicino tutte le risorse JDBC in modo esplicito, reso più facile utilizzando try-con-le risorse intorno Connection, PreparedStatemente ResultSetanche. Nessun motivo per non farlo davvero, poiché il tentativo con le risorse lo rende così semplice e rende il nostro codice più autocompattante quanto alle nostre intenzioni.
Basil Bourque,

Risposte:


85

Non è necessario il tentativo esterno nel tuo esempio, quindi puoi almeno scendere da 3 a 2 e inoltre non è necessario chiudere ;alla fine dell'elenco delle risorse. Il vantaggio di utilizzare due blocchi di prova è che tutto il codice è presente in anticipo, quindi non è necessario fare riferimento a un metodo separato:

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

5
Come si chiama Connection::setAutoCommit? Tale chiamata non è consentita trytra il con = e ps =. Quando si ottiene una connessione da un'origine dati che può essere supportata da un pool di connessioni, non è possibile ipotizzare l'impostazione di autoCommit.
Basil Bourque,

1
di solito inietti la connessione nel metodo (a differenza dell'approccio ad hoc mostrato nella domanda dell'OP), potresti utilizzare una classe di gestione della connessione che verrà chiamata per fornire o chiudere una connessione (sia essa aggregata o meno). in quel gestore puoi specificare il tuo comportamento di connessione
svarog

@BasilBourque potresti passare DriverManager.getConnection(myConnectionURL)a un metodo che imposta anche il flag autoCommit e restituisce la connessione (o impostalo nell'equivalente del createPreparedStatementmetodo nell'esempio precedente ...)
rogerdpack

@rogerdpack Sì, ha senso. Avere la propria implementazione di DataSourcedove fa il getConnectionmetodo come si dice, ottenere la connessione e configurarlo come necessario, quindi passare la connessione.
Basil Bourque,

1
@rogerdpack grazie per il chiarimento nella risposta. Ho aggiornato questo alla risposta selezionata.
Jonas,

187

Mi rendo conto che è stata data risposta molto tempo fa, ma voglio suggerire un approccio aggiuntivo che evita il doppio blocco di prova con risorse nidificato.

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

24
No, è coperto, il problema è che il codice sopra sta chiamando PrepStatement dall'interno di un metodo che non dichiara di lanciare SQLException. Inoltre, il codice sopra ha almeno un percorso in cui può fallire senza chiudere l'istruzione preparata (se si verifica una SQLException mentre chiama setInt.)
Trejkaz

1
@Trejkaz buon punto sulla possibilità di non chiudere il PreparedStatement. Non ci avevo pensato, ma hai ragione!
Jeanne Boyarsky,

2
@ArturoTena sì - l'ordine è garantito
Jeanne Boyarsky il

2
@JeanneBoyarsky c'è un altro modo per farlo? Altrimenti avrei bisogno di creare un metodo createPreparedStatement specifico per ogni frase sql
John Alexander Betts,

1
Per quanto riguarda il commento di Trejkaz, createPreparedStatementnon è sicuro a prescindere da come lo si utilizza. Per risolverlo dovresti aggiungere un try-catch attorno a setInt (...), catturare qualsiasi SQLException, e quando succede chiama ps.close () e ripeti l'eccezione. Ma ciò comporterebbe un codice quasi lungo e non elegante come il codice che l'OP voleva migliorare.
Florian F,

4

Ecco un modo conciso che utilizza lambda e JDK 8 Supplier per adattarsi a tutto nel tentativo esterno:

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

5
Questo è più conciso dell '"approccio classico" come descritto da @bpgergo? Non la penso così e il codice è più difficile da capire. Quindi, per favore, spiega il vantaggio di questo approccio.
rmuller,

Non penso, in questo caso, che ti venga richiesto di catturare esplicitamente SQLException. In realtà è "facoltativo" in una prova con risorse. Nessuna altra risposta menziona questo. Quindi, probabilmente puoi semplificarlo ulteriormente.
Djangofan,

cosa succede se DriverManager.getConnection (JDBC_URL, prop); restituisce null?
gaurav,

2

Che ne dici di creare una classe wrapper aggiuntiva?

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


Quindi nella classe chiamante è possibile implementare il metodo PrepStatement come:

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


2
Nulla nel commento sopra dice mai di no.
Trejkaz,

2

Come altri hanno affermato, il tuo codice è sostanzialmente corretto anche se l'esterno trynon è necessario. Ecco qualche altro pensiero.

DataSource

Altre risposte qui sono corrette e buone, come la risposta accettata da bpgergo. Ma nessuno dei programmi mostra l'uso DataSource, comunemente raccomandato rispetto all'uso DriverManagernella moderna Java.

Quindi, per completezza, ecco un esempio completo che recupera la data corrente dal server di database. Il database utilizzato qui è Postgres . Qualsiasi altro database funzionerebbe in modo simile. Sostituiresti l'uso di org.postgresql.ds.PGSimpleDataSourcecon un'implementazione di DataSourceappropriate al tuo database. Un'implementazione è probabilmente fornita dal tuo driver specifico o dal pool di connessioni se segui quel percorso.

DataSourceUn'implementazione deve non essere chiuso, perché non è mai “aperto”. A DataSourcenon è una risorsa, non è connessa al database, quindi non contiene connessioni di rete né risorse sul server di database. A DataSourcesono semplicemente le informazioni necessarie quando si effettua una connessione al database, con il nome o l'indirizzo di rete del server di database, il nome utente, la password utente e le varie opzioni che si desidera specificare quando viene stabilita una connessione. Quindi l' DataSourceoggetto di implementazione non rientra nelle parentesi di prova con risorse.

Prova con risorse nidificata

Il codice utilizza correttamente le istruzioni nidificate di prova con risorse.

Si noti nel seguente codice di esempio che utilizziamo anche la sintassi try-with-resources due volte , una nidificata all'interno dell'altra. L'esterno trydefinisce due risorse: Connectione PreparedStatement. L'interno trydefinisce la ResultSetrisorsa. Questa è una struttura di codice comune.

Se viene generata un'eccezione da quella interna e non rilevata lì, la ResultSetrisorsa verrà automaticamente chiusa (se esiste, non è nulla). Successivamente, PreparedStatementverrà chiuso e infine Connectionverrà chiuso. Le risorse vengono automaticamente chiuse in ordine inverso rispetto a quanto dichiarato nelle istruzioni di prova con risorsa.

Il codice di esempio qui è eccessivamente semplicistico. Come scritto, potrebbe essere eseguito con una singola istruzione try-with-resources. Ma in un vero lavoro probabilmente farai più lavoro tra la coppia nidificata di trychiamate. Ad esempio, potresti estrarre valori dall'interfaccia utente o da un POJO e quindi passarli per soddisfare i ?segnaposto all'interno di SQL tramite chiamate ai PreparedStatement::set…metodi.

Note di sintassi

Punto e virgola finale

Si noti che il punto e virgola che segue l'ultima istruzione di risorsa tra parentesi delle risorse di prova è facoltativo. Lo includo nel mio lavoro per due motivi: coerenza e aspetto completo, e rende più semplice incollare un mix di linee senza doversi preoccupare dei punti e virgola di fine riga. Il tuo IDE può contrassegnare l'ultimo punto e virgola come superfluo, ma non c'è nulla di male nel lasciarlo.

Java 9 - Usa i var esistenti in try-with-resources

Novità di Java 9 è un miglioramento della sintassi di prova con risorse. Ora possiamo dichiarare e popolare le risorse al di fuori delle parentesi trydell'istruzione. Non l'ho ancora trovato utile per le risorse JDBC, ma tienilo a mente nel tuo lavoro.

ResultSet dovrebbe chiudersi, ma potrebbe non esserlo

In un mondo ideale ResultSetsi chiuderebbe come promette la documentazione:

Un oggetto ResultSet viene automaticamente chiuso quando l'oggetto Statement che lo ha generato viene chiuso, rieseguito o utilizzato per recuperare il risultato successivo da una sequenza di più risultati.

Sfortunatamente, in passato alcuni driver JDBC non sono riusciti a mantenere questa promessa. Come risultato, molti programmatori JDBC imparato a esplicitamente vicino tutte le loro risorse JDBC, tra cui Connection, PreparedStatemente ResultSettroppo. La moderna sintassi di prova con risorse ha reso tutto più semplice e con un codice più compatto. Si noti che il team Java si è preso la briga di contrassegnare ResultSetcome AutoCloseable, e suggerisco di usarlo. L'uso di una prova con le risorse intorno a tutte le risorse JDBC rende il codice più autocompattante delle tue intenzioni.

Esempio di codice

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." );
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.