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

时间:2011-02-21 15:13:31

标签: java hibernate jpa

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

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

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

for (Model model : models)
{
     System.out.println(model.getId());
}

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

修改:我定位的主要用例是一种批处理作业。如果运行需要很长时间,那就没关系了。没有涉及Web客户端;我只需要为每一行“做一些事情”,一次一个(或一些小N)。我只是想避免同时将它们全部留在记忆中。

15 个答案:

答案 0 :(得分:53)

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

然后,像这样使用它:

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

答案 1 :(得分:34)

我尝试了这里提供的答案,但是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();

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

答案 2 :(得分:29)

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

我们会在其帮助下定期处理数十亿行。

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

答案 3 :(得分:17)

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

此外,我建议使用更低级别的解决方案,因为ORM的开销(反射只是冰山一角)可能非常重要,即使用像ResultSet这样的轻量级支持,也可以迭代普通JdbcTemplate。 1}}会快得多。

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

答案 4 :(得分: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();  
}  

关闭方法

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}

答案 5 :(得分:5)

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

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

如果您已启动大量记录的更新,则最好保持更新简单并使用Query.executeUpdate()。 (可选)您可以使用工作管理器中的消息驱动Bean以异步模式执行更新。

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

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

答案 6 :(得分:4)

没有“正确”做什么,这不是JPA或JDO或任何其他ORM打算做的事情,直接JDBC将是你最好的选择,因为你可以配置它来带回一小部分一次一行,并在使用它们时将它们刷新,这就是存在服务器端游标的原因。

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

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

无论你怎么做,拉出一百万个东西,甚至是一个简单的List<Integer>都不会非常有效。正确的方法是做一个简单的SELECT id FROM table,设置为SERVER SIDE(依赖于供应商),将光标设置为FORWARD_ONLY READ-ONLY并迭代它。

