Как мне использовать try-with-resources с JDBC?

148

У меня есть метод для получения пользователей из базы данных с 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;
}

Как мне использовать Java 7 try-with-resources для улучшения этого кода?

Я пробовал с приведенным ниже кодом, но он использует много tryблоков и не сильно улучшает читаемость . Должен ли я использовать try-with-resourcesпо-другому?

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
источник
5
Во втором примере вам не нужно внутреннее, try (ResultSet rs = ps.executeQuery()) {потому что объект ResultSet автоматически закрывается объектом Statement, который его сгенерировал
Alexander Farber
2
@AlexanderFarber К сожалению, были пресловутые проблемы с драйверами, которые не смогли самостоятельно закрыть ресурсы. Школа жестких ударов учит нас всегда близко все ресурсы JDBC явно упрощает использование примерочных с-ресурсов вокруг Connection, PreparedStatementи ResultSetтоже. На самом деле нет причин не делать этого, поскольку попытка использования ресурсов делает это настолько простым и делает наш код более самодокументируемым в соответствии с нашими намерениями.
Василий Бурк

Ответы:

85

В вашем примере нет необходимости во внешней попытке, так что вы можете, по крайней мере, опуститься с 3 до 2, а также вам не нужно закрывать ;в конце списка ресурсов. Преимущество использования двух блоков try состоит в том, что весь ваш код представлен заранее, поэтому вам не нужно ссылаться на отдельный метод:

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
источник
5
Как ты звонишь Connection::setAutoCommit? Такой вызов не допускается в пределах tryмежду con = и ps =. При получении соединения из источника данных, который может быть поддержан пулом соединений, мы не можем предположить, как установлен autoCommit.
Василий Бурк
1
вы обычно вводите соединение в метод (в отличие от специального подхода, показанного в вопросе OP), вы можете использовать класс управления соединением, который будет вызываться для обеспечения или закрытия соединения (независимо от того, будет оно объединено или нет). в этом менеджере вы можете указать поведение вашего соединения
svarog
@BasilBourque, вы можете перейти DriverManager.getConnection(myConnectionURL)в метод, который также устанавливает флаг autoCommit и возвращает соединение (или устанавливает его в эквиваленте createPreparedStatementметода в предыдущем примере ...)
rogerdpack
@rogerdpack Да, это имеет смысл. Имейте свою собственную реализацию того, DataSourceгде getConnectionметод делает, как вы говорите, получите соединение и настройте его по мере необходимости, затем передавая соединение.
Василий Бурк
1
@rogerdpack спасибо за разъяснение в ответе. Я обновил это до выбранного ответа.
Джонас
187

Я понимаю, что на это уже давно дан ответ, но хочу предложить дополнительный подход, позволяющий избежать вложенного двойного блока 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;
}
Жанна Боярская
источник
24
Нет, проблема в том, что код выше вызывает метод prepareStatement из метода, который не объявляет выброс SQLException. Кроме того, приведенный выше код имеет, по крайней мере, один путь, по которому он может завершиться неудачей, не закрывая подготовленный оператор (если при вызове setInt возникает исключение SQLException)
Trejkaz
1
@Trejkaz хороший момент о возможности не закрывать PreparedStatement. Я не думал об этом, но ты прав!
Жанна Боярская
2
@ArturoTena да - заказ гарантирован
Жанна Боярская
2
@JeanneBoyarsky есть еще один способ сделать это? Если нет, то мне нужно было бы создать определенный метод createPreparedStatement для каждого предложения sql
Джон Александр Беттс
1
Что касается комментария Трейказа, createPreparedStatementэто небезопасно, независимо от того, как вы его используете. Чтобы исправить это, вам нужно будет добавить try-catch вокруг setInt (...), перехватить любой SQLException, и когда это произойдет, вызовите ps.close () и повторно сгенерируйте исключение. Но это привело бы к тому, что код стал бы почти таким же длинным и нелегким, как код, который ОП хотел улучшить.
Florian F
4

Вот краткий способ использования lambdas и JDK 8 Supplier, чтобы все соответствовало внешним требованиям:

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
Это более лаконично, чем «классический подход», описанный @bpgergo? Я так не думаю, и код сложнее для понимания. Поэтому, пожалуйста, объясните преимущество этого подхода.
rmuller
Я не думаю, что в этом случае вам необходимо явно перехватывать SQLException. Это на самом деле "необязательно" при попытке с ресурсами. Другие ответы не упоминают об этом. Таким образом, вы, вероятно, можете упростить это дальше.
Джангофан
что если DriverManager.getConnection (JDBC_URL, prop); возвращает ноль?
Гаурав
2

