¿Cómo debo usar try-with-resources con JDBC?

148

Tengo un método para obtener usuarios de una base de datos 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;
}

¿Cómo debo usar Java 7 try-with-resources para mejorar este código?

He intentado con el siguiente código, pero usa muchos trybloques y no mejora mucho la legibilidad . ¿Debo usar try-with-resourcesde otra manera?

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
fuente
55
En su segundo ejemplo, no necesita el interior try (ResultSet rs = ps.executeQuery()) {porque el objeto de declaración que lo generó cierra automáticamente un objeto ResultSet
Alexander Farber
2
@AlexanderFarber Desafortunadamente, ha habido problemas notorios con los controladores que no pudieron cerrar los recursos por su cuenta. La Escuela de golpes nos enseña a siempre cerca de todos los recursos JDBC de forma explícita, se facilita el uso de try-con-recursos alrededor Connection, PreparedStatementy ResultSettambién. No hay razón para no hacerlo realmente, ya que la prueba con recursos lo hace tan fácil y hace que nuestro código se documente más en cuanto a nuestras intenciones.
Basil Bourque

Respuestas:

85

No hay necesidad de realizar un intento externo en su ejemplo, por lo que al menos puede bajar de 3 a 2, y tampoco necesita cerrar ;al final de la lista de recursos. La ventaja de usar dos bloques de prueba es que todo su código está presente por adelantado, por lo que no tiene que referirse a un método separado:

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
fuente
55
¿Cómo se llama Connection::setAutoCommit? Tal llamada no está permitida tryentre el con = y ps =. Al obtener una conexión de un DataSource que puede respaldarse con un grupo de conexiones, no podemos suponer cómo se configura autoCommit.
Basil Bourque
1
generalmente inyectaría la conexión en el método (a diferencia del enfoque ad-hoc que se muestra en la pregunta de OP), podría usar una clase de administración de conexión que se llamará para proporcionar o cerrar una conexión (ya sea agrupada o no). en ese administrador puede especificar su comportamiento de conexión
svarog
@BasilBourque podría pasar DriverManager.getConnection(myConnectionURL)a un método que también establezca el indicador autoCommit y devuelva la conexión (o establecerlo en el equivalente del createPreparedStatementmétodo en el ejemplo anterior ...)
rogerdpack
@rogerdpack Sí, eso tiene sentido. Tenga su propia implementación de DataSourcedonde el getConnectionmétodo hace lo que usted dice, obtenga la conexión y configúrela según sea necesario, luego pase la conexión.
Basil Bourque
1
@rogerdpack gracias por la aclaración en la respuesta. He actualizado esto a la respuesta seleccionada.
Jonas
187

Me doy cuenta de que esto fue respondido hace mucho tiempo, pero quiero sugerir un enfoque adicional que evite el doble bloqueo anidado de prueba con recursos.

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
fuente
24
No, está cubierto, el problema es que el código anterior está llamando a prepareStatement desde un método que no declara lanzar SQLException. Además, el código anterior tiene al menos una ruta donde puede fallar sin cerrar la instrucción preparada (si se produce una excepción SQLEx mientras se llama a setInt.)
Trejkaz
1
@Trejkaz buen punto sobre la posibilidad de no cerrar la Declaración Preparada. No pensé en eso, ¡pero tienes razón!
Jeanne Boyarsky
2
@ArturoTena sí, el pedido está garantizado
Jeanne Boyarsky
2
@JeanneBoyarsky, ¿hay otra forma de hacer esto? Si no, necesitaría crear un método createPreparedStatement específico para cada oración sql
John Alexander Betts
1
Con respecto al comentario de Trejkaz, createPreparedStatementno es seguro independientemente de cómo lo use. Para solucionarlo, tendría que agregar un try-catch alrededor del setInt (...), capturar cualquiera SQLException, y cuando ocurra, llame a ps.close () y vuelva a lanzar la excepción. Pero eso daría como resultado un código casi tan largo y poco elegante como el código que el OP quería mejorar.
Florian F
4

Aquí hay una manera concisa usando lambdas y JDK 8 Supplier para adaptarse a todo en el intento externo:

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
fuente
55
¿Esto es más conciso que el "enfoque clásico" descrito por @bpgergo? No lo creo y el código es más difícil de entender. Entonces, explique la ventaja de este enfoque.
rmuller
No creo, en este caso, que se requiera capturar la excepción SQLException explícitamente. En realidad es "opcional" en una prueba con recursos. Ninguna otra respuesta menciona esto. Entonces, probablemente pueda simplificar esto aún más.
djangofan
¿Qué pasa si DriverManager.getConnection (JDBC_URL, prop); devuelve nulo?
gaurav
2

¿Qué pasa con la creación de una clase de contenedor adicional?

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


Luego, en la clase de llamada, puede implementar el método prepareStatement como:

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
fuente
2
Nada en el comentario anterior dice que no.
Trejkaz
2

Como han dicho otros, su código es básicamente correcto, aunque el exterior no tryes necesario. Aquí hay algunos pensamientos más.

DataSource

Otras respuestas aquí son correctas y buenas, como la respuesta aceptada por bpgergo. Pero ninguno de ellos muestra el uso DataSource, comúnmente recomendado sobre el uso de DriverManageren Java moderno.

Entonces, en aras de la integridad, aquí hay un ejemplo completo que obtiene la fecha actual del servidor de la base de datos. La base de datos utilizada aquí es Postgres . Cualquier otra base de datos funcionaría de manera similar. Reemplazaría el uso de org.postgresql.ds.PGSimpleDataSourcecon una implementación DataSourceapropiada para su base de datos. Es probable que su controlador particular o grupo de conexiones proporcione una implementación si sigue esa ruta.

Una DataSourceimplementación no necesita cerrarse, porque nunca se "abre". A DataSourceno es un recurso, no está conectado a la base de datos, por lo que no contiene conexiones de red ni recursos en el servidor de la base de datos. A DataSourcees simplemente información necesaria cuando se realiza una conexión a la base de datos, con el nombre o la dirección de red del servidor de la base de datos, el nombre de usuario, la contraseña de usuario y varias opciones que desea especificar cuando finalmente se realiza una conexión. Por lo tanto, su DataSourceobjeto de implementación no va dentro de sus paréntesis de prueba con recursos.

Prueba anidada con recursos

Su código hace un uso adecuado de las declaraciones anidadas de prueba con recursos.

Observe en el código de ejemplo a continuación que también usamos la sintaxis try-with-resources dos veces , una anidada dentro de la otra. El exterior trydefine dos recursos: Connectiony PreparedStatement. Lo interno trydefine el ResultSetrecurso. Esta es una estructura de código común.

Si se lanza una excepción desde el interior y no se captura allí, el ResultSetrecurso se cerrará automáticamente (si existe, no es nulo). A continuación, PreparedStatementse cerrará y, por último, Connectionse cerrará. Los recursos se cierran automáticamente en orden inverso en el que se declararon dentro de las declaraciones de prueba con recursos.

El código de ejemplo aquí es demasiado simplista. Tal como está escrito, podría ejecutarse con una única declaración de prueba con recursos. Pero en un trabajo real, es probable que esté haciendo más trabajo entre el par de tryllamadas anidadas . Por ejemplo, puede estar extrayendo valores de su interfaz de usuario o un POJO, y luego pasarlos para cumplir con los ?marcadores de posición dentro de su SQL a través de llamadas a PreparedStatement::set…métodos.

Notas de sintaxis

Punto y coma final

Observe que el punto y coma que sigue a la última declaración de recursos entre paréntesis de try-with-resources es opcional. Lo incluyo en mi propio trabajo por dos razones: consistencia y parece completo, y hace que copiar y pegar una mezcla de líneas sea más fácil sin tener que preocuparme por los puntos y comas al final de la línea. Su IDE puede marcar el último punto y coma como superfluo, pero no hay nada de malo en dejarlo.

Java 9: ​​utilice vars existentes en try-with-resources

Nuevo en Java 9 es una mejora de la sintaxis de prueba con recursos. Ahora podemos declarar y completar los recursos fuera de los paréntesis de la trydeclaración. Todavía no he encontrado esto útil para los recursos de JDBC, pero tenlo en cuenta en tu propio trabajo.

ResultSet debería cerrarse, pero puede que no

En un mundo ideal, ResultSetse cerraría como promete la documentación:

Un objeto ResultSet se cierra automáticamente cuando el objeto Sentencia que lo generó se cierra, se vuelve a ejecutar o se usa para recuperar el siguiente resultado de una secuencia de resultados múltiples.

Desafortunadamente, en el pasado, algunos controladores de JDBC no pudieron cumplir esta promesa. Como resultado, muchos programadores JDBC aprendieron a cierre de forma explícita todos sus recursos JDBC entre ellos Connection, PreparedStatementy ResultSettambién. La sintaxis moderna de prueba con recursos ha hecho que sea más fácil y con un código más compacto. Tenga en cuenta que el equipo de Java se tomó la molestia de marcar ResultSetcomo AutoCloseable, y sugiero que hagamos uso de eso. El uso de una prueba con recursos alrededor de todos sus recursos JDBC hace que su código se documente más en cuanto a sus intenciones.

Ejemplo de código

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." );
    }
}
Albahaca Bourque
fuente