Try/Try-with-resources и Connection, Statement и ResultSet

Недавно я поговорил с моим профессором о том, как обращаться с базовой схемой подключения jdbc. Предположим, мы хотим выполнить два запроса, это то, что он предлагает

public void doQueries() throws MyException{
    Connection con = null;
    try {
        con = DriverManager.getConnection(dataSource);
        PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
        PreparedStatement s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

        rs.close();
        s2.close();
        s1.close();
    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't really do anything
        }
    }
}

Мне не нравится этот подход, и у меня есть два вопроса:

1.A) Я думаю, что если какое-либо исключение выбрано, когда мы делаем "другие вещи" или в строке rs.close() или s2.close(), тогда s1 не будет закрыт, когда метод завершится. Я прав об этом?

1.B) Профессор просит меня явно закрыть ResultSet (даже если в документации Statement ясно указано, что он закроет ResultSet). Она говорит, что Sun рекомендует его. Есть ли основания для этого?

Теперь это то, что я считаю правильным кодом для одного и того же:

public void doQueries() throws MyException{
    Connection con = null;
    PreparedStatement s1 = null;
    PreparedStatement s2 = null;
    try {
        con = DriverManager.getConnection(dataSource);
        s1 = con.prepareStatement(updateSqlQuery);
        s2 = con.prepareStatement(selectSqlQuery);

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        try {
            if (s2 != null) {
                s2.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (s1 != null) {
                s1.close();
            }
        } catch (SQLException e3) {
            // Can't do nothing
        }
        try {
            if (con != null) {
                con.close();
            }
        } catch (SQLException e2) {
            // Can't do nothing
        }
    }
}

2.A) Правильно ли этот код? (Гарантировано ли, что все будет закрыто при завершении метода?)

2.B) Это очень большой и многословный (и становится хуже, если есть больше заявлений). Есть ли более короткий или более элегантный способ сделать это без использования try-in-resources?

Наконец, это код, который мне больше всего нравится

public void doQueries() throws MyException{
    try (Connection con = DriverManager.getConnection(dataSource);
         PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
         PreparedStatement s2 = con.prepareStatement(selectSqlQuery))
    {

        // Set the parameters of the PreparedStatements and maybe do other things

        s1.executeUpdate();
        ResultSet rs = s2.executeQuery();

    } catch (SQLException e) {
        throw new MyException(e);
    }
}

3) Правильно ли этот код? Я думаю, что моему профессору это не нравится, потому что нет явного закрытия ResultSet, но она сказала мне, что с ней все в порядке, пока в документации ясно, что все закрыто. Можете ли вы дать какую-либо ссылку на официальную документацию с аналогичным примером или на основе документации, показывающей, что с этим кодом нет проблем?

Ответ 1

ТЛ; др

  • В теории закрытие утверждения закрывает набор результатов.
  • На практике некоторые ошибочные реализации драйвера JDBC, как известно, не смогли этого сделать. Таким образом, совет от вашего инструктора, который она выучила в школе суровых ударов. Если вы не знакомы с каждой реализацией каждого драйвера JDBC, который может быть развернут для вашего приложения, используйте try-with-resources, чтобы автоматически закрывать каждый уровень вашего JDBC работа, такая как заявления и наборы результатов.

Используйте синтаксис try-with-resources

Ни один из ваших кодов не полностью использует try-with-resources. В синтаксисе try-with-resources вы объявляете и создаете экземпляры Connection, PreparedStatement и ResultSet в скобках перед скобками. Смотрите Учебник для Oracle.

Хотя ваш ResultSet явно не закрыт в вашем последнем примере кода, он должен быть закрыт косвенно, когда его оператор закрыт. Но, как описано ниже, он не может быть закрыт из-за неисправного драйвера JDBC.

AutoCloseable

Любые такие объекты, реализующие AutoCloseable, будут автоматически вызываться их методом close. Так что нет необходимости в этих пунктах finally.

Да, для гуманитарных специальностей, читающих это, да, команда Java написала с ошибкой "закрываемый".

Как вы узнаете, какие объекты автоматически закрываются, а какие нет? Посмотрите на документацию их класса, чтобы узнать, объявляет ли он AutoCloseable как суперинтерфейс. И наоборот, смотрите страницу JavaDoc для AutoCloseable, чтобы получить список всех связанных субинтерфейсов и реализующих классов (на самом деле десятки).

Например, для работы с SQL мы видим, что Connection, Statement, PreparedStatement, ResultSet, и RowSet все автоматически закрываются, но DataSource нет. Это имеет смысл, поскольку DataSource хранит данные о потенциальных ресурсах (соединениях с базой данных), но сам не является ресурсом. DataSource никогда не бывает "открытым", поэтому нет необходимости закрывать.

См. учебное пособие по Oracle, Заявление об использовании ресурсов.

Пример кода

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

Цитировать ResultSet JavaDoc:

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

Как подсказывал ваш учитель, в некоторых драйверах JDBC были серьезные недостатки, которые не соответствовали обещанию спецификации JDBC закрыть ResultSet, когда его Statement или PreparedStatement закрыты. Так много программистов имеют привычку закрывать каждый объект ResultSet явно.

