2

I have a problem. I am using the following MySQL driver in my java project:

// SET THE MYSQL DRIVER
Class.forName("com.mysql.cj.jdbc.Driver");
SqlConn sqlConn = new SqlConn();

In the SqlConn class, I have the following function:

public ResultSet executeQuery(String query) {
    Statement stmt = null;
    ResultSet rs = null;
    try {
        stmt = conn.createStatement();

        if (stmt.execute(query)) {
            rs = stmt.getResultSet();
        }

        // Now do something with the ResultSet ....
    } catch (SQLException ex) {
        System.out.println("SQLException: " + ex.getMessage());
        System.out.println("SQLState: " + ex.getSQLState());
        System.out.println("VendorError: " + ex.getErrorCode());
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException ignored) {
            }
        }

        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException sqlEx) {
            } // ignore

            stmt = null;
        }
    }

    return rs;
}

The function is used like this:

ResultSet result = sqlConn.executeQuery("SELECT Market, Coin FROM Wallets GROUP BY Market, Coin ORDER BY Market, Coin;");

But then when I want to loop over it like this:

while (result.next()) {
    System.out.println(result.getString("Market"));
    System.out.println(result.getString("Coin"));
    System.out.println();
}

I get the following error:

Exception in thread "main" java.sql.SQLException: Operation not allowed after ResultSet closed
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
    at com.mysql.cj.jdbc.result.ResultSetImpl.checkClosed(ResultSetImpl.java:464)
    at com.mysql.cj.jdbc.result.ResultSetImpl.next(ResultSetImpl.java:1744)
    at com.company.drivers.SimulatorDriver.main(SimulatorDriver.java:82)

What am I doing wrong and how can I fix this?

Reporter
  • 3,897
  • 5
  • 33
  • 47
A. Vreeswijk
  • 822
  • 1
  • 19
  • 57
  • Where are you looping over it? The place where's the comment? – Dropout Mar 10 '21 at 09:42
  • Where exactly is that `while` loop, that has to be inside the try block, before the `finally` block closes everything. Never return a `ResultSet`. – luk2302 Mar 10 '21 at 09:42
  • 1
    The final query will always executed, no matter if an exception occurs or not. In your final block there is `rs.close()`. – Reporter Mar 10 '21 at 09:42
  • @luk2302 not quite, because he is making an object of his result, but in my case I can execute query's from multiple tables, so I don't know which object I need to make and return. Isn't there a more generic way to do this, so I keep 1 function? – A. Vreeswijk Mar 10 '21 at 09:51
  • @A.Vreeswijk What will happen when you remove `rs.close()`? – Reporter Mar 10 '21 at 10:52

2 Answers2

4

In the code you pasted, you first create a connection which you then fail to close (Resource leak!), then you make a statement and a resultset. These, you do close, always (via a finally block), before you return. Thus, this method creates a ResultSet which it immediately closes and returns this now closed resultset.

All of this stuff is 20+ year old thinking. It makes total sense to try to paper over JDBC with some nice methods, but there is absolutely no need to invent this wheel yourself; use JOOQ or JDBI which did this for you.

