1

I've created a method to fetch details from Database which returns an object of ResultSet by executing a callableStatement. I want to close the Connection & CallableStatement. For performing this I'm using try-with-resource. But the problem arise here, as I'm return the ResultSet from my method, it also gets closed and throws an exception while fetching data from it:

com.microsoft.sqlserver.jdbc.SQLServerException: The result set is closed.

As per my knowledge, ResultSet will automatically get closed when either all data is fetched from ResultSet or if the CallableStatement will get closed. In my case second thing is happening as I'm using try-with-resource.

This is how my code looks like.

public ResultSet getUSStates() throws SQLException {
    try(
            Connection inGlobalConnnections = getConnection();
            CallableStatement csmt = inGlobalConnnections.prepareCall("{call mst.USP_GetUSStates()}");) {

        csmt.execute();
        return csmt.getResultSet();
    } catch (Exception e) {
        _log.error(e, e);
    }

    return null;
}

What should I do to fetch records and close the connections afterwards?

NOTE: I'm using multiple methods like this to fetch details from database. So, please suggest way where I have to perform minimal changes in my code.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • The caller should close it – g00se Aug 29 '23 at 19:31
  • 2
    Don't return a ResultSet. Copy the data into a Data Transfer Object (q.v.) and return that. – David Conrad Aug 29 '23 at 19:44
  • The trouble with that approach is it might not scale well. Jdbc returns a cursor for good reasons – g00se Aug 29 '23 at 20:23
  • @DavidConrad As I mentioned earlier, I'm using multiple methods like this. So, by going with DTO approach I need to update all those methods by changing into my code. – Anubhav kalra Aug 30 '23 at 07:41
  • If you can comfortably hold your results in memory then you only need one method, say `processResults` inside each try-with-resources block – g00se Aug 30 '23 at 08:53

1 Answers1

2

As the comments suggest, a ResultSet should generally be processed immediately, not handed off.

I suggest either:

  • Transform the ResultSet into a RowSet.
  • Process the database rows into a collection of Java objects. Often a good time for Java records.

javax.sql.RowSet

Instead, use the sub-interface of ResultSet that is meant for handing off data directly from the database: RowSet. In your case, CachedRowSet sounds appropriate.

See tutorial by Oracle. And see article by Michael Good on Baeldung.

The catch is finding a concrete implementation of that interface. You may find one bundled with JDKs based on OpenJDK. But as I recall, that implementation is very old (like dating back to the Sun Microsystems days), and has not been maintained/updated.

Row sets always seemed like a great idea to me. But apparently they have never received the attention they deserve.

For more on obtaining implementations of RowSet, see these Questions: here and here.

Here is a full example app, using H2 Database Engine.

Notice how, in modern Java, RowSetProvider uses the Java Service Provider Interface (SPI) to locate and load an implementation at runtime. (Well, I think that is what’s going on here.) See: What the reason to use RowSetProvider to obtain RowSetFactory?.

package work.basil.example;

import org.h2.jdbcx.JdbcDataSource;

import javax.sql.DataSource;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetProvider;
import java.sql.*;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

/**
 * Example of using a disconnected RowSet, specifically a CachedRowSet,
 * to keep a ResultSet even after disconnecting from database.
 */
public class App
{
    public static void main ( String[] args )
    {
        System.out.println ( "Hello World! " + Instant.now ( ) );
        App app = new App ( );
        app.demo ( );
    }

    private void demo ( )
    {
        DataSource dataSource = this.getDataSource ( );
        this.createDatabase ( dataSource );
        this.populateTable ( dataSource );
        this.dumpTable ( dataSource );
        CachedRowSet rowSet = this.fetchRowSet ( dataSource );
        this.reportRowSet ( rowSet );
    }

    private DataSource getDataSource ( )
    {
        org.h2.jdbcx.JdbcDataSource ds = new JdbcDataSource ( ); // The `javax.sql.DataSource` interface implemented by `org.h2.jdbcx.JdbcDataSource`.
        ds.setURL ( "jdbc:h2:mem:" + "RowSetExDb" + ";DB_CLOSE_DELAY=-1" );  // Set delay to -1 to keep in-memory database even after last connection closed.
        ds.setUser ( "scott" );
        ds.setPassword ( "tiger" );
        ds.setDescription ( "Dummy database for demo showing how to use a RowSet after closing connection." );
        return ds; // Generalizing from the concrete class to the interface.
    }

    private void createDatabase ( final DataSource dataSource )
    {
        String sql = """
                CREATE TABLE person_ ( 
                    pkey_ UUID DEFAULT random_uuid() PRIMARY KEY , 
                    name_ VARCHAR NOT NULL 
                ) ;
                """;
        try (
                Connection conn = dataSource.getConnection ( ) ;
                Statement stmt = conn.createStatement ( ) ;
        )
        {
            stmt.execute ( sql );
        }
        catch ( SQLException e ) { throw new RuntimeException ( e ); }
    }

