Guida all'interfaccia ResultSet JDBC

1. Panoramica

L'API JDBC (Java Database Connectivity) fornisce l'accesso al database da un'applicazione Java. È possibile utilizzare JDBC per connettersi a qualsiasi database purché sia ​​disponibile il driver JDBC supportato.

Il ResultSet è una tabella di dati generata dall'esecuzione di query di database. In questo tutorial, daremo uno sguardo più approfondito all'API ResultSet .

2. Generazione di un ResultSet

Innanzitutto, recuperiamo un ResultSet chiamando executeQuery () su qualsiasi oggetto che implementa l' interfaccia Statement . Sia PreparedStatement che CallableStatement sono sottointerfacce di Statement :

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

L' oggetto ResultSet mantiene un cursore che punta alla riga corrente del set di risultati. Useremo next () sul nostro ResultSet per scorrere i record.

Successivamente, utilizzeremo i metodi getX () durante l'iterazione dei risultati per recuperare i valori dalle colonne del database , dove X è il tipo di dati della colonna. In effetti, forniremo i nomi delle colonne del database ai metodi 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"); } 

Allo stesso modo, il numero di indice della colonna può essere utilizzato con i metodi getX () invece del nome della colonna. Il numero di indice è la sequenza delle colonne nell'istruzione SQL select.

Se l'istruzione select non elenca i nomi delle colonne, il numero di indice è la sequenza di colonne nella tabella. La numerazione dell'indice delle colonne inizia da uno:

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

3. Recupero dei metadati dal ResultSet

In questa sezione vedremo come recuperare informazioni sulle proprietà e sui tipi di colonna in un ResultSet .

Innanzitutto, usiamo il metodo getMetaData () sul nostro ResultSet per ottenere il ResultSetMetaData :

ResultSetMetaData metaData = rs.getMetaData();

Successivamente, otteniamo il numero di colonne presenti nel nostro ResultSet :

Integer columnCount = metaData.getColumnCount();

Inoltre, possiamo utilizzare uno dei metodi seguenti sul nostro oggetto metadati per recuperare le proprietà di ogni colonna:

  • getColumnName (int columnNumber) - per ottenere il nome della colonna
  • getColumnLabel (int columnNumber) - per accedere all'etichetta della colonna, che è specificata dopo AS nella query SQL
  • getTableName (int columnNumber) - per ottenere il nome della tabella a cui appartiene questa colonna
  • getColumnClassName (int columnNumber) - per acquisire il tipo di dati Java della colonna
  • getColumnTypeName (int columnNumber) - per ottenere il tipo di dati della colonna nel database
  • getColumnType (int columnNumber) - per ottenere il tipo di dati SQL della colonna
  • isAutoIncrement (int columnNumber) - indica se la colonna è a incremento automatico
  • isCaseSensitive (int columnNumber) - specifica se il caso della colonna è importante
  • isSearchable (int ColumnNumber) - suggerisce se possiamo utilizzare la colonna in cui clausola della query SQL
  • isCurrency (int columnNumber) - segnala se la colonna contiene un valore in contanti
  • isNullable (int columnNumber) - restituisce zero se la colonna non può essere nulla, uno se la colonna può contenere un valore null e due se il nullability della colonna è sconosciuto
  • isSigned (int columnNumber) - restituisce true se i valori nella colonna sono firmati, altrimenti restituisce false

Analizziamo le colonne per ottenere le loro proprietà:

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. Navigazione nel ResultSet

Quando otteniamo un ResultSet , la posizione del cursore è prima della prima riga. Inoltre, per impostazione predefinita, il ResultSet si sposta solo in avanti. Tuttavia, possiamo utilizzare un ResultSet scorrevole per altre opzioni di navigazione.

In questa sezione discuteremo le varie opzioni di navigazione.

4.1. Tipi di ResultSet

