1. 概要

Java Database Connectivity(JDBC)API は、Javaアプリケーションからデータベースへのアクセスを提供します。 サポートされているJDBCドライバーが使用可能である限り、JDBCを使用して任意のデータベースに接続できます。

ResultSetは、データベースクエリの実行によって生成されたデータのテーブルです。このチュートリアルでは、 ResultSetAPIについて詳しく見ていきます。

2. ResultSetの生成

まず、 Statementインターフェイスを実装するオブジェクトでexecuteQuery()を呼び出すことにより、ResultSetを取得します。 PreparedStatementCallableStatementはどちらも、Statementのサブインターフェイスです。

PreparedStatement pstmt = dbConnection.prepareStatement("select * from employees");
ResultSet rs = pstmt.executeQuery();

ResultSet オブジェクトは、結果セットの現在の行を指すカーソルを維持します。  ResultSetnext()を使用して、レコードを反復処理します。

次に、 getX()メソッドを使用して結果を反復処理し、データベース列から値をフェッチします。ここで、Xは列のデータ型です。 実際、 getX()メソッドにデータベースの列名を提供します。

while(rs.next()) {
    String name = rs.getString("name");
    Integer empId = rs.getInt("emp_id");
    Double salary = rs.getDouble("salary");
    String position = rs.getString("position");
}

同様に、列のインデックス番号は、列名の代わりにgetX()メソッドで使用できます。 インデックス番号は、SQLselectステートメントの列のシーケンスです。

selectステートメントに列名がリストされていない場合、インデックス番号はテーブル内の列のシーケンスです。 列インデックスの番号付けは、次の1つから始まります。

Integer empId = rs.getInt(1);
String name = rs.getString(2);
String position = rs.getString(3);
Double salary = rs.getDouble(4);

3. ResultSetからのメタデータの取得

このセクションでは、ResultSetの列のプロパティとタイプに関する情報を取得する方法を説明します。

まず、 ResultSetgetMetaData()メソッドを使用して、ResultSetMetaDataを取得しましょう。

ResultSetMetaData metaData = rs.getMetaData();

次に、ResultSetにある列の数を取得しましょう。

Integer columnCount = metaData.getColumnCount();

さらに、メタデータオブジェクトで以下のメソッドのいずれかを使用して、各列のプロパティを取得できます。

  • getColumnName(int columnNumber) を使用して、列の名前を取得します
  • getColumnLabel(int columnNumber) は、SQLクエリのASの後に指定されている列のラベルにアクセスします
  • getTableName(int columnNumber) は、この列が属するテーブル名を取得します
  • getColumnClassName(int columnNumber) は、列のJavaデータ型を取得します
  • getColumnTypeName(int columnNumber) を使用して、データベース内の列のデータ型を取得します
  • getColumnType(int columnNumber) を使用して、列のSQLデータ型を取得します
  • isAutoIncrement(int columnNumber) は、列が自動インクリメントであるかどうかを示します
  • isCaseSensitive(int columnNumber) は、列の大文字と小文字を区別するかどうかを指定します
  • isSearchable(int columnNumber) は、SQLクエリのwhere句で列を使用できるかどうかを示します
  • isCurrency(int columnNumber) は、列に現金値が含まれているかどうかを通知します
  • isNullable(int columnNumber) は、列をnullにできない場合は zero を返し、列にnull値を含めることができる場合はoneを返します。 2列のNULL可能性が不明な場合
  • isSigned(int columnNumber) は、列の値が署名されている場合は true を返し、そうでない場合はfalseを返します。

列を繰り返し処理して、プロパティを取得しましょう。

for (int columnNumber = 1; columnNumber <= columnCount; columnNumber++) {
    String catalogName = metaData.getCatalogName(columnNumber);
    String className = metaData.getColumnClassName(columnNumber);
    String label = metaData.getColumnLabel(columnNumber);
    String name = metaData.getColumnName(columnNumber);
    String typeName = metaData.getColumnTypeName(columnNumber);
    int type = metaData.getColumnType(columnNumber);
    String tableName = metaData.getTableName(columnNumber);
    String schemaName = metaData.getSchemaName(columnNumber);
    boolean isAutoIncrement = metaData.isAutoIncrement(columnNumber);
    boolean isCaseSensitive = metaData.isCaseSensitive(columnNumber);
    boolean isCurrency = metaData.isCurrency(columnNumber);
    boolean isDefiniteWritable = metaData.isDefinitelyWritable(columnNumber);
    boolean isReadOnly = metaData.isReadOnly(columnNumber);
    boolean isSearchable = metaData.isSearchable(columnNumber);
    boolean isReadable = metaData.isReadOnly(columnNumber);
    boolean isSigned = metaData.isSigned(columnNumber);
    boolean isWritable = metaData.isWritable(columnNumber);
    int nullable = metaData.isNullable(columnNumber);
}

