返回一个结果集

28

我正在尝试创建一个方法,可以查询我的数据库并检索整个表。

目前,如果我在方法内部使用数据,则它可以正常工作。但是,我希望该方法返回结果。

当前代码会报错:java.sql.SQLException: ResultSet关闭后不允许操作

我该如何实现这一点?

public ResultSet select() {

    con = null;
    st = null;
    rs = null;

    try {
        con = DriverManager.getConnection(url, user, password);
        st = con.createStatement();

        rs = st.executeQuery("SELECT * FROM biler");
        /*
        if (rs.next()) {
            System.out.println(rs.getString("model"));
        }*/

    } catch (SQLException ex) {
        Logger lgr = Logger.getLogger(MySQL.class.getName());
        lgr.log(Level.SEVERE, ex.getMessage(), ex);

    } finally {
        try {
            if (rs != null) {
                rs.close();
            }
            if (st != null) {
                st.close();
            }
            if (con != null) {
                con.close();
            }

        } catch (SQLException ex) {
            Logger lgr = Logger.getLogger(MySQL.class.getName());
            lgr.log(Level.WARNING, ex.getMessage(), ex);
        }
    }

    return rs;
}

4
为什么你无法理解明显的错误信息 - "java.sql.SQLException: Operation not allowed after ResultSet closed"? - Lion
8个回答

63

永远不要通过公共方法传递ResultSet。这很容易导致资源泄漏,因为你必须保持语句和连接的打开状态。关闭它们会隐式地关闭结果集。但是如果保持它们的打开状态,将会导致它们悬空并在打开太多时使数据库耗尽资源。

可以将其映射到Javabean集合中并返回:

public List<Biler> list() throws SQLException {
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;
    List<Biler> bilers = new ArrayList<Biler>();

    try {
        connection = database.getConnection();
        statement = connection.prepareStatement("SELECT id, name, value FROM Biler");
        resultSet = statement.executeQuery();

        while (resultSet.next()) {
            Biler biler = new Biler();
            biler.setId(resultSet.getLong("id"));
            biler.setName(resultSet.getString("name"));
            biler.setValue(resultSet.getInt("value"));
            bilers.add(biler);
        }
    } finally {
        if (resultSet != null) try { resultSet.close(); } catch (SQLException ignore) {}
        if (statement != null) try { statement.close(); } catch (SQLException ignore) {}
        if (connection != null) try { connection.close(); } catch (SQLException ignore) {}
    }

    return bilers;
}

或者,如果您已经使用Java 7,请直接使用try-with-resources语句自动关闭这些资源:

public List<Biler> list() throws SQLException {
    List<Biler> bilers = new ArrayList<Biler>();

    try (
        Connection connection = database.getConnection();
        PreparedStatement statement = connection.prepareStatement("SELECT id, name, value FROM Biler");
        ResultSet resultSet = statement.executeQuery();
    ) {
        while (resultSet.next()) {
            Biler biler = new Biler();
            biler.setId(resultSet.getLong("id"));
            biler.setName(resultSet.getString("name"));
            biler.setValue(resultSet.getInt("value"));
            bilers.add(biler);
        }
    }

    return bilers;
}

顺便提一下,你不应该将ConnectionStatementResultSet声明为实例变量(线程安全的主要问题!),也不应该在那个时候抑制SQLException(调用者将不知道发生了什么问题),也不应该在同一个try块中关闭资源(如果例如结果集关闭抛出异常,则语句和连接仍然打开)。所有这些问题都在上面的代码片段中得到解决。


感谢您花时间撰写详细的答案。 使用这段代码,我需要为每个表格都创建一个方法。这真的是最好的做法吗? - Patrick Reck
2
是的,当您坚持使用低级别的JDBC时,可以。但是,您可以像Hibernate在十年前所做的那样,将重复的样板代码进行重构到相当高的程度。不,我个人认为JPA是最好的方式。然后只需要一个 return em.createQuery("SELECT b FROM Biler b", Biler.class).getResultList(); 即可。 - BalusC
1
请问您能否解释一下为什么您称实例变量“conn”存在“主要线程安全问题”?如果它不是静态的,为什么会存在并发问题?我看不出来,因为“conn”仅在实例内部可用,而不是类内部。 - user1156544

16

