JPA:迭代大型结果集的正确模式是什么?
比方说,我有一个数百万行的表。 使用JPA,迭代对该表的查询的正确方法是什么,这样我就不会拥有数百万个对象的所有内存列表 ?
例如,如果桌子很大,我怀疑以下情况会炸毁:
List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList(); for (Model model : models) { System.out.println(model.getId()); }
是分页(循环和手动更新setFirstResult()
/ setMaxResult()
)真的是最好的解决scheme?
编辑 :我要定位的主要用例是一种批处理作业。 如果运行时间很长,这很好。 没有涉及networking客户端; 我只需要为每一行“做一些事情”,一次一个(或者一些小N)。 我只是想尽量避免让他们在记忆中。
使用Hibernate的Java持久性的持久性提供了一个使用ScrollableResults
的解决scheme,但是它只适用于Hibernate。
所以看来,使用setFirstResult
/ setMaxResults
和手动迭代确实是必要的。 这是我使用JPA的解决scheme:
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(); } }
我尝试了这里给出的答案,但是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之间的查询参数。 没有它们,“滚动”调用会尝试将所有内容加载到内存中,或者永远不会结束或运行到OutOfMemoryError。
你不能直接在JPA中做这个,但是Hibernate支持无状态会话和可滚动结果集。
我们经常在其帮助下处理数十亿行。
这是一个链接到文档: http : //docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession
说实话,我会build议离开JPA并坚持JDBC(但肯定使用JdbcTemplate
支持类或类似的)。 JPA(和其他的ORM提供者/规范)并不是为了在一个事务中对许多对象进行操作而devise的,因为它们假设所有加载的应该保留在第一级caching中(因此需要在JPA中clear()
)。
另外我推荐更低级别的解决scheme,因为ORM的开销(reflection只是冰山一angular)可能非常重要,即使使用一些轻量级的支持(如提到的JdbcTemplate
也会快得多。
JPA根本不是为大量实体执行操作而devise的。 您可以使用flush()
/ clear()
来避免OutOfMemoryError
,但是再次考虑这一点。 你很less付出巨大的资源消耗的代价。
如果你使用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(); }
closures方法
static void closeCursor(Iterable<?> list) { if (list.iterator() instanceof Cursor) { ((Cursor) list.iterator()).close(); } }
这取决于你必须做的操作的types。 你为什么循环上百万行? 你正在更新批处理模式? 你打算把所有的logging都显示给客户吗? 你计算检索的实体的一些统计数据?
如果您要向客户显示一百万条logging,请重新考虑您的用户界面。 在这种情况下,适当的解决scheme是对结果进行分页并使用setFirstResult()
和setMaxResult()
。
如果您启动了大量logging的更新,则最好保持更新简单并使用Query.executeUpdate()
。 或者,您可以使用消息驱动Bean或工作pipe理器以asynchronous模式执行更新。
如果您正在计算检索到的实体的一些统计信息,则可以利用JPA规范定义的分组函数。
对于任何其他情况,请更具体:)
没有什么“适当的”做什么,这不是JPA或JDO或任何其他ORM打算做什么,直接的JDBC将是你最好的select,因为你可以configuration它来带回less量的行一个时间和刷新他们,因为他们使用,这就是为什么服务器端游标存在。
ORM工具不是为批量处理而devise的,它们被devise为让你操作对象,并试图使RDBMS中的数据存储在尽可能透明的位置,大多数在透明部分至less在某种程度上失败。 在这种规模下,没有办法处理成千上万行(对象),而使用任何ORM的数百万甚至更less,因为对象实例化开销很简单,所以在任何合理的时间内执行。
使用适当的工具。 直接的JDBC和存储过程在2011年肯定会有一席之地,特别是在他们比这些ORM框架更好的时候。
无论你如何操作,拉一百万的东西,即使是简单的List<Integer>
都不会很高效。 正确的方法来做你所要求的是一个简单的SELECT id FROM table
,设置为SERVER SIDE
(供应商依赖)和光标FORWARD_ONLY READ-ONLY
并迭代。
如果真的要通过调用一个Web服务器来处理数百万个ID,那么您将不得不进行一些并发处理,以便在任何合理的时间内运行。 用一个JDBC游标拉出并将其中的一部分一次放在一个ConcurrentLinkedQueue中,并且有一个小的线程池(#CPU / Cores + 1)拉和处理它们是在具有任何“正常“的RAM量,因为你已经用完了内存。
看到这个答案 。
你可以使用另一个“伎俩”。 只加载你感兴趣的实体的标识符。说标识符的types是long = 8bytes,然后10 ^ 6这样的标识符的列表大约8Mb。 如果它是一个批处理过程(一次一个实例),那么它是可承受的。 然后迭代并完成这项工作。
另一种说法 – 无论如何你都应该这样做 – 特别是如果你修改logging,否则数据库中的回滚段将会增长。
当设置firstResult / maxRows策略 – 对于远离顶端的结果,它将非常非常慢。
还要考虑到数据库可能在读取提交的隔离中操作 ,所以为了避免幻象读取负载标识符,然后一个接一个地(或10×10或其他)加载实体。
我很惊讶地看到,在这里的答案中,存储过程的使用并不突出。 在过去,当我不得不这样做的时候,我创build了一个存储过程,以小块的forms处理数据,然后hibernate一会儿,然后继续。 hibernate的原因是不要压倒大概也用于更多实时types查询的数据库,例如连接到网站。 如果没有其他人使用数据库,则可以省去睡眠。 如果您需要确保一次处理每条logging一次,则需要创build一个额外的表(或字段)来存储您已处理的logging,以便在重新启动时保持弹性。
在这里节省的性能是非常重要的,可能比JPA / Hibernate / AppServer领域中的任何事情都要快,数据库服务器很可能有自己的服务器端游标types的机制来高效地处理大型结果集。 性能节约来自于不必将数据从数据库服务器发送到应用程序服务器,在那里处理数据,然后将其发回。
使用存储过程有一些明显的缺点,这些存储过程可能会完全排除你,但是如果你已经在个人工具箱中使用了这个技能并且可以在这种情况下使用它,那么你可以相当快地把这些东西。
展开@Tomasz Nurkiewicz的回答。 您可以访问DataSource
,然后可以为您提供连接
@Resource(name = "myDataSource", lookup = "java:comp/DefaultDataSource") private DataSource myDataSource;
在你的代码中你有
try (Connection connection = myDataSource.getConnection()) { // raw jdbc operations }
这将允许您绕过JPA进行一些特定的大批量操作,如导入/导出,但是如果您需要,您仍然可以访问其他JPA操作的实体pipe理器。
使用Pagination
概念来检索结果
我自己也想过这个。 这似乎很重要:
- 你的数据集有多大(行)
- 你正在使用什么JPA实现
- 你为每一行做了什么样的处理。
我已经写了一个迭代器,以便更换这两种方法(findAll vs findEntries)。
我build议你尝试两个。
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(); } }
我结束了不使用我的块迭代器(所以它可能没有被testing)。 顺便说一句,如果你想使用它,你将需要谷歌collections。
hibernate有4种不同的方式来实现你想要的。 每个人都有devise权衡,限制和后果。 我build议探索每一个,并决定哪一个适合你的情况。
- 使用scroll()的无状态会话
- 在每次迭代之后使用session.clear()。 当需要连接其他实体时,请在单独的会话中加载它们。 有效地,第一个会话模拟无状态会话,但保留有状态会话的所有function,直到对象被分离。
- 使用iterate()或list(),但只在第一个查询中获得id,然后在每次迭代中的一个单独的会话中,在迭代结束时执行session.load并closures会话。
- 使用Query.iterate()与EntityManager.detach()aka Session.evict();