JDBCでtry-with-resourcesを使用するにはどうすればよいですか?


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

5
2番目の例では、ResultSetオブジェクトはそれを生成したStatementオブジェクトによって自動的に閉じられるtry (ResultSet rs = ps.executeQuery()) {ため、内部は必要ありません
Alexander Farber

2
@AlexanderFarber残念ながら、自分でリソースを閉じられなかったドライバーに悪名高い問題がありました。ハードノックスの学校は明示的に常に近いすべてのJDBCリソースに私たちを教え、周りのtry--リソースとを使用して容易になりConnectionPreparedStatementResultSetあまりにも。try-with-resourcesを使用すると、コードが非常に簡単になり、コードが意図どおりに自己文書化されるので、実際にそうする理由はありません。
バジルブルク

回答:


85

あなたの例では外側の試行の必要はないので、少なくとも3から2に下げることができ;、リソースリストの最後で閉じる必要もありません。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;
}

5
どのように電話しConnection::setAutoCommitますか?そのような呼び出しはとのtry間では許可されていません。接続プールでバッキングされる可能性のあるDataSourceから接続を取得する場合、autoCommitの設定方法を想定できません。con = ps =
バジルブルク2015

1
通常、メソッドに接続を挿入します(OPの質問に示されているアドホックアプローチとは異なります)。接続を提供または閉じるために呼び出される接続管理クラスを使用できます(プールされているかどうかに関係なく)。そのマネージャーでは、接続動作を指定できます
svarog

@BasilBourque DriverManager.getConnection(myConnectionURL)autoCommitフラグも設定して接続を返すメソッドに移動できます(またはcreatePreparedStatement前の例のメソッドに相当するものに設定します...)
rogerdpack

@rogerdpackはい、それは理にかなっています。独自に実装していDataSourceた場合getConnection、あなたが言うように方法はありませんが、接続を取得し、必要に応じて設定し、その後、接続を渡します。
バジルブルク2017

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
いいえ、カバーされています。問題は、上記のコードがSQLExceptionのスローを宣言していないメソッドの内部からprepareStatementを呼び出していることです。また、上記のコードには、準備されたステートメントを閉じずに失敗する可能性のあるパスが少なくとも1つあります(setIntの呼び出し中にSQLExceptionが発生した場合)
Trejkaz

1
@Trejkazは、PreparedStatementを閉じない可能性について良い点があります。思わなかったけど正解!
Jeanne Boyarsky、2013年

2
@ArturoTenaはい-注文は保証されます
Jeanne Boyarsky

2
@JeanneBoyarskyこれを行う別の方法はありますか?そうでない場合、私は、各SQL文のための特定のcreatePreparedStatementメソッドを作成する必要があります
ジョン・アレクサンダー・ベッツ

1
TrejkazのコメントについてcreatePreparedStatementは、どのように使用しても安全ではありません。これを修正するには、setInt(...)の周りにtry-catchを追加し、anyをキャッチし、SQLExceptionそれが発生したらps.close()を呼び出して例外を再スローする必要があります。しかし、その結果、OPが改善したいと思っていたコードとほぼ同じ長さで不格好なコードになります。
Florian F

4

ここでは、ラムダとJDK 8サプライヤーを使用してすべてを外側の試みに合わせる簡潔な方法を示します。

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 2016年

この場合、SQLExceptionを明示的にキャッチする必要があるとは思いません。実際には、try-with-resourcesでは「オプション」です。他の答えはこれについて言及していません。したがって、おそらくこれをさらに簡略化できます。
djangofan 2017

DriverManager.getConnection(JDBC_URL、prop); nullを返しますか?
gaurav

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
上記のコメントには、そうではないとの記載はありません。
トレイカズ2013年

2

他の人が述べたように、あなたのコードは基本的には正しいですが、外側tryは不要です。ここにいくつかの考えがあります。

DataSource