如果您在检索时间不知道ResultSet想要什么,我建议将整个内容映射到一个类似这样的地图中:

    List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>();
    Map<String, Object> row = null;

    ResultSetMetaData metaData = rs.getMetaData();
    Integer columnCount = metaData.getColumnCount();

    while (rs.next()) {
        row = new HashMap<String, Object>();
        for (int i = 1; i <= columnCount; i++) {
            row.put(metaData.getColumnName(i), rs.getObject(i));
        }
        resultList.add(row);
    }

基本上您拥有的东西与ResultSet相同(没有ResultSetMetaData)。


2
这种方法是否会占用更多的内存,因为我们创建了一个单独的 hashMap 而不是只有 resultSet 并且没有关闭它? - ramu
你可以在 while 循环后关闭语句和连接,这样你就只有 resultlist 对象了,这意味着不会有问题。 - mcvkr

8

好的,你需要在finally块中调用rs.close()方法。

这基本上是个好主意,因为你应该关闭所有的资源(连接、语句、结果集等)。

但是你必须在使用完它们后关闭它们。

至少有三种可能的解决方案:

  1. don't close the resultset (and connection, ...) and require the caller to call a separate "close" method.

    This basically means that now the caller needs to remember to call close and doesn't really make things easier.

  2. let the caller pass in a class that gets passed the resultset and call that within your method

    This works, but can become slightly verbose, as you'll need a subclass of some interface (possibly as an anonymous inner class) for each block of code you want to execute on the resultset.

    The interface looked like this:

    public interface ResultSetConsumer<T> {
      public T consume(ResultSet rs);
    }
    

    and your select method looked like this:

    public <T> List<T> select(String query, ResultSetConsumer<T> consumer) {
      Connection con = null;
      Statement st = null;
      ResultSet rs = null;
    
        try {
          con = DriverManager.getConnection(url, user, password);
          st = con.createStatement();
    
          rs = st.executeQuery(query);
          List<T> result = new ArrayList<T>();
          while (rs.next()) {
              result.add(consumer.consume(rs));
          }
        } catch (SQLException ex) {
          // logging
        } finally {
          try {
            if (rs != null) {
                rs.close();
            }
            if (st != null) {
                st.close();
            }
            if (con != null) {
                con.close();
            }
          } catch (SQLException ex) {
            Logger lgr = Logger.getLogger(MySQL.class.getName());
            lgr.log(Level.WARNING, ex.getMessage(), ex);
          }
        }
      return rs;
    }
    
  3. do all the work inside the select method and return some List as a result.

    This is probably the most widely used one: iterate over the resultset and convert the data into custom data in your own DTOs and return those.


我本来想用第三个选项,但是我无法弄清楚如何创建一个包含所有信息的列表,而不管选择了哪个表。 - Patrick Reck
@PatrickReck:为什么您想要这样做,而不考虑所选择的表?不同的表包含不同的数据类型。 - Joachim Sauer
1
没错。我想要的是一个单一的方法,我可以通过它传递一个 SQL 调用,并返回从调用中检索到的数据。如果我要选择整个 Car 表(id、model),我希望这两个被返回。如果是 Customer 表(id、name、address、phone),我希望所有这些数据都使用同一个方法被返回。 - Patrick Reck
@PatrickReck:我强烈反对这样做,但如果你愿意的话,你可以返回一个 List<Map<String,Object>>(显然,每个结果的 map 包含从字段名到值的映射)。 - Joachim Sauer
1
@PatrickReck:另外,请查看我的更新帖子,以获取选项#2的示例代码,也许会有所帮助。 - Joachim Sauer

6
正如之前的每个人所说,将结果集传递是一个不好的主意。如果您正在使用连接池库,例如c3p0,那么您可以安全地使用CachedRowSet及其实现CachedRowSetImpl。使用此功能,您可以关闭连接。它只在必要时使用连接。以下是Java文档中的片段:

CachedRowSet对象是一个断开连接的行集,这意味着它仅在短时间内使用与其数据源的连接。它在读取数据以填充自身行时连接到其数据源,然后在将更改传播回其基础数据源时再次连接到其数据源。其余时间,CachedRowSet对象处于断开连接状态,包括在修改其数据时。断开连接使RowSet对象更加简洁,因此更容易传递给另一个组件。例如,断开连接的RowSet对象可以被序列化并通过网络传输到轻量级客户端,例如个人数字助理(PDA)。

