无法在 Java 中循环 ResultSet

Can't loop over ResultSet in Java

我有问题。我在我的 java 项目中使用以下 MySQL 驱动程序:

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

SqlConnclass中,我有如下函数:

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

函数是这样使用的:

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

但是当我想像这样循环它时:

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

我收到以下错误:

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)

我做错了什么,我该如何解决?

在您粘贴的代码中,您首先创建一个连接,然后无法关闭(资源泄漏!),然后创建一个语句和一个结果集。这些,你 关闭,总是(通过 finally 块),在你 return 之前。因此,此方法创建了一个 ResultSet,它立即关闭并 returns 这个现在关闭的结果集。

所有这些都是 20 多岁的想法。尝试用一些不错的方法来掩盖 JDBC 是完全有意义的,但是绝对没有必要自己发明这个轮子;使用为您完成此操作的 JOOQ or JDBI

如果您坚持使用此代码,请注意此代码:

  1. 在管理资源时使用 try-with-resources,不要使用 finally 块。

  2. 所有 3 个东西都需要关闭(Connection、Statement 和 ResultSet),但请注意 close() 传播:如果关闭结果集,则只是关闭结果集,但如果关闭语句,您还关闭了它生成的所有结果集,如果关闭连接,您也关闭了它生成的所有语句(以及所有结果集),依次。这使得实际上不可能编写这样的方法(您如何管理如何关闭事物)?因此,研究 lambda。无论如何你都需要 lambdas 来重试。

  3. 声明几乎完全没有用。一旦你有参数化查询(即 SELECT * FROM users WHERE username = ... username the user just entered into a web form here ... 你不能使用任何这个:你不能在你连接用户名的地方创建一个字符串,因为如果在表单上输入的用户名是 whatever' OR 1 == 1; DROP TABLE users CASCADE; EXEC 'format C: /y';? 不,唯一的方法是 PreparedStatement,它支持参数化,不会立即让您面临 SQL 注入攻击。

  4. 说真的,JOOQ 或 JDBI 做到了这一切,而且做得更好。

  5. 你不需要 Class.forName("com.mysql.cj.jdbc.Driver"); - 20 年都不需要了。

  6. 'start with nothing, and out comes a resultset'这个模型不行。数据库有交易;有一个更大的上下文(事务),通常包含多个查询。您需要重新设计此 API 以考虑到这一点:将连接对象传递给 executeQuery 方法,或者创建您自己的对象来表示连接,并且它会具有查询方法。 JOOQ 和 JDBI 也涵盖了这一点。假设非灾难性的危险隔离级别,即使是一系列只读查询也需要事务。

除了中的优点外,我可以提供更多说明并给出示例代码。

JDBC 驱动程序自动加载

使用 Class.forName 加载 JDBC driver 是老派,从 Java 的最早版本开始。

在现代Java,没有这个必要。 Service Provider Interfaces (SPI) facility in Java (Wikipedia) 被 JDBC 用来自动定位和加载可用的 JDBC 驱动程序。

DataSource

在您的代码库中使用 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 此信息。

您的 JDBC 驱动程序可能为直接连接提供了 DataSource 的实现。您还可以使用其他实现,例如用于连接池的实现。

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

实例化一个 DataSource 对象,并保留它。根据需要将其传递给您的各种数据库方法。

DataSource dataSource = this.configureDataSource();

掉落table

首先,在试验时,您可能希望删除任何现有的 table 然后重建它。

在此示例中,我们期望一个名为 event_ 的 table。

请注意我们如何使用 try-with-resources 语法,如其他答案中所建议的那样。当控制流离开 try 块时,括号内列出的资源将自动关闭。无论是成功离开块还是因为抛出的异常(或错误)而离开,都会发生这种关闭。使用 try-with-resources 可以让您的代码更整洁、更易于理解,样板文件更少。

对于 SQL 字符串,我们使用 Java 15 中新增的 text blocks 功能。三个引号字符标记每个文本块。非常方便格式化 SQL、XML、JSON 等

请注意,我们使用 Statement rather than PreparedStatement,因为我们的 SQL 没有不受信任的文本。如果在 SQL 中使用来自用户或其他不受信任来源的文本,请始终使用 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();
        }
    }

创建table

让我们创建用于此示例的 table。

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

