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

f2uvfpb9  于 2021-06-30  发布在  Java
关注(0)|答案(15)|浏览(342)

假设我有一张有数百万行的table。使用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)。我只是尽量避免把它们同时记在记忆里。

cotxawn7

cotxawn71#

如果你用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();
    }
}
luaexgnf

luaexgnf2#

你可以用另一个“把戏”。只加载您感兴趣的实体的标识符集合。假设标识符的类型为long=8bytes,那么10^6一个这样的标识符的列表大约是8mb。如果它是一个批处理过程(一次一个示例),那么它是可以忍受的。然后迭代并完成这项工作。
还有一句话-无论如何,您应该成批地执行此操作-特别是当您修改记录时,否则数据库中的回滚段将增加。
当涉及到设置firstresult/maxrows策略时,对于远离顶端的结果来说,这将是非常缓慢的。
还要考虑到数据库可能是在读提交隔离状态下运行的,因此要避免幻像读取加载标识符,然后逐个加载实体(或10乘10或其他)。

wyyhbhjk

wyyhbhjk3#

我很惊讶地看到,在这里的答案中,存储过程的使用并不是很突出。在过去,当我不得不做这样的事情时,我会创建一个存储过程来处理小块数据,然后休眠一段时间,然后继续。休眠的原因是为了不让数据库崩溃,因为数据库可能也被用于更实时的查询类型,例如连接到网站。如果没有其他人使用数据库,那么您可以省去睡眠。如果需要确保每个记录只处理一次,那么需要创建一个附加表(或字段)来存储已处理的记录,以便在重新启动时具有弹性。
这里的性能节省非常显著,可能比jpa/hibernate/appserver land中的任何操作都快几个数量级,而且您的数据库服务器很可能有自己的服务器端游标类型的机制来高效地处理大型结果集。性能上的节省来自于不必将数据从数据库服务器传送到应用程序服务器,在应用程序服务器上处理数据,然后再传送回来。
使用存储过程有一些明显的缺点,这可能会完全排除这一点,但是如果您的个人工具箱中有这种技能,并且能够在这种情况下使用它,那么您可以相当快地解决这类问题。

8tntrjer

8tntrjer4#

我自己也很奇怪。这似乎很重要:
数据集有多大(行)
您使用的是什么jpa实现
您对每一行所做的处理。
我已经编写了一个迭代器,可以很容易地交换这两种方法(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();
    }

}

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

6l7fqoea

6l7fqoea5#

这取决于你要做的手术。你为什么要绕着一百万圈?你在批量更新什么东西吗?你要向客户显示所有记录吗?您正在计算检索到的实体的一些统计信息吗?
如果您要向客户端显示一百万条记录,请重新考虑您的用户界面。在这种情况下,合适的解决方案是对结果分页并使用 setFirstResult() 以及 setMaxResult() .
如果您已经启动了大量记录的更新,那么最好保持更新的简单性和易用性 Query.executeUpdate() . 或者,您可以使用消息驱动bean或工作管理器以异步模式执行更新。
如果您正在对检索到的实体计算一些统计信息,那么可以利用jpa规范定义的分组函数。
其他情况请具体说明:)

bjg7j2ky

bjg7j2ky6#

我尝试了这里给出的答案,但是jboss 5.1+mysql connector/j5.1.15+hibernate3.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。

ctrmrzij

ctrmrzij7#

使用hibernate有4种不同的方法来实现你想要的。每个都有设计权衡、限制和后果。我建议对每一个问题进行探讨,并决定哪一个适合你的情况。
与scroll()一起使用无状态会话
每次迭代后使用session.clear()。当需要附加其他实体时,请在单独的会话中加载它们。实际上,第一个会话模拟无状态会话,但保留有状态会话的所有特性,直到对象分离。
使用iterate()或list(),但在第一个查询中仅获取ID,然后在每次迭代的单独会话中,在迭代结束时执行session.load并关闭会话。
将query.iterate()与entitymanager.detach()或session.evict()一起使用;

jobtbby3

jobtbby38#

老实说,我建议离开jpa,继续使用jdbc(当然是使用jdbc) JdbcTemplate 支持类等)。jpa(以及其他orm提供者/规范)并不是设计用来操作一个事务中的多个对象的,因为它们假定加载的所有对象都应该留在一级缓存中(因此需要 clear() 在jpa中)。
另外,我建议使用更低级的解决方案,因为orm(反射只是冰山一角)的开销可能非常大,以至于在平原上迭代 ResultSet ,甚至使用了一些轻量级的支持,如前所述 JdbcTemplate 会快得多。
jpa并不是设计用来对大量实体执行操作的。你可以和我一起玩 flush() / clear() 避免 OutOfMemoryError ,但请再次考虑这一点。付出巨大资源消耗的代价,你收获很少。

u4dcyp6a

u4dcyp6a9#

JavaPersistencewithHibernate的第537页给出了一个使用 ScrollableResults 唉,只是为了冬眠。
所以使用 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();
    }
}
niknxzdl

niknxzdl10#

来扩展@tomasz nurkiewicz的答案。您可以访问 DataSource 这反过来又可以为你提供一个连接

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

在你的代码里

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

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

b1zrtrql

b1zrtrql11#

下面是一个简单、直接的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)
}
de90aj5v

de90aj5v12#

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;
    }
mctunoxg

mctunoxg13#

没有“适当”的方法来做这件事,这不是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)拉取并处理它们,这是在内存“正常”的机器上完成任务的唯一方法,因为内存已经用完了。
也可以看到这个答案。

hyrbngr7

hyrbngr714#

在纯jpa中不能真正做到这一点,但是hibernate支持无状态会话和可滚动的结果集。
我们通常在它的帮助下处理数十亿行。
以下是文档链接:http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-无状态会话

u5rb5r59

u5rb5r5915#

使用 Pagination 检索结果的概念

相关问题