Эта дополнительная обязанность стала проще благодаря синтаксису try-with-resources. В реальной работе вам, скорее всего, придется попробовать все ваши объекты AutoCloseable, например, ResultSet. Поэтому мое собственное мнение таково: почему бы не попробовать его с другими ресурсами? Это не повредит, делает ваш код более самодокументированным о ваших намерениях, и это может помочь, если ваш код когда-либо столкнется с одним из этих неисправных драйверов JDBC. Единственная цена - пара паренов, при условии, что в любом случае у вас есть возможность попробовать другие вещи.

Как указано в Учебнике Oracle, несколько объявленных вместе объектов AutoCloseable будут закрыты в обратном порядке, как мы и хотели бы.

Совет: Синтаксис try-with-resources допускает необязательную точку с запятой в последнем объявленном элементе ресурса. Я использую точку с запятой как привычку, потому что она хорошо читается на глаз, непротиворечива и облегчает редактирование. Я включил его в вашу строку PreparedStatement s2.

public void doQueries() throws MyException{
    // First try-with-resources.
    try ( Connection con = DriverManager.getConnection( dataSource ) ;
          PreparedStatement s1 = con.prepareStatement( updateSqlQuery ) ;
          PreparedStatement s2 = con.prepareStatement( selectSqlQuery ) ;
    ) {

        … Set parameters of PreparedStatements, etc.

        s1.executeUpdate() ;

        // Second try-with-resources, nested within first.
        try (
            ResultSet rs = s2.executeQuery() ;
        ) {
            … process ResultSet
        } catch ( SQLException e2 ) {  
            … handle exception related to ResultSet.
        }

    } catch ( SQLException e ) {  
        … handle exception related to Connection or PreparedStatements.
    }
}

Я предполагаю, что есть более элегантный синтаксис для такого рода работы, который может быть изобретен в будущем языке программирования. Но на данный момент у нас есть попытка с ресурсами, и я использую это счастливо. Хотя попытка использования ресурсов не совсем элегантна, она значительно лучше старого синтаксиса.

Кстати, Oracle рекомендует использовать реализацию DataSource для получения соединений, а не подход DriverManager, который можно увидеть в вашем коде. Использование DataSource во всем коде облегчает переключение драйверов или переключение в пул соединений. Посмотрите, обеспечивает ли ваш драйвер JDBC реализацию DataSource.

Обновление: Java 9

Теперь в Java 9 вы можете инициализировать ресурсы перед попыткой с ресурсами. Смотрите эту статью. Такая гибкость может быть полезна в некоторых сценариях.

Ответ 2

Самое интересное в коде JDBC заключается в том, что вы кодируете спецификацию, где не всегда понятно, насколько совместима ваша реализация. Существует множество различных баз данных и драйверов, а некоторые драйверы лучше, чем другие. Это, как правило, заставляет людей ошибаться на стороне осторожности, рекомендуя такие вещи, как закрытие всего явно. Вы можете быть в порядке с закрытием только соединения здесь. Закрытие resultSet только для того, чтобы быть в безопасности, с чем сложно спорить. Вы не указываете, какую базу данных или драйвер вы используете здесь, я бы не хотел жестко кодировать в предположениях о драйвере, который может быть недействительным для некоторой реализации.

Замыкание в последовательности оставляет вас открытым для проблем, когда исключение может быть брошено и заставить некоторые из закрытий быть пропущены. Вы правы, чтобы беспокоиться об этом.

Помните, что это игрушечный пример. В самом реальном коде используется пул соединений, где вызов метода close фактически не закрывает соединение, вместо этого он возвращает соединение с пулом. Таким образом, ресурсы могут не закрываться после использования пула. Если вы хотите изменить этот код, чтобы использовать пул соединений, вам придется вернуться и закрыть инструкции как минимум.

Также, если вы возражаете против многословия этого вопроса, ответ на это заключается в том, чтобы скрыть код в утилитах многократного использования, в которых используются стратегии, преобразователи resultSet, подготовленные средства настройки отчетов и т.д. Это все было сделано до, конечно же; вы будете в пути к переосмыслению Spring JDBC.

Говоря о том, что: Spring JDBC закрывает все явно (возможно, потому, что ему нужно работать с максимально возможным числом драйверов и не хочет вызывать проблемы из-за того, что какой-то драйвер не хорошо себя ведет).

Ответ 3

Это действительно основная мотивация для try-in-resources. См. Java учебники в качестве справочника. Ваш профессор устарел. Если вы хотите иметь дело с проблемой набора результатов, вы всегда можете заключить его в другой оператор try-with-resources.

Ответ 4

Вы можете сделать класс util для обработки закрытия этих ресурсов. т.е. FYI я просто проигнорировал SQLExceptions от попытки закрыть ресурсы в классе util, но вы могли бы как очень хорошо регистрировать или собирать и бросать их, как только вы закончите закрывать ресурсы в коллекции в зависимости от ваших потребностей.

