查询结果流提前关闭 - Spring Data JPA和Hibernate

16

这里是一个包含本问题代码并展示了错误的存储库:https://github.com/agsimeonov/stream-bug

我一直在尝试使用以下代码片段(data.txt是一个每行都有一个数字的3000行文件)通过Spring Data JPA和Hibernate来流式查询结果:

try (Stream<Customer> stream = repository.streamAll()) {
  stream.forEach(customer -> {
    try {
      File data = new File(getClass().getClassLoader().getResource("data.txt").getFile());
      try (BufferedReader reader = new BufferedReader(new FileReader(data))) {
        while (reader.readLine() != null) {
          // Do stuff for the current customer
        }
      }
    } catch (IOException e) {}
    System.out.println(customer);
  });
}

这里是领域对象:

@Entity
@Table(name = "customer")
public class Customer {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String firstName;
  private String lastName;

  public Customer() {}

  public Customer(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
  }
}

这里是代码库:

public interface CustomerRepository extends JpaRepository<Customer, Long> {
  @Query("SELECT c FROM Customer c")
  Stream<Customer> streamAll();
}

这样做会导致以下错误:

org.hibernate.exception.GenericJDBCException: could not advance using next()
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:47)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:109)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:95)
    at org.hibernate.internal.ScrollableResultsImpl.convert(ScrollableResultsImpl.java:69)
    at org.hibernate.internal.ScrollableResultsImpl.next(ScrollableResultsImpl.java:104)
    at org.springframework.data.jpa.provider.PersistenceProvider$HibernateScrollableResultsIterator.hasNext(PersistenceProvider.java:454)
    at java.util.Iterator.forEachRemaining(Iterator.java:115)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at stream.bug.StreamBugApplication.lambda$0(StreamBugApplication.java:34)
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800)
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:784)
    at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:771)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1186)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1175)
    at stream.bug.StreamBugApplication.main(StreamBugApplication.java:22)
Caused by: org.h2.jdbc.JdbcSQLException: The object is already closed [90007-193]
    at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
    at org.h2.message.DbException.get(DbException.java:179)
    at org.h2.message.DbException.get(DbException.java:155)
    at org.h2.message.DbException.get(DbException.java:144)
    at org.h2.jdbc.JdbcResultSet.checkClosed(JdbcResultSet.java:3202)
    at org.h2.jdbc.JdbcResultSet.next(JdbcResultSet.java:129)
    at org.hibernate.internal.ScrollableResultsImpl.next(ScrollableResultsImpl.java:99)
    ... 12 more

我花了很多时间进行调试,最终成功创建了一个小的Spring Boot示例应用程序,展示了流错误: https://github.com/agsimeonov/stream-bug

我知道一些事情:

首先 - 这个bug与底层数据库无关。 虽然在示例项目中我使用了H2,但我尝试过使用Postgres,仍然会出现非常相似的错误。请注意,在我的其他项目中,我使用Tomcat连接池,我已经尝试了不同的连接池,所以这绝对不是连接池或底层数据库导致的问题。以下是使用Postgres和Tomcat连接池的示例跟踪,您可能会注意到它非常相似:

org.hibernate.exception.GenericJDBCException: could not advance using next()
  at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:47)
  at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:111)
  at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:97)
  at org.hibernate.internal.ScrollableResultsImpl.convert(ScrollableResultsImpl.java:69)
  at org.hibernate.internal.ScrollableResultsImpl.next(ScrollableResultsImpl.java:104)
  at org.springframework.data.jpa.provider.PersistenceProvider$HibernateScrollableResultsIterator.hasNext(PersistenceProvider.java:454)
  at java.util.Iterator.forEachRemaining(Iterator.java:115)
  at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
  at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
  at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
  at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
  at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
  at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
  at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
  at com.trove.sunstone.attributefusion.services.impl.PhysicalServiceImpl.match(PhysicalServiceImpl.java:130)
  at com.trove.sunstone.attributefusion.AppRunner.main(AppRunner.java:31)
  Suppressed: java.lang.reflect.UndeclaredThrowableException
    at com.sun.proxy.$Proxy238.hashCode(Unknown Source)
    at java.util.HashMap.hash(HashMap.java:338)
    at java.util.HashMap.get(HashMap.java:556)
    at org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.release(ResourceRegistryStandardImpl.java:76)
    at org.hibernate.internal.AbstractScrollableResults.close(AbstractScrollableResults.java:104)
    at org.springframework.data.jpa.provider.PersistenceProvider$HibernateScrollableResultsIterator.close(PersistenceProvider.java:465)
    at org.springframework.data.util.StreamUtils$CloseableIteratorDisposingRunnable.run(StreamUtils.java:96)
    at java.util.stream.AbstractPipeline.close(AbstractPipeline.java:323)
    at com.trove.sunstone.attributefusion.services.impl.PhysicalServiceImpl.match(PhysicalServiceImpl.java:137)
    ... 1 more
  Caused by: java.sql.SQLException: Statement closed.
    at org.apache.tomcat.jdbc.pool.interceptor.AbstractQueryReport$StatementProxy.invoke(AbstractQueryReport.java:224)
    ... 10 more
