Como devo usar a tentativa com recursos com o JDBC?

148

Eu tenho um método para obter usuários de um banco de dados com 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;
}

Como devo usar o Java 7 try-with-resources para melhorar esse código?

Eu tentei com o código abaixo, mas ele usa muitos tryblocos e não melhora muito a legibilidade . Devo usar try-with-resourcesde outra maneira?

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
fonte
5
Em seu segundo exemplo, você não precisa do interior try (ResultSet rs = ps.executeQuery()) {, porque Um ResultSet objeto é automaticamente fechada pelo objeto Statement que o gerou
Alexander Farber
2
@AlexanderFarber Infelizmente, houve problemas notórios com drivers que não conseguiram fechar recursos por conta própria. A School of Hard Knocks nos ensina a sempre perto todos os recursos JDBC explicitamente, facilitada usando try-com-recursos redor Connection, PreparedStatemente ResultSettambém. Não há razão para não fazer isso, pois a tentativa com recursos facilita e torna nosso código mais auto-documentado quanto às nossas intenções.
Basil Bourque

Respostas:

85

Não há necessidade de uma tentativa externa no seu exemplo; portanto, você pode pelo menos descer de 3 para 2 e também não precisa fechar ;no final da lista de recursos. A vantagem de usar dois blocos try é que todo o seu código está presente antecipadamente, assim você não precisa se referir a um 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
fonte
5
Como você liga Connection::setAutoCommit? Essa chamada não é permitida entre o tryentre con = e ps =. Ao obter uma conexão de um DataSource que pode ser apoiada por um conjunto de conexões, não podemos assumir como o autoCommit está definido.
Basil Bourque
1
normalmente você injeta a conexão no método (diferente da abordagem ad-hoc mostrada na pergunta do OP), você pode usar uma classe de gerenciamento de conexão que será chamada para fornecer ou fechar uma conexão (seja em pool ou não). em que o gerente você pode especificar seu comportamento de conexão
svarog
@BasilBourque você pode mover DriverManager.getConnection(myConnectionURL)para um método que também define o sinalizador autoCommit e retorna a conexão (ou colocá-lo no equivalente do createPreparedStatementmétodo no exemplo anterior ...)
rogerdpack
@rogerdpack Sim, isso faz sentido. Tenha sua própria implementação de DataSourceonde o getConnectionmétodo faz o que você diz, obtenha a conexão e configure-a conforme necessário, passando a conexão.
Basil Bourque
1
@rogerdpack obrigado pelo esclarecimento na resposta. Eu atualizei isso para a resposta selecionada.
Jonas
187

Sei que isso foi respondido há muito tempo, mas quero sugerir uma abordagem adicional que evite o bloco duplo aninhado try-with-resources.

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
fonte
24
Não, está coberto, o problema é que o código acima está chamando prepareStatement de dentro de um método que não declara lançar SQLException. Além disso, o código acima tem pelo menos um caminho de onde ele pode falhar sem fechar a declaração preparada (se uma SQLException ocorre ao chamar setInt.)
Trejkaz
1
@Trejkaz bom ponto sobre a possibilidade de não fechar o PreparedStatement. Eu não pensei nisso, mas você está certo!
Jeanne Boyarsky
2
@ArturoTena sim - a ordem é garantida
Jeanne Boyarsky
2
@JeanneBoyarsky, existe outra maneira de fazer isso? Se não eu precisaria para criar um método específico createPreparedStatement para cada sentença SQL
John Alexander Betts
1
Em relação ao comentário de Trejkaz, createPreparedStatementnão é seguro, independentemente de como você o use. Para corrigi-lo, você teria que adicionar um try-catch ao redor do setInt (...), pegar qualquer um SQLExceptione, quando isso acontecer, chamar ps.close () e repetir a exceção. Mas isso resultaria em um código quase tão longo e deselegante quanto o código que o OP queria melhorar.
Florian F
4

Aqui está uma maneira concisa de usar lambdas e JDK 8 Supplier para ajustar tudo na tentativa externa:

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
fonte
5
Isso é mais conciso do que a "abordagem clássica" descrita por @bpgergo? Acho que não, e o código é mais difícil de entender. Então, por favor, explique a vantagem dessa abordagem.
Rmuller 08/10/19
Eu não acho, nesse caso, que você precise capturar o SQLException explicitamente. Na verdade, é "opcional" em uma tentativa com recursos. Nenhuma outra resposta menciona isso. Portanto, você provavelmente pode simplificar isso ainda mais.
djangofan 10/09
e se DriverManager.getConnection (JDBC_URL, prop); retorna nulo?
21818
2

Que tal criar uma classe adicional de wrapper?

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


Em seguida, na classe de chamada, você pode implementar o 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
fonte
2
Nada no comentário acima diz que não.
8303 Trejkaz
2

Como outros já declararam, seu código está basicamente correto, embora o externo tryseja desnecessário. Aqui estão mais alguns pensamentos.

DataSource

Outras respostas aqui são corretas e boas, como a resposta aceita por bpgergo. Mas nenhum dos programas mostra o uso de DataSource, comumente recomendado sobre o uso DriverManagerno Java moderno.

Portanto, por uma questão de integridade, aqui está um exemplo completo que busca a data atual do servidor de banco de dados. O banco de dados usado aqui é o Postgres . Qualquer outro banco de dados funcionaria da mesma forma. Você substituiria o uso de org.postgresql.ds.PGSimpleDataSourcepor uma implementação deDataSource apropriada ao seu banco de dados. Provavelmente, uma implementação é fornecida pelo seu driver ou pool de conexão específico, se você seguir esse caminho.

Uma DataSourceimplementação não precisa ser fechada, porque nunca é "aberta". A DataSourcenão é um recurso, não está conectado ao banco de dados e, portanto, não mantém conexões de rede nem recursos no servidor de banco de dados. A DataSourcesão simplesmente as informações necessárias ao fazer uma conexão com o banco de dados, com o nome ou endereço da rede do servidor de banco de dados, o nome do usuário, a senha do usuário e as várias opções que você deseja especificar quando uma conexão for finalmente estabelecida. Portanto, seu DataSourceobjeto de implementação não entra nos parênteses da tentativa com recursos.

Tentativa aninhada com recursos

Seu código utiliza corretamente as instruções try-with-resources aninhadas.

Observe no código de exemplo abaixo que também usamos a sintaxe try-with-resources duas vezes , uma aninhada dentro da outra. O externo trydefine dois recursos: Connectione PreparedStatement. O interno trydefine o ResultSetrecurso. Essa é uma estrutura de código comum.

Se uma exceção for lançada a partir da interna e não for capturada lá, o ResultSetrecurso será fechado automaticamente (se existir, não será nulo). Depois disso, o PreparedStatementserá fechado, e por último o Connectioné fechado. Os recursos são fechados automaticamente na ordem inversa na qual foram declarados nas instruções try-with-resource.

O código de exemplo aqui é excessivamente simplista. Conforme escrito, ele pode ser executado com uma única instrução try-with-resources. Mas, em um trabalho real, você provavelmente fará mais trabalho entre o par de trychamadas aninhadas . Por exemplo, você pode extrair valores de sua interface com o usuário ou de um POJO e passá-los para preencher ?espaços reservados em seu SQL por meio de chamadas para PreparedStatement::set…métodos.

Notas de sintaxe

Ponto e vírgula à direita

Observe que o ponto-e-vírgula após a última instrução de recurso entre parênteses da tentativa com recursos é opcional. Eu o incluo no meu próprio trabalho por duas razões: Consistência e aparência completa, e facilita a cópia e a colagem de uma mistura de linhas sem ter que se preocupar com ponto e vírgula no final da linha. Seu IDE pode sinalizar o último ponto-e-vírgula como supérfluo, mas não há mal em deixá-lo.

Java 9 - Use vars existentes em try-with-resources

O que há de novo no Java 9 é um aprimoramento da sintaxe de tentativa com recursos. Agora podemos declarar e preencher os recursos fora dos parênteses dotry declaração. Ainda não achei isso útil para os recursos JDBC, mas lembre-se de seu próprio trabalho.

ResultSet deve fechar-se, mas pode não

Em um mundo ideal, ResultSetele se fecha como a documentação promete:

Um objeto ResultSet é fechado automaticamente quando o objeto Statement que o gerou é fechado, reexecutado ou usado para recuperar o próximo resultado de uma sequência de vários resultados.

Infelizmente, no passado, alguns drivers JDBC falharam em cumprir essa promessa. Como resultado, muitos programadores JDBC aprendeu a fechar explicitamente todos os seus recursos JDBC incluindo Connection, PreparedStatemente ResultSettambém. A sintaxe moderna da tentativa com recursos tornou isso mais fácil e com código mais compacto. Observe que a equipe Java se deu ao trabalho de marcar ResultSetcomo AutoCloseablee sugiro que façamos uso disso. Usar uma tentativa com recursos em torno de todos os seus recursos JDBC torna seu código mais auto-documentado quanto às suas intenções.

Exemplo 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." );
    }
}
Basil Bourque
fonte