Il tipo di ResultSet indica come guideremo attraverso il set di dati:

  • TYPE_FORWARD_ONLY - l'opzione predefinita, in cui il cursore si sposta dall'inizio alla fine
  • TYPE_SCROLL_INSENSITIVE: il nostro cursore può spostarsi nel set di dati sia in avanti che indietro; se ci sono modifiche ai dati sottostanti durante lo spostamento nel set di dati, vengono ignorate; il set di dati contiene i dati dal momento in cui la query del database restituisce il risultato
  • TYPE_SCROLL_SENSITIVE - simile al tipo insensibile allo scorrimento, tuttavia per questo tipo, il set di dati riflette immediatamente eventuali modifiche ai dati sottostanti

Non tutti i database supportano tutti i tipi di ResultSet . Quindi, controlliamo se il tipo è supportato utilizzando il supportsResultSetType sul nostro oggetto DatabaseMetaData :

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

4.2. ResultSet scorrevole

Per ottenere un ResultSet scorrevole , è necessario passare alcuni parametri aggiuntivi durante la preparazione dell'istruzione .

Ad esempio, otterremmo un ResultSet scorrevole utilizzando TYPE_SCROLL_INSENSITIVE o TYPE_SCROLL_SENSITIVE come tipo di ResultSet :

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

4.3. Opzioni di navigazione

Possiamo utilizzare una qualsiasi delle seguenti opzioni su un ResultSet scorrevole :

  • next () : passa alla riga successiva dalla posizione corrente
  • previous () - passa alla riga precedente
  • first () - passa alla prima riga del ResultSet
  • last () - salta all'ultima riga
  • beforeFirst() – moves to the start; calling next() on our ResultSet after calling this method returns the first row from our ResultSet
  • afterLast() – leaps to the end; calling previous() on our ResultSet after executing this method returns the last row from our ResultSet
  • relative(int numOfRows) – go forward or backward from the current position by the numOfRows
  • absolute(int rowNumber) – jumps to the rowNumber specified

Let's see some examples:

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 Row Count

Let’s use getRow() to get the current row number of our ResultSet.

First, we’ll navigate to the last row of the ResultSet and then use getRow() to get the number of records:

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

5. Updating Data in a ResultSet

By default, the ResultSet is read-only. However, we can use an updatable ResultSet to insert, update, and delete the rows.

5.1. ResultSet Concurrency

The concurrency mode indicates if our ResultSet can update the data.

The CONCUR_READ_ONLY option is the default and should be used if we don't need to update the data using our ResultSet.

However, if we need to update the data in our ResultSet, then the CONCUR_UPDATABLE option should be used.

Not all databases support all the concurrency modes for all ResultSet types. Therefore, we need to check if our desired type and concurrency mode are supported using the supportsResultSetConcurrency() method:

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

5.2. Obtaining an Updatable ResultSet

To obtain an updatable ResultSet, we need to pass an additional parameter when we prepare the Statement. For that, let’s use CONCUR_UPDATABLE as the third parameter while creating a statement:

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

5.3. Updating a Row

In this section, we'll update a row using the updatable ResultSet created in the previous section.

We can update data in a row by calling updateX() methods, passing the column names and values to update. We can use any supported data type in place of X in the updateX() method.

Let's update the “salary” column, which is of type double:

rs.updateDouble("salary", 1100.0);

Note that this just updates the data in the ResultSet, but the modifications are not yet saved back to the database.

Finally, let’s call updateRow() to save the updates to the database:

rs.updateRow(); 

Instead of the column names, we can pass the column index to the updateX() methods. This is similar to using the column index for getting the values using getX() methods. Passing either the column name or index to the updateX() methods yields the same result:

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

5.4. Inserting a Row

Now, let's insert a new row using our updatable ResultSet.

First, we'll use moveToInsertRow() to move the cursor to insert a new row:

rs.moveToInsertRow();

Next, we must call updateX() methods to add the information to the row. We need to provide data to all the columns in the database table. If we don't provide data to every column, then the default column value is used:

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

Then, let's call insertRow() to insert a new row into the database:

rs.insertRow();

Finally, let's use moveToCurrentRow(). This will take the cursor position back to the row we were at before we started inserting a new row using the moveToInsertRow() method:

rs.moveToCurrentRow();

5.5. Deleting a Row

In this section, we'll delete a row using our updatable ResultSet.