4. ResultSetのナビゲート

ResultSet を取得すると、カーソルの位置は最初の行の前になります。 さらに、デフォルトでは、ResultSetは順方向にのみ移動します。 ただし、他のナビゲーションオプションには、スクロール可能なResultSetを使用できます。

このセクションでは、さまざまなナビゲーションオプションについて説明します。

4.1. ResultSetタイプ

ResultSet タイプは、データセットをどのように操作するかを示します。

  • TYPE_FORWARD_ONLY – デフォルトのオプションで、カーソルが最初から最後まで移動します
  • TYPE_SCROLL_INSENSITIVE – カーソルは、データセット内を順方向と逆方向の両方に移動できます。 データセット内を移動しているときに基になるデータに変更があった場合、それらは無視されます。 データセットには、データベースクエリが結果を返したときのデータが含まれています
  • TYPE_SCROLL_SENSITIVE – スクロール非依存タイプと同様ですが、このタイプの場合、データセットは基になるデータへの変更を即座に反映します

すべてのデータベースがすべてのResultSetタイプをサポートしているわけではありません。 それでは、DatabaseMetaDataオブジェクトのsupportsResultSetTypeを使用して、タイプがサポートされているかどうかを確認しましょう。

DatabaseMetaData dbmd = dbConnection.getMetaData();
boolean isSupported = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);

4.2. スクロール可能なResultSet

スクロール可能なResultSetを取得するには、Statement の準備中に、いくつかの追加パラメーターを渡す必要があります。

たとえば、TYPE_SCROLL_INSENSITIVEまたはTYPE_SCROLL_SENSITIVEResultSetタイプとして使用することにより、スクロール可能なResultSetを取得します。

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_INSENSITIVE,
  ResultSet.CONCUR_UPDATABLE); 
ResultSet rs = pstmt.executeQuery();

4.3. ナビゲーションオプション

スクロール可能なResultSetでは、以下のオプションのいずれかを使用できます。

  • next() –現在の位置から次の行に進みます
  • previous() –前の行に移動します
  • first()– は、ResultSetの最初の行に移動します
  • last()–は最後の行にジャンプします
  • beforeFirst()–は先頭に移動します。 このメソッドを呼び出した後、 ResultSetnext()を呼び出すと、ResultSetから最初の行が返されます。
  • afterLast()–は最後まで飛躍します。 このメソッドの実行後にResultSetprevious()を呼び出すと、ResultSetから最後の行が返されます。
  • relative(int numOfRows)– は、numOfRowsによって現在の位置から前後に移動します
  • abstract(int rowNumber)–は指定されたrowNumberにジャンプします

いくつかの例を見てみましょう:

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_SENSITIVE,
  ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    // iterate through the results from first to last
}
rs.beforeFirst(); // jumps back to the starting point, before the first row
rs.afterLast(); // jumps to the end of resultset

rs.first(); // navigates to the first row
rs.last(); // goes to the last row

rs.absolute(2); //jumps to 2nd row

rs.relative(-1); // jumps to the previous row
rs.relative(2); // jumps forward two rows

while (rs.previous()) {
    // iterates from current row to the first row in backward direction
}

4.4. ResultSet行数

getRow()を使用して、ResultSetの現在の行番号を取得しましょう。

まず、 ResultSet の最後の行に移動し、 getRow()を使用してレコード数を取得します。

rs.last();
int rowCount = rs.getRow();

5. ResultSetのデータを更新しています

デフォルトでは、ResultSetは読み取り専用です。 ただし、更新可能な ResultSet を使用して、行を挿入、更新、および削除することができます。

5.1. ResultSet同時実行性

同時実行モードは、ResultSetがデータを更新できるかどうかを示します。

CONCUR_READ_ONLY オプションがデフォルトであり、ResultSetを使用してデータを更新する必要がない場合に使用する必要があります。

ただし、 ResultSet のデータを更新する必要がある場合は、CONCUR_UPDATABLEオプションを使用する必要があります。