Как насчет создания дополнительного класса оболочки?

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


Затем в вызывающем классе вы можете реализовать метод prepareStatement как:

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
Ничто в комментарии выше никогда не говорит, что это не так.
Трейказ
2

Как уже говорили другие, ваш код в основном правильный, хотя внешний tryне нужен. Вот еще несколько мыслей.

DataSource

Другие ответы здесь правильные и хорошие, такие как принятый Ответ от bpgergo. Но ни один из них не демонстрирует использование DataSource, обычно рекомендуемое по сравнению с использованием DriverManagerв современной Java.

Итак, ради полноты, вот полный пример, который выбирает текущую дату с сервера базы данных. Используемая здесь база данных - Postgres . Любая другая база данных будет работать аналогично. Вы бы заменили использование org.postgresql.ds.PGSimpleDataSourceс реализациейDataSource соответствующую вашей базе данных. Реализация, вероятно, обеспечивается вашим конкретным драйвером или пулом соединений, если вы идете по этому пути.

DataSourceРеализации нужны не быть закрыты, потому что она никогда не «открыто». A DataSourceне является ресурсом, не подключен к базе данных, поэтому он не содержит сетевых подключений и ресурсов на сервере базы данных. A DataSource- это просто информация, необходимая при установлении соединения с базой данных, с сетевым именем или адресом сервера базы данных, именем пользователя, паролем пользователя и различными параметрами, которые вы хотите указать, когда соединение в конце концов устанавливается. Таким образом, ваш DataSourceобъект реализации не входит в скобки try-with-resources.

Вложенная попытка с ресурсами

Ваш код правильно использует вложенные операторы try-with-resources.

Обратите внимание, что в приведенном ниже примере кода мы также используем синтаксис try-with-resources дважды , один вложен в другой. Внешний tryопределяет два ресурса: Connectionи PreparedStatement. Внутреннее tryопределяет ResultSetресурс. Это общая структура кода.

Если исключение выдается из внутреннего и не перехватывается там, ResultSetресурс автоматически закрывается (если он существует, не ноль). После этого PreparedStatementбудет закрыто, и, наконец, Connectionзакрыто. Ресурсы автоматически закрываются в обратном порядке, в котором они были объявлены в инструкциях try-with-resource.

Пример кода здесь слишком упрощен. Как написано, это может быть выполнено с одним оператором try-with-resources. Но в реальной работе вы, вероятно, будете выполнять больше работы между вложенными парами tryвызовов. Например, вы можете извлекать значения из вашего пользовательского интерфейса или POJO, а затем передавать их для выполнения ?заполнителей в вашем SQL через вызовыPreparedStatement::set… методов.

Синтаксические заметки

Точка с запятой

Обратите внимание, что точка с запятой, завершающая последний оператор ресурса в скобках try-with-resources, является необязательной. Я включил его в свою собственную работу по двум причинам: согласованность, и она выглядит завершенной, и это облегчает вставку копий в сочетание строк, не беспокоясь о точках с запятой в конце строки. Ваша IDE может пометить последнюю точку с запятой как лишнюю, но не оставляя вреда.

Java 9 - использовать существующие переменные в try-with-resources

Новое в Java 9 - это улучшение синтаксиса проб с ресурсами. Теперь мы можем объявить и заполнить ресурсы за пределами скобок tryоператора. Я еще не нашел это полезным для ресурсов JDBC, но помните об этом в своей собственной работе.

ResultSet должен закрыть себя, но не может

В идеальном мире ResultSetон закрывается, как обещает документация:

Объект ResultSet автоматически закрывается, когда объект Statement, который его сгенерировал, закрывается, выполняется повторно или используется для получения следующего результата из последовательности нескольких результатов.

К сожалению, в прошлом некоторые драйверы JDBC печально не выполняли это обещание. В результате, многие программисты JDBC научились явно закрыть все свои ресурсы , включая JDBC Connection, PreparedStatementи ResultSetтоже. Современный синтаксис try-with-resources сделал это проще и с более компактным кодом. Обратите внимание, что команда Java пошла на то, чтобы пометить ResultSetкакAutoCloseable , и я предлагаю использовать это. Использование try-with-resources во всех ваших ресурсах JDBC делает ваш код более самодокументируемым в соответствии с вашими намерениями.

Пример кода

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." );
    }
}
Базилик Бурк
источник