First, we'll navigate to the row we want to delete. Then, we'll call the deleteRow() method to delete the current row:

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

6. Holdability

The holdability determines if our ResultSet will be open or closed at the end of a database transaction.

6.1. Holdability Types

Use CLOSE_CURSORS_AT_COMMIT if the ResultSet is not required after the transaction is committed.

Use HOLD_CURSORS_OVER_COMMIT to create a holdable ResultSet. A holdable ResultSet is not closed even after the database transaction is committed.

Not all databases support all the holdability types.

So, let's check if the holdability type is supported using supportsResultSetHoldability() on our DatabaseMetaData object. Then, we'll get the default holdability of the database using 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. Holdable ResultSet

To create a holdable ResultSet, we need to specify the holdability type as the last parameter while creating a Statement. This parameter is specified after the concurrency mode.

Note that if we're using Microsoft SQL Server (MSSQL), we have to set holdability on the database connection, rather than on the ResultSet:

dbConnection.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);

Let's see this in action. First, let's create a Statement, setting the holdability to HOLD_CURSORS_OVER_COMMIT:

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

Now, let's update a row while retrieving the data. This is similar to the update example we discussed earlier, except that we'll continue to iterate through the ResultSet after committing the update transaction to the database. This works fine on both MySQL and MSSQL databases:

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

It's worth noting that MySQL supports only HOLD_CURSORS_OVER_COMMIT. So, even if we use CLOSE_CURSORS_AT_COMMIT, it will be ignored.

The MSSQL database supports CLOSE_CURSORS_AT_COMMIT. This means that the ResultSet will be closed when we commit the transaction. As a result, an attempt to access the ResultSet after committing the transaction results in a ‘Cursor is not open error’. Therefore, we can’t retrieve further records from the ResultSet.

7. Fetch Size

Typically, when loading data into a ResultSet, the database drivers decide on the number of rows to fetch from the database. On a MySQL database, for example, the ResultSet normally loads all the records into memory at once.

Sometimes, however, we may need to deal with a large number of records that won't fit into our JVM memory. In this case, we can use the fetch size property either on our Statement or ResultSet objects to limit the number of records initially returned.

Whenever additional results are required, ResultSet fetches another batch of records from the database. Using the fetch size property, we can provide a suggestion to the database driver on the number of rows to fetch per database trip. The fetch size we specify will be applied to the subsequent database trips.

If we don't specify the fetch size for our ResultSet, then the fetch size of the Statement is used. If we don't specify fetch size for either the Statement or the ResultSet, then the database default is used.

7.1. Using Fetch Size on Statement

Now, let's see the fetch size on Statement in action. We'll set the fetch size of the Statement to 10 records. If our query returns 100 records, then there will be 10 database round trips, loading 10 records each time:

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. Using Fetch Size on ResultSet

Now, let's change the fetch size in our previous example using the ResultSet.

First, we'll use the fetch size on our Statement. This allows our ResultSet to initially load 10 records after executing the query.

Then, we'll modify the fetch size on the ResultSet. This will override the fetch size we earlier specified on our Statement. So, all the subsequent trips will load 20 records until all the records are loaded.

As a result, there will be only 6 database trips to load all the records:

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 }

Finally, we'll see how to modify the fetch size of the ResultSet while iterating the results.

Similar to the previous example, we'll first set the fetch size to 10 on our Statement. So, our first 3 database trips will load 10 records per each trip.

Quindi, modificheremo la dimensione di recupero sul nostro ResultSet su 20 durante la lettura del 30 ° record. Quindi, i prossimi 4 viaggi caricheranno 20 record per ogni viaggio.

Pertanto, avremo bisogno di 7 viaggi nel database per caricare tutti i 100 record:

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. Conclusione

In questo articolo, abbiamo visto come utilizzare l' API ResultSet per recuperare e aggiornare i dati da un database. Molte delle funzionalità avanzate di cui abbiamo discusso dipendono dal database che stiamo utilizzando. Pertanto, è necessario verificare il supporto per tali funzionalità prima di utilizzarle.

Come sempre, il codice è disponibile su GitHub.