public class DBUtil {
public static void closeConnections(Connection ...connections){
    if(connections != null ){
        for(Connection conn : connections){
            if(conn != null){
                try {
                    conn.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

public static void closeResultSets(ResultSet ...resultSets){
    if(resultSets != null ){
        for(ResultSet rs: resultSets){
            if(rs != null){
                try {
                    rs.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

public static void closeStatements(Statement ...statements){
    if(statements != null){
        for(Statement statement : statements){
            if(statement != null){
                try {
                    statement.close();
                } catch (SQLException ignored) {
                    //ignored
                }
            }
        }
    }
}

}

а затем просто вызовите его из вашего метода:

    public void doQueries() throws MyException {
    Connection con = null;
    try {
        con = DriverManager.getConnection(dataSource);
        PreparedStatement s1 = null;
        PreparedStatement s2 = null;
        try {
            s1 = con.prepareStatement(updateSqlQuery);
            s2 = con.prepareStatement(selectSqlQuery);

            // Set the parameters of the PreparedStatements and maybe do other things
            s1.executeUpdate();
            ResultSet rs = null;
            try {
                rs = s2.executeQuery();
            } finally {
                DBUtil.closeResultSets(rs);
            }
        } finally {
            DBUtil.closeStatements(s2, s1);
        }

    } catch (SQLException e) {
        throw new MyException(e);
    } finally {
        DBUtil.closeConnections(con);
    }
}

Ответ 5

Это то, что я считаю лучшим решением для обработки таких ресурсов, как JDBC. Этот метод обеспечивает непреложную функцию, используя конечные переменные и только объявляя и присваивая эти переменные, если они необходимы, он очень эффективен для ЦП и гарантирует во всех случаях, что все ресурсы, которые назначены и открыты, закрыты независимо от состояния исключений. Техника, которую вы используете, оставляет пробелы, которые могут привести к утечкам ресурсов, если они не будут тщательно реализованы для решения всех сценариев. Этот метод не позволяет утечки ресурсов при условии, что шаблон всегда соблюдается:
1) назначить ресурс
2) попробуйте 3) использовать ресурс
4) наконец закрыть ресурс

public void doQueries() throws MyException {
   try {
      final Connection con = DriverManager.getConnection(dataSource);
      try {
         final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
         try {

            // Set the parameters of the PreparedStatements and maybe do other things

            s1.executeUpdate();

         } finally {
            try { s1.close(); } catch (SQLException e) {}
         }

         final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
         try {

            // Set the parameters of the PreparedStatements and maybe do other things

            final ResultSet rs = s2.executeQuery();
            try {

               // Do something with rs

            } finally {
               try { rs.close(); } catch (SQLException e) {}
            }
         } finally {
            try { s2.close(); } catch (SQLException e) {}
         }
      } finally {
         try { con.close(); } catch (SQLException e) {}
      }
   } catch (SQLException e) {
      throw new MyException(e);
   }
}

С помощью Java 7 вы можете использовать новые возможности try -with-resources, чтобы упростить это: новый try -with-resources следует за вышеуказанным логическим потоком, поскольку он гарантирует, что все ресурсы будут включены в блок с ресурсами, который назначаются get closed. любое исключение, заброшенное в блоке ресурсов, будет сброшено, но назначенные ресурсы все равно будут закрыты. Этот код значительно упрощен и выглядит следующим образом:

public void doQueries() throws MyException {
   try (
      final Connection con = DriverManager.getConnection(dataSource);
      final PreparedStatement s1 = con.prepareStatement(updateSqlQuery);
      final PreparedStatement s2 = con.prepareStatement(selectSqlQuery);
      final ResultSet rs = s2.executeQuery()) {

      s1.executeUpdate();

         // Do something with rs

   } catch (SQLException e) {
      throw new MyException(e);
   }
}

[EDIT]: перенесено назначение rs в блок ресурсов, чтобы показать простейшую реализацию. На практике это простое решение на самом деле не работает, поскольку это неэффективно. Соединение следует использовать повторно, поскольку установление соединения является очень дорогостоящей операцией. Кроме того, этот простой пример не присваивает параметры запроса подготовленному оператору. Необходимо обратить внимание на эти сценарии, поскольку блок ресурсов должен включать только инструкции присваивания. Чтобы изобразить это, я также добавил еще один пример

   public void doQueries() throws MyException {

      final String updateSqlQuery = "select @@servername";
      final String selecSqlQuery  = "select * from mytable where col1 = ? and col2 > ?";
      final Object[] queryParams  = {"somevalue", 1};

      try (final Connection con = DriverManager.getConnection(dataSource);
         final PreparedStatement s1 = newPreparedStatement(con, updateSqlQuery);
         final PreparedStatement s2 = newPreparedStatement(con, selectSqlQuery, queryParams);
         final ResultSet rs = s2.executeQuery()) {

         s1.executeUpdate();

         while (!rs.next()) {
            // do something with the db record.
         }
      } catch (SQLException e) {
         throw new MyException(e);
      }
   }

   private static PreparedStatement newPreparedStatement(Connection con, String sql, Object... args) throws SQLException
   {
      final PreparedStatement stmt = con.prepareStatement(sql);
      for (int i = 0; i < args.length; i++)
         stmt.setObject(i, args[i]);
      return stmt;
   }