下面是用于查询和返回ResultSet的代码片段:
public ResultSet getContent(String queryStr) {
    Connection conn = null;
    Statement stmt = null;
    ResultSet resultSet = null;
    CachedRowSetImpl crs = null;
    try {
        Connection conn = dataSource.getConnection();
        stmt = conn.createStatement();
        resultSet = stmt.executeQuery(queryStr);

        crs = new CachedRowSetImpl();
        crs.populate(resultSet);
    } catch (SQLException e) {
        throw new IllegalStateException("Unable to execute query: " + queryStr, e);
    }finally {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            LOGGER.error("Ignored", e);
        }
    }

    return crs;
}

以下是使用c3p0创建数据源的代码片段:

 ComboPooledDataSource cpds = new ComboPooledDataSource();
            try {
                cpds.setDriverClass("<driver class>"); //loads the jdbc driver
            } catch (PropertyVetoException e) {
                e.printStackTrace();
                return;
            }
            cpds.setJdbcUrl("jdbc:<url>");
            cpds.setMinPoolSize(5);
            cpds.setAcquireIncrement(5);
            cpds.setMaxPoolSize(20);

 javax.sql.DataSource dataSource = cpds;

1
这里是创建CachedRowSet的正确方法:https://dev59.com/pXE95IYBdhLWcg3wn_Vr - Vadzim
1个可能的困境是,像SonarQube这样的静态代码分析工具只会触发任何实现“AutoCloseable”的内容,很可能会将“CachedRowSet”创建(工厂或直接)标记为在“try-with-resources” / “try-finally”之外(在这种用例中有点傻)。 - galaxis

4
您可以使用专门用于您需要的CachedRowSet对象:
public CachedRowSetImpl select(String url, String user, String password) {

    CachedRowSetImpl crs = null;

    try (Connection con = DriverManager.getConnection(url, user, password);
         Statement st = con.createStatement();
         ResultSet rs = st.executeQuery("SELECT * FROM biler");) {

        crs = new CachedRowSetImpl();
        crs.populate(rs);

    } catch (SQLException ex) {
        Logger lgr = Logger.getLogger(MySQL.class.getName());
        lgr.log(Level.SEVERE, ex.getMessage(), ex);


    } catch (SQLException ex) {
        Logger lgr = Logger.getLogger(MySQL.class.getName());
        lgr.log(Level.WARNING, ex.getMessage(), ex);
    }

    return crs;
}

您可以在此处阅读文档: https://docs.oracle.com/javase/7/docs/api/javax/sql/rowset/CachedRowSet.html

1
这里是创建 CachedRowSet 的正确方法:https://dev59.com/pXE95IYBdhLWcg3wn_Vr - Vadzim

1
你正在关闭ResultSet,因此不能再使用它了。
为了返回表的内容,你需要遍历ResultSet并构建每行的表示形式(可能是一个List?)。假设每行代表某个实体,我会为每行创建这样的实体。
while (rs.next()) {
   list.add(new Entity(rs));
}
return list;

另一种方法是提供一些回调对象,您的 ResultSet 迭代将为每个 ResultSet 行调用该对象。这样,您就不需要构建代表整个表的对象(如果它很大可能会成为问题)。
   while (rs.next()) {
      client.processResultSet(rs);
   }

我不太希望客户端关闭结果集/语句/连接。为了避免资源泄漏,这些需要仔细管理,最好在一个地方处理(最好靠近打开它们的地方!)。
注意:您可以使用Apache Commons DbUtils.closeQuietly()来简单可靠地关闭连接/语句/结果集元组(正确处理null和异常)。

0

将结果集返回是不好的实践,其次您已经关闭了它,所以在关闭后您不能再使用它。 我建议您使用Java 7,并在try块中使用多个资源,如上所建议。 如果您想要整个表的结果,则应返回其输出而不是resultSet。


0

假设您可以负担将整个结果存储在内存中,那么您可以简单地返回一些类似表格的结构。例如,使用Tablesaw,只需执行以下操作:

Table t = Table.read().db(rows);

使用 rows 作为标准的 java.sql.ResultSet。详情请参见此处。如果您打算进一步切分数据,则 Tablesaw 将变得非常有用,因为它为您提供了类似于 Pandas 的功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接