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


114

假设我有一个包含数百万行的表。使用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)。我只是想避免将它们全部同时存储在内存中。


您正在使用什么数据库和JDBC驱动程序?

Answers:


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

然后,像这样使用它:

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

33
我认为,如果在批处理过程中有新的插入内容,则该示例并不安全。用户必须基于一列进行排序,以确保新插入的数据将在结果列表的末尾。
Balazs Zsoldos

当当前页面是最后一页并且少于100个元素时,检查size() == 100将跳过另一个返回空列表的查询
cdalxndr

38

我尝试了这里给出的答案,但是JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2不适用于这些。我们刚刚从JBoss 4.x迁移到JBoss 5.1,因此我们暂时坚持使用它,因此我们可以使用的最新Hibernate是3.3.2。

添加几个额外的参数可以完成这项工作,并且这样的代码可以在没有OOMEs的情况下运行:

        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
嗨,Zds,您扫描百万行的用例对我来说当然很常见,并感谢您发布最终代码。就我而言,我将记录推到Solr中,以对其进行索引以进行全文搜索。而且,由于我不会涉及的业务规则,我需要通过Hibernate,而不是仅使用JDBC或Solr的内置模块。
Mark Bennett'2

乐于帮助 :-)。我们还处理大型数据集,在这种情况下,允许用户查询同一城市/县甚至州内的所有街道名称,因此创建索引需要读取大量数据。
Zds 2012年

出现在MySQL上,您确实必须经历所有这些麻烦:stackoverflow.com/a/20900045/32453(我想不到的其他DB可能不那么严格...)
rogerdpack 2016年

32

您实际上不能在直接JPA中执行此操作,但是Hibernate支持无状态会话和可滚动结果集。

在它的帮助下,我们通常会处理数十亿行。

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


17
谢谢。很高兴知道有人通过Hibernate执行了数十亿行的操作。这里有些人声称这是不可能的。:-)
George Armhold

2
也可以在此处添加示例吗?我认为这类似于Zds的示例吗?
rogerdpack '16

19

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

我也建议使用更底层的解决方案,因为ORM的开销(反射只是冰山的一角)可能会非常大,以至于遍历纯文本ResultSet,甚至使用上述的轻量级支持JdbcTemplate也会更快。

JPA根本不旨在对大量实体执行操作。您可能会使用flush()/ clear()来回避OutOfMemoryError,但请再次考虑。您付出巨额资源消耗的代价却很少。


JPA的优点不仅在于与数据库无关,而且甚至没有使用传统数据库(NoSQL)的可能性。有时不难进行刷新/清除,并且通常不经常执行批处理操作。
亚当·根特

1
嗨,托马斯 我有很多理由抱怨JPA / Hibernate,但是请尊重地,我真的怀疑它们“不是设计用于对许多对象进行操作”。我怀疑我只需要学习此用例的正确模式即可。
乔治·阿姆霍尔德

4
好吧,我只能想到两种模式:分页(多次提到)和flush()/ clear()。第一个是恕我直言,它不是为批处理目的而设计的,而使用flush()/ clear()气味的序列就像泄漏抽象一样
Tomasz Nurkiewicz 2011年

是的,这是您提到的分页和冲洗/清除的结合。谢谢!
George Armhold

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

6
尼斯的jQuery对象
USR-本地ΕΨΗΕΛΩΝ

我尝试了一下您的代码,但仍然获得了OOM-似乎所有T对象(以及所有从T引用的联接表对象)都不是GC。分析显示它们是从org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork中的“表”中引用的,以及org.eclipse.persistence.internal.identitymaps.CacheKey。我调查了缓存,并且我的设置都是默认设置(“禁用选择性”,“软子缓存”弱,“缓存大小100”,“丢弃无效”)。我将研究禁用会话,看看是否有帮助。顺便说一句,我只是简单地使用“ for(T o:results)”遍历返回光标。
Edi Bice

Badum tssssssss
dctremblay

5

这取决于您必须执行的操作。为什么要遍历一百万行?您是否以批处理模式更新某些内容?您要向客户显示所有记录吗?您是否正在对检索到的实体计算一些统计信息?

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

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

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

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


很简单,我需要为“每一行”做一些事情。当然,这是一个常见的用例。在我现在正在处理的特定情况下,我需要使用每行的ID(PK)来查询完全不在数据库外部的外部Web服务。结果不会显示回任何客户端Web浏览器,因此没有用户界面可言。换句话说,这是一个批处理工作。
乔治·阿姆霍尔德

如果“需要”为每一行打印ID,则没有其他方法可以获取每一行,获取ID并打印。最佳解决方案取决于您需要做什么。
Dainius

@Caffeine Coma,如果只需要每一行的ID,则最大的改进可能是仅获取该列,SELECT m.id FROM Model m然后遍历List <Integer>。
约恩·霍斯特曼(JörnHorstmann),

1
@JörnHorstmann-如果有数百万行,那真的重要吗?我的观点是,具有数百万个对象(但是很小)的ArrayList对JVM堆不利。
乔治·阿姆霍尔德

