JPA:迭代大型结果集的正确模式是什么?

Geo*_*old 112 java hibernate jpa

假设我有一张包含数百万行的表格.使用JPA,对该表的查询进行迭代的正确方法是什么,这样我就没有包含数百万个对象的所有内存列表

例如,我怀疑如果表很大,以下内容会爆炸:

List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList();

for (Model model : models)
{
     System.out.println(model.getId());
}
Run Code Online (Sandbox Code Playgroud)

分页(循环和手动更新setFirstResult()/ setMaxResult())真的是最好的解决方案吗?

编辑:我定位的主要用例是一种批处理作业.如果运行需要很长时间,那就没关系了.没有涉及Web客户端; 我只需要为每一行"做一些事情",一次一个(或一些小N).我只是想避免让它们同时存在于内存中.

Geo*_*old 55

Java Persistence with Hibernate的第537页提供了一个使用的解决方案ScrollableResults,但它只适用于Hibernate.

所以似乎使用setFirstResult/ setMaxResults和手动迭代确实是必要的.这是我使用JPA的解决方案:

private List<Model> getAllModelsIterable(int offset, int max)
{
    return entityManager.createQuery("from Model m", Model.class).setFirstResult(offset).setMaxResults(max).getResultList();
}
Run Code Online (Sandbox Code Playgroud)

然后,像这样使用它:

private void iterateAll()
{
    int offset = 0;

    List<Model> models;
    while ((models = Model.getAllModelsIterable(offset, 100)).size() > 0)
    {
        entityManager.getTransaction().begin();
        for (Model model : models)
        {
            log.info("do something with model: " + model.getId());
        }

        entityManager.flush();
        entityManager.clear();
        em.getTransaction().commit();
        offset += models.size();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为如果在批处理过程中有新的插入,这个例子是不安全的.用户必须根据列确定新插入的数据将位于结果列表的末尾. (33认同)

Zds*_*Zds 36

我尝试了这里提供的答案,但JBoss 5.1 + MySQL Connector/J 5.1.15 + Hibernate 3.3.2无法使用.我们刚刚从JBoss 4.x迁移到JBoss 5.1,所以我们暂时坚持使用它,因此我们可以使用的最新Hibernate是3.3.2.

添加几个额外的参数完成了这项工作,像这样的代码在没有OOME的情况下运行:

        StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

        Query query = session
                .createQuery("SELECT a FROM Address a WHERE .... ORDER BY a.id");
        query.setFetchSize(Integer.valueOf(1000));
        query.setReadOnly(true);
        query.setLockMode("a", LockMode.NONE);
        ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
        while (results.next()) {
            Address addr = (Address) results.get(0);
            // Do stuff
        }
        results.close();
        session.close();
Run Code Online (Sandbox Code Playgroud)

关键的行是createQuery和scroll之间的查询参数.没有它们,"scroll"调用会尝试将所有内容加载到内存中,并且永远不会完成或运行到OutOfMemoryError.

  • 嗨Zds,你扫描数百万行的用例对我来说当然很常见,感谢你发布最终代码.在我的情况下,我正在将记录推送到Solr,为全文搜索索引它们.而且,由于我不会涉及的业务规则,我需要通过Hibernate,而不仅仅是使用JDBC或Solr的内置模块. (2认同)

Cyb*_*rax 31

你不能在直接的JPA中真正做到这一点,但是Hibernate支持无状态会话和可滚动的结果集.

我们经常在它的帮助下处理数十亿行.

以下是文档链接:http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession

  • 谢谢.很高兴知道有人通过Hibernate做了数十亿行.这里的一些人声称这是不可能的.:-) (16认同)
  • 也可以在此处添加示例吗?我认为它类似于Zds的示例? (2认同)

Tom*_*icz 18

说实话,我建议离开JPA并坚持使用JDBC(但肯定使用JdbcTemplate支持类等).JPA(以及其他ORM提供程序/规范)不是设计用于在一个事务中对许多对象进行操作,因为它们假设所有加载的内容应该保留在第一级缓存中(因此需要clear()在JPA中).

此外,我建议更多的低级解决方案,因为ORM的开销(反射只是冰山一角)可能是如此重要,迭代过度ResultSet,即使使用一些像上面提到的轻量级支持JdbcTemplate也会快得多.

JPA根本不是为在大量实体上执行操作而设计的.你可以玩flush()/ clear()避免OutOfMemoryError,但再考虑一下.你很少得到巨额资源消耗的代价.

  • 好吧,我只能想到两种模式:paginations(多次提到)和`flush()`/`clear()`.第一个是恕我直言,不是为了批量处理而设计的,而使用flush()/ clear()的序列闻起来像*漏洞抽象*. (4认同)

use*_*477 7

如果您使用EclipseLink,我使用此方法将结果作为Iterable获取

private static <T> Iterable<T> getResult(TypedQuery<T> query)
{
  //eclipseLink
  if(query instanceof JpaQuery) {
    JpaQuery<T> jQuery = (JpaQuery<T>) query;
    jQuery.setHint(QueryHints.RESULT_SET_TYPE, ResultSetType.ForwardOnly)
       .setHint(QueryHints.SCROLLABLE_CURSOR, true);

    final Cursor cursor = jQuery.getResultCursor();
    return new Iterable<T>()
    {     
      @SuppressWarnings("unchecked")
      @Override
      public Iterator<T> iterator()
      {
        return cursor;
      }
    }; 
   }
  return query.getResultList();  
}  
Run Code Online (Sandbox Code Playgroud)

关闭方法

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • Nice*jQuery*对象 (5认同)

frm*_*frm 5

这取决于你必须做的操作类型.你为什么要循环超过一百万行?您是否以批处理模式更新某些内容?您要向客户显示所有记录吗?您是否在检索到的实体上计算一些统计数据?

如果您要向客户显示一百万条记录,请重新考虑您的用户界面.在这种情况下,适当的解决方案是对结果进行分页并使用setFirstResult()setMaxResult().

如果您已启动大量记录的更新,则最好保持更新的简单和使用Query.executeUpdate().(可选)您可以使用消息驱动Bean oa Work Manager以异步模式执行更新.

如果要在检索到的实体上计算某些统计信息,则可以利用JPA规范定义的分组函数.

对于任何其他情况,请更具体:)