如果你真的通过每个人调用一些web服务器来处理数百万个id,你将不得不做一些并发处理,以便在任何合理的时间内运行。使用JDBC游标进行拉取并在ConcurrentLinkedQueue中一次放置一些并使用少量线程(#CPU / Cores + 1)拉动并处理它们是完成任务的唯一方法鉴于你已经没有内存,任何“正常”RAM的机器。

同样见answer

答案 7 :(得分:3)

你可以使用另一个“技巧”。仅加载您感兴趣的实体的标识符集合。说标识符的类型为long = 8bytes,然后10 ^ 6这样的标识符列表大约为8Mb。如果它是一个批处理过程(一次一个实例),那么它是可以忍受的。然后迭代并完成工作。

另一个评论 - 无论如何你应该在块中执行此操作 - 特别是如果您修改记录,否则数据库中的回滚段将会增长。

在设置firstResult / maxRows策略时 - 远离顶部的结果非常缓慢。

还要考虑到数据库可能在读取提交隔离中运行,因此要避免幻像读取加载标识符,然后逐个加载实体(或10乘10或其他)。

答案 8 :(得分:1)

我很惊讶地看到存储过程的使用在这里的答案中并不突出。在过去,当我不得不做这样的事情时,我创建了一个存储过程,以小块处理数据,然后睡了一会儿,然后继续。睡眠的原因是不要压倒数据库,数据库可能也被用于更实时类型的查询,例如连接到网站。如果没有其他人使用数据库,那么你可以省去睡眠。如果您需要确保只处理一次并且只处理一次,那么您将需要创建一个额外的表(或字段)来存储您已处理的记录,以便在重新启动时具有弹性。

这里的性能节省是显着的,可能比你在JPA / Hibernate / AppServer中可以做的任何事情快几个数量级,并且你的数据库服务器很可能有自己的服务器端游标类型的机制来有效地处理大型结果集。性能节省来自于不必将数据从数据库服务器发送到应用程序服务器,在那里处理数据,然后将其发回。

使用存储过程存在一些明显的缺点,可能会完全排除这一点,但如果您已经掌握了个人工具箱中的技能并且可以在这种情况下使用它,那么您可以淘汰这些事情很快。

答案 9 :(得分:1)

扩展@Tomasz Nurkiewicz的答案。您可以访问DataSource,而@Resource(name = "myDataSource", lookup = "java:comp/DefaultDataSource") private DataSource myDataSource; 可以为您提供连接

try (Connection connection = myDataSource.getConnection()) {
    // raw jdbc operations
}

在您的代码中

DECLARE @tbl TABLE(a INT, b INT);
INSERT INTO @tbl VALUES(1,5),(1,NULL),(2,NULL),(2,NULL),(3,NULL);
--Your test data
SELECT * FROM @tbl;

--And this is what you want - hopefully...
SELECT DISTINCT tbl.a
FROM @tbl AS tbl
WHERE NOT EXISTS(SELECT * FROM @tbl AS x WHERE x.a=tbl.a AND b IS NOT NULL)

这将允许您绕过某些特定大批量操作(如导入/导出)的JPA,但是如果需要,您仍然可以访问实体管理器以进行其他JPA操作。

答案 10 :(得分:0)

我自己也想知道这件事。这似乎很重要:

  • 您的数据集有多大(行)
  • 您正在使用的JPA实施
  • 您为每一行进行了哪种处理。

我编写了一个迭代器,可以轻松更换两种方法(findAll vs findEntries)。

我建议您同时尝试。

Long count = entityManager().createQuery("select count(o) from Model o", Long.class).getSingleResult();
ChunkIterator<Model> it1 = new ChunkIterator<Model>(count, 2) {

    @Override
    public Iterator<Model> getChunk(long index, long chunkSize) {
        //Do your setFirst and setMax here and return an iterator.
    }

};

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


public static abstract class ChunkIterator<T> 
    extends AbstractIterator<T> implements Iterable<T>{
    private Iterator<T> chunk;
    private Long count;
    private long index = 0;
    private long chunkSize = 100;

    public ChunkIterator(Long count, long chunkSize) {
        super();
        this.count = count;
        this.chunkSize = chunkSize;
    }

    public abstract Iterator<T> getChunk(long index, long chunkSize);

    @Override
    public Iterator<T> iterator() {
        return this;
    }

    @Override
    protected T computeNext() {
        if (count == 0) return endOfData();
        if (chunk != null && chunk.hasNext() == false && index >= count) 
            return endOfData();
        if (chunk == null || chunk.hasNext() == false) {
            chunk = getChunk(index, chunkSize);
            index += chunkSize;
        }
        if (chunk == null || chunk.hasNext() == false) 
            return endOfData();
        return chunk.next();
    }

}

我最终没有使用我的块迭代器(因此可能没有经过测试)。顺便说一句,如果你想使用它,你将需要谷歌收藏。

答案 11 :(得分:0)

使用Pagination概念检索结果

答案 12 :(得分:0)

有了hibernate,有4种不同的方法可以实现你想要的。每个都有设计权衡,限制和后果。我建议探索每一个并决定哪个适合你的情况。

  1. 使用无状态会话和scroll()
  2. 每次迭代后使用session.clear()。当需要连接其他实体时,请在单独的会话中加载它们。实际上,第一个会话正在模拟无状态会话,但保留了有状态会话的所有功能,直到对象被分离。
  3. 使用iterate()或list()但只获取第一个查询中的id,然后在每次迭代的单独会话中,执行session.load并在迭代结束时关闭会话。
  4. 使用Query.iterate()和EntityManager.detach()又名Session.evict();

答案 13 :(得分:0)

这是一个简单的,简单的JPA示例(在Kotlin中),显示了如何对任意大的结果集进行分页,一次可以读取100个项目的块,而无需使用游标(每个游标都消耗数据库上的资源)。它使用键集分页。

有关键集分页的概念,请参见https://use-the-index-luke.com/no-offset,有关不同分页方法及其缺点的比较,请参见https://www.citusdata.com/blog/2016/03/30/five-ways-to-paginate/

/*
create table my_table(
  id int primary key, -- index will be created
  my_column varchar
)
*/

fun keysetPaginationExample() {
    var lastId = Integer.MIN_VALUE
    do {

        val someItems =
        myRepository.findTop100ByMyTableIdAfterOrderByMyTableId(lastId)

        if (someItems.isEmpty()) break

        lastId = someItems.last().myTableId

        for (item in someItems) {
          process(item)
        }

    } while (true)
}

答案 14 :(得分:0)

JPA和NativeQuery的示例每次使用偏移量获取大小元素

public List<X> getXByFetching(int fetchSize) {
        int totalX = getTotalRows(Entity);
        List<X> result = new ArrayList<>();
        for (int offset = 0; offset < totalX; offset = offset + fetchSize) {
            EntityManager entityManager = getEntityManager();
            String sql = getSqlSelect(Entity) + " OFFSET " + offset + " ROWS";
            Query query = entityManager.createNativeQuery(sql, X.class);
            query.setMaxResults(fetchSize);
            result.addAll(query.getResultList());
            entityManager.flush();
            entityManager.clear();
        return result;
    }