すべてのデータベースがすべてのResultSetタイプのすべての同時実行モードをサポートしているわけではありません。 したがって、 supportsResultSetConcurrency()メソッドを使用して、目的のタイプと同時実行モードがサポートされているかどうかを確認する必要があります。

DatabaseMetaData dbmd = dbConnection.getMetaData(); 
boolean isSupported = dbmd.supportsResultSetConcurrency(
  ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);

5.2. 更新可能なResultSetの取得

更新可能なResultSetを取得するには、Statementを準備するときに追加のパラメーターを渡す必要があります。 そのために、ステートメントを作成するときに3番目のパラメーターとしてCONCUR_UPDATABLEを使用しましょう。

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_SENSITIVE,
  ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery();

5.3. 行の更新

このセクションでは、前のセクションで作成した更新可能なResultSetを使用して行を更新します。

updateX()メソッドを呼び出し、更新する列の名前と値を渡すことで、行のデータを更新できます。 updateX()メソッドでは、Xの代わりにサポートされている任意のデータ型を使用できます。

タイプdouble「salary」列を更新してみましょう。

rs.updateDouble("salary", 1100.0);

これはResultSetのデータを更新するだけですが、変更はまだデータベースに保存されていないことに注意してください。

最後に、 updateRow()を呼び出してデータベースへの更新を保存します

rs.updateRow();

列名の代わりに、列インデックスを updateX()メソッドに渡すことができます。 これは、 getX()メソッドを使用して値を取得するために列インデックスを使用することに似ています。 列名またはインデックスのいずれかをupdateX()メソッドに渡すと、同じ結果が得られます。

rs.updateDouble(4, 1100.0);
rs.updateRow();

5.4. 行の挿入

次に、更新可能なResultSetを使用して新しい行を挿入しましょう。

まず、 moveToInsertRow()を使用してカーソルを移動し、新しい行を挿入します。

rs.moveToInsertRow();

次に、 updateX()メソッドを呼び出して、情報を行に追加する必要があります。 データベーステーブルのすべての列にデータを提供する必要があります。 すべての列にデータを提供しない場合は、デフォルトの列値が使用されます。

rs.updateString("name", "Venkat"); 
rs.updateString("position", "DBA"); 
rs.updateDouble("salary", 925.0);

次に、 insertRow()を呼び出して、データベースに新しい行を挿入しましょう。

rs.insertRow();

最後に、 moveToCurrentRow()。を使用します。これにより、 moveToInsertRow()メソッドを使用して新しい行の挿入を開始する前の行にカーソル位置が戻ります。

rs.moveToCurrentRow();

5.5. 行の削除

このセクションでは、更新可能なResultSetを使用して行を削除します。

まず、削除する行に移動します。 次に、 deleteRow()メソッドを呼び出して、現在の行を削除します。

rs.absolute(2);
rs.deleteRow();

6. 保持性

保持可能性は、データベーストランザクションの終了時にResultSetを開くか閉じるかを決定します。

6.1. 保持タイプ

トランザクションのコミット後にResultSetが不要な場合は、CLOSE_CURSORS_AT_COMMITを使用してください。

HOLD_CURSORS_OVER_COMMIT を使用して、保持可能なResultSetを作成します。 保持可能なResultSetは、データベーストランザクションがコミットされた後でも閉じられません。

すべてのデータベースがすべての保持タイプをサポートしているわけではありません。

それでは、 DatabaseMetaDataオブジェクトでsupportsResultSetHoldability()を使用して、保持可能性タイプがサポートされているかどうかを確認しましょう。 次に、 getResultSetHoldability()を使用してデータベースのデフォルトの保持可能性を取得します。

boolean isCloseCursorSupported
  = dbmd.supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT);
boolean isOpenCursorSupported
  = dbmd.supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
boolean defaultHoldability
  = dbmd.getResultSetHoldability();

6.2. ホールド可能ResultSet

保持可能なResultSetを作成するには、 Statement を作成するときに、最後のパラメーターとしてholdabilityタイプを指定する必要があります。 このパラメーターは、並行性モードの後に指定されます。

Microsoft SQL Server(MSSQL)を使用している場合は、 ResultSet ではなく、データベース接続で保持可能性を設定する必要があることに注意してください。

dbConnection.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);

これを実際に見てみましょう。 まず、ステートメントを作成し、保持可能性をHOLD_CURSORS_OVER_COMMITに設定します。

Statement pstmt = dbConnection.createStatement(
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_UPDATABLE, 
  ResultSet.HOLD_CURSORS_OVER_COMMIT)