注意我们如何使用两个 try-with-resources,一个嵌套在另一个中。如果我们将 SQL 字符串创建代码移到更高的位置,我们可以声明和实例化 connstmt 资源是单个 try-with-resources.

除此之外,这段代码与上面的代码非常相似。我们信任 SQL 文本,所以我们使用 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();
        }
    }

插入行

接下来我们在 table.

中插入一些行

在真实的应用程序中,我们可能会使用用户输入的数据或从其他不受信任的来源获得的数据。所以这里我们使用PreparedStatement。我们碰巧使用了硬编码字符串,例如 "Dog Show",但这些值可能是真实应用程序中的变量。

java.time 类型在 JDBC 4.2 及更高版本中受支持。但是,没有添加 setLocalDate 等类型特定的方法(莫名其妙)。所以我们使用setObject/getObject.

此代码将三行添加到 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();
        }
    }

转储table

为了验证我们的行是否已成功插入,让我们将 table 的内容转储到控制台。

现在我们有一个ResultSet要检查。

问题是您的问题代码是您试图访问 JDBC 代码之外的 ResultSet。但是 JDBC 代码需要关闭它的资源。这些资源中有任何ResultSet。您需要在 您的 JDBC 代码块中访问您的 ResultSet 对象 。这里我们简单地从结果集中的每一行中获取值,然后将该值写入 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();
        }
    }

当运行时,我们得到如下。

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

循环结果集

你问题的主要部分是询问如何循环结果集。上面的代码就是这样做的。

首先,我们通过调用Statement#executeQuery获得了ResultSet作为值return。然后我们处理那个结果集。结果集一次只能处理一行。因此,“游标”会跟踪当前手头的行。

处理结果集的关键部分是调用next on your ResultSet对象。

next 方法做了三件事:

  • 第一次使用时将光标移动到第一行。
  • 在连续使用时将光标移动到下一行。
  • Returns true 如果一行已排队,准备好访问。 Returns false 如果光标移过结果集中的最后一行。

因为方法 return 是一个布尔值,我们可以依次循环直到遇到 false.

共享数据

当然,在真正的应用程序中,我们做的不仅仅是将数据库值转储到控制台。我们想与应用程序的其他部分共享该数据。

如上文和另一个答案中所述,您不能通过共享 ResultSet 对象本身来与应用程序的其他部分共享数据。相反,您必须将数据从 ResultSet 复制到其他对象中。正如另一个答案所建议的那样,您可以选择框架来协助完成这项工作。或者,您可以自己复制数据。

在 Java 16 中,透明且不变地共享此类数据的明显方法是使用新的 records 功能。记录是声明 class 的一种简短方式。您只需在一对括号内声明每个成员字段的类型和名称。编译器隐式创建构造函数、getter、equals & hashCodetoString.

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

下面的代码与dumpTable方法类似。此代码将从数据库中获取的数据传递给 Event 记录的构造函数以实例化一个 Event 对象。每个 Event 对象都被添加到一个集合中。集合 returned 到调用方法。

一般来说,最好 return immutable 来自不知道其调用者的方法的数据。作为记录,Event 上的每个字段都是只读的 (final),浅层 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 也是如此。现在我们 return 完全免疫 table 数据。

    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`. 
    }

结果是 Event 个对象的列表。调用方法可以对这些对象做任何它想做的事情。

运行 例子

您可以 运行 这些方法如下:

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

当运行:

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]]

技术细节

上面的代码是 运行 来自 IntelliJ 2021.1 beta,运行在 Java 16 早期访问(候选发布)上,在 macOS Mojave 上,连接到 MySQL 8 作为由 DigitalOcean.com 托管的托管数据库服务,通过 Maven POM 依赖关系与此 JDBC 驱动程序连接 MySQL:

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

RowSet

从数据库共享数据的另一种方法是通过 RowSet interfaces and implementations. See tutorial by Oracle

虽然我一直不明白为什么,但我没有看到这些被提及太多。这似乎是 JDBC.

的“卧铺”功能

RowSet interface extends the ResultSet界面。与 ResultSet 不同,RowSet 旨在用于 JDBC 代码之外。 RowSet 可以重新连接到数据库以检索新数据或写入更改。

A JdbcRowSet 维护与数据库的连接。

A CachedRowSet 可以在不保持与数据库的连接的情况下使用。