【问题标题】:Can't loop over ResultSet in Java [duplicate]无法在 Java 中循环 ResultSet [重复]
【发布时间】:2021-06-08 05:35:21
【问题描述】:

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

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

SqlConn 类中,我有以下功能:

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 块关闭所有内容之前,必须在 try 块内的 while 循环到底在哪里。永远不要返回ResultSet
  • 无论是否发生异常,最终的查询都会一直执行。在你的最后一个区块中有rs.close()
  • @luk2302 不完全是,因为他正在制作他的结果对象,但在我的情况下,我可以从多个表中执行查询,所以我不知道我需要制作和返回哪个对象。没有更通用的方法可以做到这一点,所以我保留1个功能?
  • @A.Vreeswijk 删除 rs.close() 会发生什么?

标签: java mysql sql resultset


【解决方案1】:

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

所有这些东西都是 20 多年前的想法。尝试用一些不错的方法覆盖 JDBC 是完全有意义的,但绝对没有必要自己发明这个轮子;使用 JOOQJDBI 为您完成此操作。

如果您坚持使用此代码,请注意以下几点:

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

  2. 所有 3 件事都需要关闭(Connection、Statement 和 ResultSet),但请注意 close() 传播:如果关闭结果集,则只需关闭结果集,但如果关闭语句,也会关闭它的任何结果集产生,如果你关闭一个连接,你也会关闭它产生的任何语句(以及所有结果集),依次。这使得编写这样的方法实际上是不可能的(你如何管理如何关闭事物)?因此,研究 lambdas。无论如何,您都需要 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. 这种“从无开始,然后产生结果集”的模型行不通。数据库有交易;有一个更大的上下文(事务),通常包含多个查询。您需要重新设计此 API 以考虑到这一点:要么将连接对象传递给 executeQuery 方法,要么让您自己的对象表示连接,并且它将具有查询方法。 JOOQ 和 JDBI 也涵盖了这一点。假设非灾难性危险的隔离级别,即使是一系列只读查询也需要事务。

【讨论】:

  • 好的,我已经成功设置了JOOQ,但是你需要帮我清除一些东西......如果我想在一个类中执行一个查询,那么我需要在SqlConn中有一个准备好的函数可以调用的类?如果是这样,这意味着我要输入的每个查询都需要很多功能?我不明白为什么这会容易得多,因为现在,我不能只传递一个执行的 SQL,但我需要使用:Result<Record> result = create.select().from("MarketCoins").fetch();.... 但我只能在SqlConn 类,所以没有空间创建自定义查询?
  • JOOQ 教程涵盖了基本用法。有自定义查询的空间。但是这些总是需要在已经存在的连接的上下文中发生(因为事务)。
  • 好的,但是我的问题中是否有某种函数可以执行查询并返回结果?因为它不知道必须返回哪个对象,或者有更好的方法吗?
  • 按照教程操作即可。不需要这样的“功能”。想想数据库:语句在事务范围内执行。
【解决方案2】:

除了Answer by rzwitserloot中的优点外,我还可以提供一些说明并给出示例代码。

JDBC 驱动程序自动加载

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

在现代 Java 中,没有必要这样做。 JDBC 使用 Java 中的 Service Provider Interfaces (SPI) 工具 (Wikipedia) 自动定位和加载可用的 JDBC 驱动程序。

DataSource

使用DataSource 是获得数据库连接的首选方法。见Tutorial by OracleDataSource 对象包含您的登录信息以及新连接所需的设置。使用DataSource 可以让您在部署时使用externalize this information,而不是在您的代码库中使用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();

删除表

首先,在试验时,您可能想要删除任何现有的表,然后重新构建它。

在此示例中,我们需要一个名为 event_ 的表。

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

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

请注意,我们使用 Statement 而不是 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();
        }
    }

创建表

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

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

插入行

接下来我们在表格中插入一些行。

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

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

此代码将三行添加到表中。

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

转储表

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

现在我们有一个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

循环结果集

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

首先,我们获得了一个ResultSet 作为调用Statement#executeQuery 返回的值。然后我们处理该结果集。结果集一次只能处理一行。因此,“光标”会跟踪当前在哪一行。

处理结果集的关键部分是在您的ResultSet 对象上调用next

next 方法做了三件事:

  • 在第一次使用时将光标移动到第一行。
  • 在连续使用时将光标移至下一行。
  • 如果行已排队等待访问,则返回 true。如果光标已移过结果集中的最后一行,则返回 false

因为该方法返回一个布尔值,我们可以连续循环,直到遇到false

共享数据

当然,在实际应用中,我们所做的不仅仅是将数据库值转储到控制台。我们希望与应用的其他部分共享这些数据。

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

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

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

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

通常最好从不知道调用者的方法返回不可变数据。作为记录,Event 上的每个字段都是只读的(final),immutable 也是浅的。所有三种字段类型(IntegerStringLocalDate)都设计为不可变的,因此每个 Event 都是不可变的。我们获取行的唯一可变部分是它们的容器,ArrayList。因此,我们通过传递给List.copyOf 从该列表中创建一个unmodifiable list。现在我们返回的是完全不可变的数据。

    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 上,作为 DigitalOcean.com 托管的托管数据库服务连接到 MySQL 8,与此 JDBC 驱动程序连接MySQL 通过 Maven POM 依赖:

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

RowSet

从数据库共享数据的另一种方法是通过RowSet 接口和实现。见tutorial by Oracle

虽然我一直不明白为什么,但我很少看到这些被提及。这似乎是JDBC 的“休眠”功能。

RowSet 接口扩展了ResultSet 接口。与ResultSet 不同,RowSet 设计用于在 JDBC 代码之外使用。 RowSet 可以重新连接到数据库以检索新数据或写入更改。

JdbcRowSet 维护与数据库的连接。

无需维护与数据库的连接即可使用CachedRowSet

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-01
    • 1970-01-01
    • 2012-07-12
    • 1970-01-01
    • 2019-02-01
    • 1970-01-01
    • 2012-10-18
    相关资源
    最近更新 更多