    private void populateTable ( final DataSource dataSource )
    {
        String sql = """
                INSERT INTO person_ ( name_ ) 
                VALUES ( ? ) 
                ;
                """;

        List < String > names = List.of ( "Alice" , "Bob" , "Carol" , "Davis" ); // Insert a row for each of these names.
        System.out.println ( "Inserting list of names: " + names );
        try (
                Connection conn = dataSource.getConnection ( ) ;
                PreparedStatement ps = conn.prepareStatement ( sql ) ;
        )
        {
            for ( String name : names )
            {
                ps.setString ( 1 , name );
                ps.executeUpdate ( );
            }
        }
        catch ( SQLException e ) { throw new RuntimeException ( e ); }
    }

    private void dumpTable ( final DataSource dataSource )
    {
        // Retrieve rows from a `ResultSet`.
        String sql = """
                SELECT * 
                FROM person_ 
                ;
                """;
        try (
                Connection conn = dataSource.getConnection ( ) ;
        )
        {
            try (
                    Statement stmt = conn.createStatement ( ) ;
                    ResultSet rs = stmt.executeQuery ( sql ) ;
            )
            {
                record Person( UUID id ,
                               String name )
                {
                }
                while ( rs.next ( ) )
                {
                    UUID pkey = rs.getObject ( "pkey_" , UUID.class );
                    String name = rs.getString ( "name_" );
                    System.out.println ( new Person ( pkey , name ) );
                }
            }
        }
        catch ( SQLException e ) { throw new RuntimeException ( e ); }
    }

    private CachedRowSet fetchRowSet ( final DataSource dataSource )
    {
        // Retrieve rows from a `ResultSet`.
        String sql =
                """
                        SELECT * 
                        FROM person_ 
                        ;
                        """;
        try (
                Connection conn = dataSource.getConnection ( ) ;
                Statement stmt = conn.createStatement ( ) ;
                ResultSet rs = stmt.executeQuery ( sql ) ;
        )
        {
            CachedRowSet crs = RowSetProvider.newFactory ( ).createCachedRowSet ( );
            crs.populate ( rs );
            return crs;
        }
        catch ( SQLException e ) { throw new RuntimeException ( e ); }
    }

    private void reportRowSet ( final CachedRowSet cachedRowSet )
    {
        try
        {
            System.out.println ( "*****|  CachedRowSet without Connection  |**************" );
            while ( cachedRowSet.next ( ) )
            {
                String pkey = cachedRowSet.getString ( "pkey_" );  // Using `String` rather than `UUID` as the default implementation of CachedRowSet does not support accessing as UUID object.
                String name = cachedRowSet.getString ( "name_" );

                String message = "pkey = " + pkey + " | name = " + name;
                System.out.println ( message );
            }
            System.out.println ( "**************************************************" );
        }
        catch ( SQLException e ) { throw new RuntimeException ( e ); }
    }
}

When run:

Hello World! 2023-08-29T22:47:30.159431Z
Inserting list of names: [Alice, Bob, Carol, Davis]
Person[id=241a0ba7-6763-4576-8e94-2c39770fe9e5, name=Alice]
Person[id=8a658d90-a834-410c-b94b-60f9629cbc57, name=Bob]
Person[id=b8736f53-3e9c-4cdf-bd1c-67c8c6ae72ab, name=Carol]
Person[id=93c7ca9f-1db1-408f-a0fd-d4b7f2dbe2f4, name=Davis]
*****|  CachedRowSet without Connection  |**************
pkey = 241a0ba7-6763-4576-8e94-2c39770fe9e5 | name = Alice
pkey = 8a658d90-a834-410c-b94b-60f9629cbc57 | name = Bob
pkey = b8736f53-3e9c-4cdf-bd1c-67c8c6ae72ab | name = Carol
pkey = 93c7ca9f-1db1-408f-a0fd-d4b7f2dbe2f4 | name = Davis
**************************************************

record

Or cache the data yourself by writing a record class, populate a collection of those objects from your ResultSet, and hand off the collection.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • I've now changed the implmentation by using `CachedResultSet` class. But getting an exception `java.sql.SQLException: Provider com.sun.rowset.RowSetFactoryImpl` Here's how my updated code looks like: `CachedRowSet cachedRowSet = RowSetProvider.newFactory().createCachedRowSet(); cachedRowSet.populate(resultSet);` – Anubhav kalra Aug 30 '23 at 07:36
  • 1
    @Anubhavkalra You’ve not provided enough detail. I ran my posted code today in Java 20. I suggest you first do the same in a throwaway project. Then compare to your own code. If still stuck, post a new Question here with full details and example code. – Basil Bourque Aug 30 '23 at 07:52
  • I've posted another question related to my problem. Here's the link to that:- https://stackoverflow.com/questions/77006002/java-sql-sqlexception-provider-com-sun-rowset-rowsetfactoryimpl-not-found – Anubhav kalra Aug 30 '23 at 08:01