ここでの他の答えは、正しいと良いです受け入れ答え bpgergoによります。しかし、最近のJavaでのDataSource使用よりも一般的に推奨されるの使用を示しているものはありませんDriverManager

したがって、完全を期すために、データベースサーバーから現在の日付をフェッチする完全な例を次に示します。ここで使用されているデータベースはPostgresです。他のデータベースも同様に機能します。の使用を、データベースorg.postgresql.ds.PGSimpleDataSourceDataSource適したの実装に置き換えます。特定のドライバー、またはそのルートをたどると接続プールによって実装が提供される可能性があります。

DataSource実装は必要はありません、それは「開かれていない」んので、閉鎖されます。A DataSourceはリソースではなく、データベースに接続されていないため、データベースサーバー上のネットワーク接続もリソースも保持していません。A DataSourceは、データベースへの接続を確立するときに必要な情報であり、データベースサーバーのネットワーク名またはアドレス、ユーザー名、ユーザーパスワード、および最終的に接続を確立するときに指定するさまざまなオプションが含まれます。したがって、DataSource実装オブジェクトは、try-with-resources括弧内に入りませ

ネストされたtry-with-resources

コードはネストされたtry-with-resourcesステートメントを適切に使用します。

以下のサンプルコードでは、try-with-resources構文を2回使用していることにも注意してください。外部tryは2つのリソースを定義します:ConnectionPreparedStatement。内部tryResultSetリソースを定義します。これは一般的なコード構造です。

例外が内部の例外からスローされ、そこでキャッチされない場合、ResultSetリソースは自動的に閉じられます(存在する場合、nullではありません)。その後、PreparedStatementが閉じられ、最後にConnectionが閉じられます。リソースは、try-with-resourceステートメント内で宣言された逆の順序で自動的に閉じられます。

ここのサンプルコードは非常に単純化されています。書かれているように、1つのtry-with-resourcesステートメントで実行できます。しかし、実際の作業では、ネストされたtry呼び出しのペアの間でより多くの作業を行う可能性があります。たとえば、ユーザーインターフェイスまたはPOJOから値を抽出?し、PreparedStatement::set…メソッドへの呼び出しを介してSQL内のプレースホルダーを満たすためにそれらを渡す場合があります。

構文メモ

末尾のセミコロン

try-with-resourcesの括弧内の最後のリソースステートメントの後のセミコロンはオプションであることに注意してください。私は2つの理由で自分の作業にそれを含めています。一貫性と完全に見えること、および行末のセミコロンを気にすることなく、行の組み合わせをコピーして貼り付けることが容易になります。IDEは最後のセミコロンに不必要なフラグを立てることがありますが、そのままにしても問題はありません。

Java 9 – try-with-resourcesで既存の変数を使用する

Java 9の新機能は、try-with-resources構文の拡張です。これで、tryステートメントの括弧の外側にリソースを宣言してデータを追加できます。私はまだこれがJDBCリソースに役立つとは思っていませんが、あなた自身の仕事でそれを覚えておいてください。

ResultSet 閉じますが、閉じない場合があります

理想的な世界でResultSetは、ドキュメンテーションが約束するように、それ自体が閉じます。

ResultSetオブジェクトは、それを生成したStatementオブジェクトが閉じられるか、再実行されるか、複数の結果のシーケンスから次の結果を取得するために使用されると、自動的に閉じられます。

残念ながら、過去に一部のJDBCドライバーは悪名高くこの約束を果たせなかった。その結果、多くのJDBCプログラマは、明示的に近いすべてのJDBCリソースを含むことを学んだConnectionPreparedStatementResultSetあまりにも。最新のtry-with-resources構文により、コードがよりコンパクトになり、操作が簡単になりました。Javaのチームはマーキングの気に行ったことをお知らせResultSetとしてAutoCloseable、私たちはそれを使用することをお勧めします。すべてのJDBCリソースの周りにtry-with-resourcesを使用すると、コードの意図がより自己文書化されます。

コード例

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." );
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.