Some notes on this code if you insist on using this:

  1. Use try-with-resources whenever managing resources, don't use finally blocks.

  2. All 3 things need closing (Connection, Statement, and ResultSet), but note that close() propagates: If you close a resultset, you just close the resultset, but if you close a statement, you also close any resultsets it spawned, and if you close a connection, you also close any statements (And thus all resultsets) it spawned, in turn. That makes writing such a method effectively impossible (how do you manage how to close things)? So, investigate lambdas. You want lambdas for retry anyway.

  3. Statement is almost entirely useless. The moment you have parameterized queries (i.e. SELECT * FROM users WHERE username = ... username the user just entered into a web form here ... you can't use any of this: You can't make a string where you concatenate the username in, because what if the username, as typed on the form, is whatever' OR 1 == 1; DROP TABLE users CASCADE; EXEC 'format C: /y';? No, the only way to go is PreparedStatement which has support for parameterization that doesn't immediately open you up to SQL injection attacks.

  4. Seriously, JOOQ or JDBI did all this, and far better.

  5. You don't need Class.forName("com.mysql.cj.jdbc.Driver"); - haven't needed it for 20 years.

  6. This model of 'start with nothing, and out comes a resultset' does not work. DBs have transactions; there is a greater context (a transaction) which usually encompasses multiple queries. You need to redesign this API to take that into account: Either pass in a connection object to the executeQuery method, or make your own object representing a connection, and it would have query methods. JOOQ and JDBI covered this, too. Assuming non-disastrously-dangerous isolationlevels, even a series of read-only queries needs transactions.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Okay, I have succefully setup JOOQ, but you need to help me clear something.... If I want to execute a query in a class, then I need to have a prepared function in the SqlConn class that can be called? If so, that means that I need a lot of functions for every query I want to enter? I don't get why this is so much easier, because now, I can't just pass a SQL that gets executed, but I need to work with: `Result result = create.select().from("MarketCoins").fetch();`.... But I can only use these lines inside the SqlConn class, so there is no room for creating custom query's? – A. Vreeswijk Mar 10 '21 at 10:18
  • The JOOQ tutorial covers basic usage. There is room for custom queries. But these always need to occur within the context of an already existing connection (because transactions). – rzwitserloot Mar 10 '21 at 10:43
  • Okay, but is it possible to some kind of function I had in my question that executes queries and returns the result? Because it has no idea which object it must return, or is there a better way to do it? – A. Vreeswijk Mar 10 '21 at 10:53
  • 1
    Just follow the tutorial. There is no need for such a 'function'. Think database: Statements are executed within the confines of a transaction. – rzwitserloot Mar 10 '21 at 11:08
0

Besides the excellent points in the Answer by rzwitserloot, I can provide a few more instructions and give example code.

JDBC driver loads automatically

Using Class.forName to load a JDBC driver is old-school, from the earliest versions of Java.

In modern Java, there is no need for that. The Service Provider Interfaces (SPI) facility in Java (Wikipedia) is used by JDBC to automatically locate and load available JDBC drivers.

DataSource

Use of a DataSource is the preferred means of getting a connection to a database. See Tutorial by Oracle. The DataSource object contains your logon info, and the settings you want for your new connection. Using a DataSource enables you to externalize this information at deployment, rather than hard-coding this info within your codebase.

Your JDBC driver likely provides an implementation of DataSource for direct connections. You may also use other implementations, such as those for connection-pooling.

    private DataSource configureDataSource ( )
    {
        System.out.println( "INFO - `configureDataSource` method. " + Instant.now() );

        com.mysql.cj.jdbc.MysqlDataSource dataSource = Objects.requireNonNull( new com.mysql.cj.jdbc.MysqlDataSource() );  // Implementation of `DataSource` for this specific database engine.
        dataSource.setServerName( "your_server_address" );
        dataSource.setPortNumber( some_port_number );
        dataSource.setDatabaseName( "your_database_name" );
        dataSource.setUser( "scott" );
        dataSource.setPassword( "tiger" );
        return dataSource;
    }

Instantiate a DataSource object, and keep it around. Pass it to your various database methods as needed.

DataSource dataSource = this.configureDataSource();

Drop table

First, while experimenting, you may want to drop any existing table and then rebuild it.

Here in this example we expect a table named event_.

Notice how we use the try-with-resources syntax, as suggested in the other Answer. The resources listed within the parentheses are automatically closed when flow-of-control leaves the try block. This closing happens whether leaving the block successfully as well as when leaving because of a thrown exception (or error). Using try-with-resources makes your code neater and easier to follow, with less boilerplate.

For the SQL strings, we use the text blocks feature new in Java 15. The triple QUOTATION MARK characters mark each block of text. Very handy for formatting snippets of SQL, XML, JSON, etc.

Notice that we use Statement rather than PreparedStatement because our SQL has no untrusted text. If using text within your SQL that came from users or other untrusted sources, always use PreparedStatement.

    private void dropTable ( DataSource dataSource )
    {
        System.out.println( "INFO - `dropTable` method. " + Instant.now() );
        try ( Connection conn = dataSource.getConnection() )
        {
            String sql = """
                         DROP TABLE IF EXISTS event_
                         ;
                         """;
            System.out.println( "sql:  \n" + sql );
            try ( Statement stmt = conn.createStatement() )
            {
                stmt.execute( sql );
            }
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }

Create table

Let's create the table used for this example.

    private void createTable ( DataSource dataSource )
    {
        System.out.println( "INFO - `createTable` method. " + Instant.now() );
        try ( Connection conn = dataSource.getConnection() )
        {
            String sql = """
                         CREATE TABLE IF NOT EXISTS event_
                            ( 
                               id_ INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,  -- ⬅ `identity` = auto-incrementing integer number.
                               title_ VARCHAR ( 30 ) NOT NULL ,
                               date_ DATE NOT NULL 
                             )
                         ;
                         """;
            System.out.println( "sql:  \n" + sql );
            try ( Statement stmt = conn.createStatement() ; )
            {
                stmt.execute( sql );
            }
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }

Notice how we use two try-with-resources, one nested within the other. If we moved that SQL string creation code higher up, we could declare and instantiate both the conn and stmt resources is a single try-with-resources.

Otherwise, this code is quite similar to code above. We trust the SQL text, so we use Statement.

    private void createTable ( DataSource dataSource )
    {
        System.out.println( "INFO - `createTable` method. " + Instant.now() );

        String sql = """
                     CREATE TABLE IF NOT EXISTS event_
                        ( 
                           id_ INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,  -- ⬅ `identity` = auto-incrementing integer number.
                           title_ VARCHAR ( 30 ) NOT NULL ,
                           date_ DATE NOT NULL 
                         )
                     ;
                     """;
        System.out.println( "sql:  \n" + sql );
        try (
                Connection conn = dataSource.getConnection() ;
                Statement stmt = conn.createStatement() ;
        )
        {
            stmt.execute( sql );
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }

Insert rows

Next we insert some rows into our table.

In a real app, we are likely using data entered by users or obtained from another untrusted source. So here we use PreparedStatement. We happen to be using hard-coded strings such as "Dog Show", but these values would likely be variables in a real app.

The java.time types are supported in JDBC 4.2 and later. However, no type-specific methods such as setLocalDate were added (inexplicably). So we use setObject/getObject.

This code adds three rows to the table.

    private void insertRows ( DataSource dataSource )
    {
        System.out.println( "INFO - `insertRows` method. " + Instant.now() );
        String sql = """
                     INSERT INTO event_ ( title_ , date_ )
                     VALUES ( ? , ? )
                     ;
                     """;
        try (
                Connection conn = dataSource.getConnection() ;
                PreparedStatement pstmt = conn.prepareStatement( sql ) ;
        )
        {
            pstmt.setString( 1 , "Dog Show" );
            pstmt.setObject( 2 , LocalDate.of( 2022 , Month.JANUARY , 21 ) );
            pstmt.executeUpdate();

            pstmt.setString( 1 , "Cat Show" );
            pstmt.setObject( 2 , LocalDate.of( 2022 , Month.FEBRUARY , 22 ) );
            pstmt.executeUpdate();

            pstmt.setString( 1 , "Bird Show" );
            pstmt.setObject( 2 , LocalDate.of( 2022 , Month.MARCH , 23 ) );
            pstmt.executeUpdate();
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }

Dump table

To verify our rows were successfully inserted, let's dump the contents of this table to the console.

Now we have a ResultSet to examine.

The problem is your Question's code is that you were trying to access a ResultSet outside of the JDBC code. But the JDBC code needs to close its resources. Among these resources is any ResultSet. You need to access your ResultSet object within your JDBC code block. Here we simply grab the values from each row of the result set, then write that value to System.out.

    private void dumpTable ( DataSource dataSource )
    {
        System.out.println( "INFO - `dumpTable` method. " + Instant.now() );

        String sql = "SELECT * FROM event_ ;";
        try (
                Connection conn = dataSource.getConnection() ;
                Statement stmt = conn.createStatement() ;
                ResultSet rs = stmt.executeQuery( sql ) ;
        )
        {
            System.out.println( "-------|  event_ table  |--------------------" );
            while ( rs.next() )
            {
                //Retrieve by column name
                int id = rs.getInt( "id_" );
                String title = rs.getString( "title_" );
                LocalDate date = rs.getObject( "date_" , LocalDate.class );

                System.out.println( "id_=" + id + " | title_=" + title + " | date_=" + date );
            }
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }

When run, we get the following.

INFO - `dumpTable` method. 2021-03-11T02:31:22.769442Z
-------|  event_ table  |--------------------
id_=1 | title_=Dog Show | date_=2022-01-21
id_=2 | title_=Cat Show | date_=2022-02-22
id_=3 | title_=Bird Show | date_=2022-03-23

Looping the result set

The main part of your Question is asking how to loop a result set. The code above does just that.

First, we obtained a ResultSet as the value returned by call to Statement#executeQuery. Then we process that result set. A result set can only deal with one row at a time. So a “cursor” keeps track of which row is currently in hand.

The key part to processing the result set is calling next on your ResultSet object.

The next method does three things:

  • Moves the cursor to the first row, on first usage.
  • Moves the cursor to the next row, on successive usages.
  • Returns true if a row is queued, ready for access. Returns false if the cursor has moved past the last row in the result set.

Because the method returns a boolean, we can loop successively until encountering a false.

Sharing data

Of course, in a real app we do more than dump database values to the console. We want to share that data with other parts of the app.

As mentioned above and in the other Answer, you cannot share data with other parts of the app by sharing the ResultSet object itself. Instead you must copy the data out of the ResultSet and into other objects. As the other Answer suggests, you have a choice of frameworks to assist with this chore. Or, you may copy the data yourself.

In Java 16, the obvious way to transparently and immutably share such data is to the use the new records feature. A record is a brief way to declare a class. You simply declare the type and name of each member field within a pair of parentheses. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString.

public record Event(Integer id , String title , LocalDate happening) {}

The following code is similar to the dumpTable method. This code passes the data fetched from database to the constructor of Event record to instantiate a Event object. Each Event object is added to a collection. The collection is returned to the calling method.

Generally it is best to return immutable data from a method that is not aware of its callers. As a record, each field on the Event is read-only (final), so is shallowly immutable. All three field types (Integer, String, and LocalDate) are designed to be immutable, so each Event is deeply immutable. The only mutable part of our fetch rows is their container, an ArrayList. So we make an unmodifiable list from that list by passing to List.copyOf. Now we are returning entirely immutable data.

    private List < Event > fetchEvents ( DataSource dataSource )
    {
        System.out.println( "INFO - `fetchEvents` method. " + Instant.now() );

        List < Event > events = new ArrayList <>();
        String sql = "SELECT * FROM event_ ;";
        try (
                Connection conn = dataSource.getConnection() ;
                Statement stmt = conn.createStatement() ;
                ResultSet rs = stmt.executeQuery( sql ) ;
        )
        {
            while ( rs.next() )
            {
                //Retrieve by column name
                int id = rs.getInt( "id_" );
                String title = rs.getString( "title_" );
                LocalDate date = rs.getObject( "date_" , LocalDate.class );
                Event event = new Event( id , title , date );  // Instantiate a record, an `Event` object. 
                events.add( event );

                System.out.println( event );
            }
        }
        catch ( SQLException e )
        {
            e.printStackTrace();
        }
        return List.copyOf( events );  // Return an unmodifiable list produced by `List.copyOf`. 
    }

The result is a list of Event objects. The calling method can do whatever it wants with those objects.

Run example

You can run those methods like this:

        DataSource dataSource = this.configureDataSource();
        this.dropTable( dataSource );
        this.createTable( dataSource );
        this.insertRows( dataSource );
        this.dumpTable( dataSource );
        List < Event > events = this.fetchEvents(dataSource);
        System.out.println( "events = " + events );

When run:

events = [Event[id=1, title=Dog Show, happening=2022-01-21], Event[id=2, title=Cat Show, happening=2022-02-22], Event[id=3, title=Bird Show, happening=2022-03-23]]

Technical details

The code above was run from within IntelliJ 2021.1 beta, running on Java 16 early access (release candidate), on macOS Mojave, connecting to MySQL 8 as a managed database service hosted by DigitalOcean.com, connecting with this JDBC driver for MySQL via Maven POM dependency:

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>

RowSet

Another way to share data from your database is through the RowSet interfaces and implementations. See tutorial by Oracle.

I do not see these mentioned much, though I have never understood why. The seem to be quite the “sleeper” feature of JDBC.

The RowSet interface extends the ResultSet interface. A RowSet is designed to be used outside the JDBC code, unlike ResultSet. A RowSet can reconnect to the database to retrieve fresh data or to write changes.

A JdbcRowSet maintains a connection to the database.

A CachedRowSet can be used without maintaining a connection to the database.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154