小智 5

没有“正确”的方法来执行此操作,这不是 JPA 或 JDO 或任何其他 ORM 的目的,直接 JDBC 将是您的最佳选择,因为您可以将其配置为带回少量行一段时间并在使用时刷新它们,这就是存在服务器端游标的原因。

ORM 工具不是为批量处理而设计的,它们旨在让您操作对象并尝试使存储数据的 RDBMS 尽可能透明,大多数至少在某种程度上在透明部分失败。在这种规模下,由于对象实例化的开销,简单明了,没有办法处理数十万行( Objects ),更不用说使用任何 ORM 处理数百万行并让它在任何合理的时间内执行。

使用适当的工具。直接 JDBC 和存储过程在 2011 年肯定占有一席之地,尤其是在它们与这些 ORM 框架相比更擅长做的事情上。

List<Integer>不管你怎么做,把一百万个东西拉出来,即使是一个简单的东西也不会很有效率。做你所要求的正确方法是一个简单的SELECT id FROM table,设置为SERVER SIDE(依赖于供应商)和光标到FORWARD_ONLY READ-ONLY并迭代它。

如果你真的要通过调用一些 web 服务器来处理数百万个 id,你将不得不做一些并发处理,以便在任何合理的时间内运行。使用 JDBC 游标拉取并一次将其中几个放入ConcurrentLinkedQueue并使用一个小的线程池(#CPU/Cores + 1)拉取和处理它们是在具有任何“正常”的 RAM 量,因为您的内存已经用完了。

也看到这个答案

  • 由于多种原因,扫描数百万条记录很常见,例如将它们索引到搜索引擎中。尽管我同意 JDBC 通常是一条更直接的途径,但您有时会走进一个项目,该项目已经将非常复杂的业务逻辑捆绑在 Hibernate 层中。如果你绕过它并转到 JDBC,你就绕过了业务逻辑,这有时需要重新实现和维护。当人们发布有关非典型用例的问题时,他们通常知道这有点奇怪,但可能是继承某些东西而不是从头开始构建,并且可能无法透露细节。 (4认同)