Caused by: org.postgresql.util.PSQLException: This ResultSet is closed.
  at org.postgresql.jdbc.PgResultSet.checkClosed(PgResultSet.java:2740)
  at org.postgresql.jdbc.PgResultSet.next(PgResultSet.java:1817)
  at org.hibernate.internal.ScrollableResultsImpl.next(ScrollableResultsImpl.java:99)
  ... 11 more

第二点 - 奇怪的是,如果从stream中的forEach()方法中删除以下代码行,则会导致stream正常结束。这让我相信可能存在某种时间问题,但是我尝试使用Thread.sleep()替换文件读取来复制它,但没有成功。另外,data.txt是一个包含3000行每行一个数字的文件。

try {
  File data = new File(getClass().getClassLoader().getResource("data.txt").getFile());
  try (BufferedReader reader = new BufferedReader(new FileReader(data))) {
    while (reader.readLine() != null) {
      // Do stuff for the current customer
    }
  }
} catch (IOException e) {}

第三步 - 替换:

Stream<Customer> stream = repository.streamAll()

使用:

Stream<Customer> stream = repository.findAll().stream()

修复了这个问题,所以这明显是流和/或ScrollableResults的一个bug,将所有数据加载到列表中可以使应用程序无错误地完成,但是对于我的当前项目,我需要直接使用Streams,因此使用findAll()不是一个选项。

如果有人遇到过这个问题并且已经能够解决,请告诉我。另外,请随意查看、分叉和/或更改提供的存储库中的代码,这可帮助解决此问题。我创建了这个项目作为演示,应该用来说明这个错误。


几个无关的注意事项:使用文件IO加载类加载器中的资源是错误的:一旦应用程序打包成jar或war,就不会再有文件了。直接从getResourceAsStream()返回的流中读取即可。而且为每个结果集元素从资源中读取相同的3000行是浪费的。只需一次性读取这些行,并将它们存储在List、Map或任何最适合的内存集合中即可。 - JB Nizet
我明白了。那段代码只是问题的一个示例。实际上有许多不同的文件,读取哪个文件取决于当前客户端,因此我无法将其存储在内存中。在这个项目中,我仅提供1个严格的示例。我将文件与项目一起打包,并在原始项目中使用类加载器以简化操作,而实际上输入文件并未与项目一起打包。 - Alexander Simeonov
即使是举例,也没有理由使用这样的破损代码。正确的代码甚至更短...而且不清楚为什么你在流中使用try-with-resource,但不在读取器中使用。 - Holger
引用时不要把句子拆开。我非常清楚流需要关闭。不清楚的是为什么你在一个地方展示了这个知识,但却不一致地没有在之后正确使用它。我看不出为什么例子必须随意编写,使用被反对甚至是错误的技术,当编写正确的例子更加简单时。 - Holger
甚至无法排除这些损坏的东西对问题的负责性。您有一个catch(IOException ex){},它会默默地忽略异常,并且由于手动关闭,读取器在异常情况下不会关闭,因此您不会注意到后续错误,例如打开的文件句柄过多。因此,编写一个干净的示例足以证明这不是问题将是非常有帮助的。 - Holger
显示剩余5条评论
1个回答

17
我在Spring Data JPA的JIRA上发布了我的问题,作为一个bug报告,而这个问题显然已经被观察到了。在那里进行了一些讨论后,我现在在与Stream相关的代码上使用@Transactional来解决问题。感谢Oliver Gierke在这里指出了这一点:https://jira.spring.io/browse/DATAJPA-989?focusedCommentId=133710&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-133710 我已经在最新的提交中将解决该错误的解决方案推送到了我的示例错误仓库中:https://github.com/agsimeonov/stream-bug/commit/9da536d0a9d921787f6d2d4d75720d363ba0358b

1
我遇到了同样的问题,感谢大家的提问和回答。 - Guido Medina
在我的情况下,流通过REST端点公开,因此这就是必须添加@Transactional的地方。但是,如果您使用Jersey进行REST端点,则似乎无法解决此问题!转换为Spring MVC解决了这个问题(使用@Transactional)。 - Triple S

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