@Dainius:我的问题确实是:“如何在没有整个ArrayList内存的情况下遍历每一行?” 换句话说,我想要一次拉N的接口,其中N明显小于100万。:-)
George Armhold

5

没有“适当的”方法可以执行此操作,这不是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量,因为您已经用完了内存。

也请参阅此答案


1
因此,您是说没有公司需要访问用户表的每一行?他们的程序员只是在需要时将Hibernate扔出窗外吗?“ 有没有办法处理几十万行 ” -在我的问题我指出setFirstResult / setMaxResult,所以显然有一种方式。我问是否有更好的。
乔治·阿姆霍尔德

“不管您怎么做,将一百万个东西甚至拖入简单的List <Integer>都不会非常有效。” 这正是我的意思。我问的是如何创建巨型列表,而是遍历结果集。
乔治·阿姆霍尔德

正如我在回答中所建议的那样,使用带有FORWARD_ONLY READ_ONLY和SERVER_SIDE游标的简单直接JDBC select语句。如何使JDBC使用SERVER_SIDE游标取决于数据库驱动程序。

1
我完全同意这个答案。最佳解决方案取决于问题。如果问题很容易加载几个实体,那么JPA很好。如果问题是有效地使用大量数据,则直接使用JDBC更好。
extraneon

4
扫描数百万条记录很常见,原因有很多,例如将它们编入搜索引擎。并且尽管我同意JDBC通常是一条更直接的路线,但有时您会走进一个已经在Hibernate层中捆绑了非常复杂的业务逻辑的项目。如果绕过它并转到JDBC,则会绕过业务逻辑,这对于重新实现和维护有时并非易事。当人们发布关于非典型用例的问题时,他们通常会知道这有点怪异,但可能是继承某些内容而不是从头开始构建,甚至可能无法透露细节。
Mark Bennett'2

4

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

另一句话-无论如何,您都应该分块执行此操作-特别是在修改记录时,否则数据库中的回滚段会增加。

设置firstResult / maxRows策略时- 对于远离顶部的结果,它会非常缓慢。

还应考虑到数据库可能正在以提交隔离的方式运行,因此要避免幻像读取加载标识符,然后一个一个地(或10个到十个,或其他)来加载实体。


@Marcin,您好,您或其他任何人都可以提供指向示例代码的链接,这些示例代码应用此分块且id优先的逐步方法(最好使用Java8流)吗?
krevelen

2

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

这里的性能节省非常可观,可能比您在JPA / Hibernate / AppServer领域中所做的任何事情都快几个数量级,并且数据库服务器很可能具有自己的服务器端游标类型的机制,可以有效地处理大型结果集。节省性能的原因是不必将数据从数据库服务器传送到应用程序服务器,在该服务器上您可以处理数据,然后再将其传送回。

使用存储过程可能会给您带来完全的不利影响,但是如果您在个人工具箱中拥有这种技能并且可以在这种情况下使用它,则可以相当快地淘汰掉这些东西。 。


1
-2下注-下一位下注者请为您的下注辩护吗?
危险

1
阅读这些内容时,我也想过同样的事情。该问题表明没有UI的大批量处理作业。假设您不需要特定于应用程序服务器的资源,为什么要完全使用应用程序服务器?存储过程将更加高效。
jdessey 2014年

@jdessey视情况而定,假设我们有一个导入工具,该工具在导入时应该对系统的其他部分执行某些操作,例如,根据已经编码为EJB的一些业务规则,将行添加到另一个表中。然后,除非您可以使EJB在嵌入式模式下运行,否则在应用服务器上运行将更有意义。
Archimedes Trajano 2015年

1

扩展@Tomasz Nurkiewicz的答案。您可以访问DataSource,从而可以为您提供连接

@Resource(name = "myDataSource",
    lookup = "java:comp/DefaultDataSource")
private DataSource myDataSource;

在您的代码中

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

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


0

使用Pagination概念检索结果


4
分页对于GUI非常有用。但是,为了处理大量数据,ScrollableResultSet是很久以前发明的。只是不在JPA中。
extraneon

0

我自己对此感到纳闷。似乎很重要:

  • 您的数据集有多大(行)
  • 您正在使用什么JPA实现
  • 您对每一行进行什么样的处理。

我编写了一个Iterator,可以轻松地交换两种方法(findAll与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();
    }

}

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


关于“您要为每一行执行什么样的处理”-如果行数为数百万,我怀疑即使是只有id列的简单对象也会引起问题。我也考虑过编写包装setFirstResult / setMaxResult的自己的Iterator,但是我认为这一定是一个常见的问题(并有望解决!)。
乔治·阿姆霍尔德

@Caffeine Coma我发布了Iterator,您可能还可以做一些JPA来适应它。告诉我是否有帮助。我最终没有使用(没有找到一个all)。
亚当·根特

0

使用休眠模式,可以通过4种不同的方式来实现所需的功能。每个设计都有权衡,局限和后果。我建议您仔细研究每种情况,并确定哪种方法适合您的情况。

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

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

0

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

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;
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.