それでは、データを取得しながら行を更新しましょう。 これは、データベースに更新トランザクションをコミットした後、 ResultSet を繰り返し処理することを除いて、前に説明した更新の例と似ています。 これは、MySQLデータベースとMSSQLデータベースの両方で正常に機能します。

dbConnection.setAutoCommit(false);
ResultSet rs = pstmt.executeQuery("select * from employees");
while (rs.next()) {
    if(rs.getString("name").equalsIgnoreCase("john")) {
        rs.updateString("name", "John Doe");
        rs.updateRow();
        dbConnection.commit();
    }                
}
rs.last();

MySQLがサポートするのはHOLD_CURSORS_OVER_COMMITのみであることに注意してください。 したがって、 CLOSE_CURSORS_AT_COMMIT を使用しても、無視されます。

MSSQLデータベースは、CLOSE_CURSORS_AT_COMMITをサポートしています。 これは、トランザクションをコミットするときにResultSetが閉じられることを意味します。 その結果、トランザクションをコミットした後に ResultSet にアクセスしようとすると、「カーソルはオープンエラーではありません」という結果になります。 したがって、ResultSetからそれ以上のレコードを取得することはできません。

7. フェッチサイズ

通常、データを ResultSet にロードする場合、データベースドライバーはデータベースからフェッチする行数を決定します。 たとえば、MySQLデータベースでは、 ResultSet は通常、すべてのレコードを一度にメモリにロードします。

ただし、JVMメモリに収まらない多数のレコードを処理する必要がある場合があります。 この場合、StatementまたはResultSetオブジェクトのいずれかでフェッチサイズプロパティを使用して、最初に返されるレコードの数を制限できます。

追加の結果が必要な場合は常に、ResultSetはデータベースからレコードの別のバッチをフェッチします。 fetch sizeプロパティを使用すると、データベーストリップごとにフェッチする行数についてデータベースドライバーに提案を提供できます。 指定したフェッチサイズは、後続のデータベーストリップに適用されます。

ResultSet のフェッチサイズを指定しない場合は、Statementのフェッチサイズが使用されます。 StatementまたはResultSetのいずれにもフェッチサイズを指定しない場合は、データベースのデフォルトが使用されます。

7.1. ステートメントでのフェッチサイズの使用

それでは、Statementのフェッチサイズを実際に見てみましょう。 ステートメントのフェッチサイズを10レコードに設定します。 クエリが100レコードを返す場合、データベースのラウンドトリップは10回発生し、毎回10レコードが読み込まれます。

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees", 
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(10);

ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    // iterate through the resultset
}

7.2. ResultSetでのフェッチサイズの使用

次に、 ResultSet を使用して、前の例のフェッチサイズを変更しましょう。

まず、ステートメントでフェッチサイズを使用します。 これにより、 ResultSet は、クエリの実行後に最初に10レコードをロードできます。

次に、ResultSetのフェッチサイズを変更します。 これにより、ステートメントで以前に指定したフェッチサイズが上書きされます。 したがって、以降のすべてのトリップでは、すべてのレコードがロードされるまで20レコードがロードされます。

その結果、すべてのレコードをロードするためのデータベーストリップは6回だけになります。

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees", 
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(10);

ResultSet rs = pstmt.executeQuery();
 
rs.setFetchSize(20); 

while (rs.next()) { 
    // iterate through the resultset 
}

最後に、結果を反復処理しながらResultSetのフェッチサイズを変更する方法を説明します。

前の例と同様に、最初にStatementでフェッチサイズを10に設定します。 したがって、最初の3回のデータベーストリップでは、トリップごとに10レコードがロードされます。

次に、30番目のレコードを読み取りながら、ResultSetのフェッチサイズを20に変更します。 したがって、次の4回のトリップでは、各トリップごとに20レコードがロードされます。

したがって、100個のレコードすべてをロードするには、7回のデータベーストリップが必要です。

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees", 
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(10);

ResultSet rs = pstmt.executeQuery();

int rowCount = 0;

while (rs.next()) { 
    // iterate through the resultset 
    if (rowCount == 30) {
        rs.setFetchSize(20); 
    }
    rowCount++;
}

8. 結論

この記事では、 ResultSetAPIを使用してデータベースからデータを取得および更新する方法について説明しました。 ここで説明した高度な機能のいくつかは、使用しているデータベースに依存しています。 したがって、これらの機能を使用する前に、それらの機能のサポートを確認する必要があります。

いつものように、コードはGitHubから入手できます。