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()) - лучшее решение?

Изменить : основной вариант использования, на который я нацелен, - это своего рода пакетное задание. Ничего страшного, если на запуск уходит много времени. Веб-клиент не задействован; Мне просто нужно «что-то сделать» для каждой строки, по одной (или нескольким маленьким N) за раз. Я просто стараюсь не хранить их все в памяти одновременно.


Какую базу данных и драйвер JDBC вы используете?

Ответы:


55

Страница 537 Java Persistence with Hibernate дает решение с использованием 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.

Добавление пары дополнительных параметров выполнило свою работу, и такой код работает без 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
Привет, Zds, ваш вариант использования сканирования миллионов строк, безусловно, обычен для меня, и СПАСИБО за публикацию окончательного кода. В моем случае я вставляю записи в Solr, чтобы проиндексировать их для полнотекстового поиска. И из-за бизнес-правил, которые я не буду вдаваться в подробности, мне нужно использовать Hibernate, а не просто использовать встроенные модули JDBC или Solr.
Марк Беннетт

Рад был помочь :-). Мы также имеем дело с большими наборами данных, в этом случае позволяя пользователю запрашивать все названия улиц в одном городе / округе или иногда даже в штате, поэтому создание индексов требует чтения большого количества данных.
Zds

Похоже, что с MySQL вам действительно нужно пройти через все эти обручи: stackoverflow.com/a/20900045/32453 (другие БД могут быть менее строгими, я полагаю ...)
rogerdpack 01

32

Вы не можете сделать это в прямом JPA, однако Hibernate поддерживает сеансы без сохранения состояния и прокручиваемые наборы результатов.

Мы регулярно обрабатываем с его помощью миллиарды строк.

Вот ссылка на документацию: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession


17
Спасибо. Приятно знать, что кто-то выполняет миллиарды строк через Hibernate. Некоторые здесь утверждают, что это невозможно. :-)
Джордж Армхолд

2
Можно ли добавить сюда пример? Полагаю, это похоже на пример Zds?
rogerdpack 01

19

Честно говоря, я бы посоветовал оставить JPA и придерживаться JDBC (но, конечно, использовать JdbcTemplateкласс поддержки или что-то подобное). JPA (и другие поставщики / спецификации ORM) не предназначены для работы со многими объектами в рамках одной транзакции, поскольку они предполагают, что все загруженное должно оставаться в кэше первого уровня (отсюда и необходимость вclear() в JPA).

Также я рекомендую более низкоуровневое решение, потому что накладные расходы на ORM (отражение - это только верхушка айсберга) могут быть настолько значительными, что итерация по простому ResultSet, даже с использованием некоторой облегченной поддержки, как упомянуто, JdbcTemplateбудет намного быстрее.

JPA просто не предназначен для выполнения операций с большим количеством сущностей. Вы можете играть с flush()/, clear()чтобы избежать OutOfMemoryError, но подумайте об этом еще раз. Вы получаете очень мало, расплачиваясь за огромное потребление ресурсов.


Преимущество JPA заключается не только в том, что он не зависит от базы данных, но и в возможности даже не использовать традиционную базу данных (NoSQL). Время от времени выполнять промывку / очистку несложно, и обычно пакетные операции выполняются нечасто.
Адам Гент

1
Привет, Томас. У меня есть множество причин жаловаться на JPA / Hibernate, но, с уважением, я действительно сомневаюсь, что они «не предназначены для работы со многими объектами». Я подозреваю, что мне просто нужно изучить правильный шаблон для этого варианта использования.
Джордж Армхолд

4
Ну, я могу думать только о двух шаблонах: нумерации страниц (упоминалось несколько раз) и flush()/ clear(). Первый - IMHO, не предназначенный для целей пакетной обработки, а использование последовательности flush () / clear () пахнет дырявой абстракцией .
Tomasz Nurkiewicz

Да, это была комбинация разбивки на страницы и сброса / очистки, как вы упомянули. Спасибо!
Джордж Армхолд,

7

Если вы используете EclipseLink I, используя этот метод, чтобы получить результат как 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-local-ΕΨΗΕΛΩΝ 06

Я попробовал ваш код, но все же получил OOM - похоже, все объекты T (и все объекты соединенных таблиц, упомянутые из T) никогда не являются GC. Профилирование показывает, что на них ссылаются из «таблицы» в org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork вместе с org.eclipse.persistence.internal.identitymaps.CacheKey. Я заглянул в кеш, и все мои настройки установлены по умолчанию (Disable Selective, Weak with Soft Subcache, Cache Size 100, Drop Invalidate). Я посмотрю, как отключить сеансы, и посмотрю, поможет ли это. Кстати, я просто перебираю курсор возврата, используя «for (T o: results)».
Edi Bice

Badum tssssssss
dctremblay

5

Это зависит от типа операции, которую вы должны выполнить. Почему вы зацикливаете более миллиона строк? Вы что-то обновляете в пакетном режиме? Вы собираетесь показывать клиенту все записи? Вы подсчитываете статистику по найденным объектам?

Если вы собираетесь отображать миллион записей для клиента, пересмотрите свой пользовательский интерфейс. В этом случае подходящим решением является разбивка результатов на страницы и использование setFirstResult()и setMaxResult().

Если вы запустили обновление большого количества записей, вам лучше сделать его простым и удобным Query.executeUpdate(). При желании вы можете выполнить обновление в асинхронном режиме, используя управляемый сообщениями компонент или Диспетчер работ.

Если вы вычисляете некоторую статистику по извлеченным объектам, вы можете воспользоваться функциями группировки, определенными спецификацией JPA.

В любом другом случае просьба уточнить :)


Проще говоря, мне нужно что-то делать «для каждой» строки. Конечно, это обычный вариант использования. В конкретном случае, над которым я сейчас работаю, мне нужно запросить внешнюю веб-службу, которая находится полностью за пределами моей базы данных, используя идентификатор (PK) из каждой строки. Результаты не отображаются ни в одном клиентском веб-браузере, поэтому нет пользовательского интерфейса, о котором можно было бы говорить. Другими словами, это пакетная работа.
Джордж Армхолд

Если вам «нужен» идентификатор печати для каждой строки, нет другого способа получить каждую строку, получить идентификатор и распечатать. Лучшее решение зависит от того, что вам нужно делать.
Dainius

@Caffeine Coma, если вам нужен только идентификатор каждой строки, тогда наибольшее улучшение, вероятно, будет связано только с выборкой этого столбца, а SELECT m.id FROM Model mзатем итерацией по List <Integer>.
Йорн Хорстманн

1
@ Jörn Horstmann, будет ли это иметь значение, если строк будут миллионы? Я хочу сказать, что список ArrayList с миллионами объектов (пусть даже небольших) не годится для кучи JVM.
Джордж Армхолд

@Dainius: мой вопрос действительно таков: «как я могу перебирать каждую строку, не имея в памяти всего ArrayList?» Другими словами, мне нужен интерфейс для извлечения N за раз, где N значительно меньше 1 миллиона. :-)
Джордж Армхолд

5

Нет "правильного", что это делать, это не то, для чего предназначены JPA, JDO или любой другой ORM, прямой JDBC будет вашей лучшей альтернативой, так как вы можете настроить его для возврата небольшого количества строк в время и сбрасывайте их по мере использования, поэтому курсоры на стороне сервера существуют.

Инструменты ORM не предназначены для массовой обработки, они предназначены для того, чтобы вы могли манипулировать объектами и пытаться сделать РСУБД, в которой хранятся данные, максимально прозрачной, большинство из которых терпят неудачу в прозрачной части, по крайней мере, в некоторой степени. В этом масштабе невозможно обработать сотни тысяч строк (объектов), не говоря уже о миллионах, с помощью любого ORM и заставить его выполняться в любое разумное время из-за накладных расходов на создание экземпляров объекта, простых и простых.

Используйте соответствующий инструмент. Прямые JDBC и хранимые процедуры определенно найдут свое место в 2011 году, особенно в том, что они делают лучше, чем эти структуры ORM.

Втянуть миллион чего угодно, даже в простое List<Integer>, не будет очень эффективно, независимо от того, как вы это делаете. Правильный способ сделать то, о чем вы просите, - простой SELECT id FROM table, установить SERVER SIDE(зависит от поставщика), установить курсор FORWARD_ONLY READ-ONLYи перебрать его.

Если вы действительно запрашиваете миллионы идентификаторов для обработки, вызывая для каждого из них какой-то веб-сервер, вам также придется выполнить некоторую параллельную обработку, чтобы это работало в разумное время. Вытягивание курсором JDBC и размещение нескольких из них одновременно в ConcurrentLinkedQueue, а также получение и обработка небольшого пула потоков (# CPU / Cores + 1) - единственный способ завершить вашу задачу на машине с любым " нормальный "объем ОЗУ, учитывая, что у вас уже заканчивается память.

См. Также этот ответ .


1
То есть вы утверждаете, что ни одной компании никогда не нужно просматривать каждую строку таблицы пользователей? Их программисты просто выбросят Hibernate из окна, когда придет время это сделать? « Нет никакого способа , чтобы обрабатывать сотни тысяч строк » - на мой вопрос , который я указал setFirstResult / setMaxResult, так ясно есть это путь. Спрашиваю, есть ли лучше.
Джордж Армхолд

«Вытягивание миллиона чего угодно, даже в простой List <Integer>, не будет очень эффективным, независимо от того, как вы это делаете». Это именно моя точка зрения. Я спрашиваю, как не создавать гигантский список, а как перебирать набор результатов.
Джордж Армхолд

Используйте простой прямой оператор выбора JDBC с FORWARD_ONLY READ_ONLY с курсором SERVER_SIDE, как я предложил в своем ответе. Как заставить JDBC использовать курсор SERVER_SIDE зависит от драйвера базы данных.

1
Полностью согласен с ответом. Лучшее решение зависит от проблемы. Если проблема заключается в простой загрузке нескольких объектов, JPA - это хорошо. Если проблема заключается в эффективном использовании огромных объемов данных, лучше использовать JDBC.
extraneon

4
Сканирование миллионов записей является обычным явлением по ряду причин, например, для их индексации в поисковой системе. И хотя я согласен с тем, что JDBC обычно является более прямым путем, иногда вы заходите в проект, который уже имеет очень сложную бизнес-логику, объединенную на уровне Hibernate. Если вы обойдете его и перейдете к JDBC, вы обойдете бизнес-логику, которую иногда нетривиально повторно реализовать и поддерживать. Когда люди задают вопросы о нетипичных вариантах использования, они часто понимают, что это немного странно, но они могут наследовать что-то, а не строить с нуля, и, возможно, не могут раскрыть детали.
Марк Беннетт

4

Можно использовать еще один «трюк». Загружать только набор идентификаторов интересующих вас объектов. Скажем, идентификатор имеет тип long = 8bytes, тогда 10 ^ 6, список таких идентификаторов составляет около 8Мб. Если это пакетный процесс (по одному экземпляру за раз), то это терпимо. Затем просто повторите и сделайте свою работу.

Еще одно замечание - вы все равно должны делать это кусками - особенно если вы изменяете записи, иначе сегмент отката в базе данных будет расти.

Когда дело доходит до установки стратегии firstResult / maxRows - она ​​будет ОЧЕНЬ ОЧЕНЬ медленной для результатов, далеких от вершины.

Также примите во внимание, что база данных, вероятно, работает в изолированном режиме чтения , чтобы избежать фантомного чтения идентификаторов загрузки, а затем загружать объекты один за другим (или 10 на 10 или что-то еще).


Привет, @Marcin, не могли бы вы или кто-нибудь еще предоставить ссылку на пример кода, применяющего этот поэтапный подход с разбиением на части и сначала по идентификатору, предпочтительно с использованием потоков Java8?
Кревелен

2

Я был удивлен, увидев, что использование хранимых процедур не было более заметным в ответах здесь. Раньше, когда мне приходилось делать что-то подобное, я создавал хранимую процедуру, которая обрабатывала данные небольшими порциями, затем ненадолго засыпала, а затем продолжала. Причина спящего режима заключается в том, чтобы не перегружать базу данных, которая предположительно также используется для запросов в более реальном времени, таких как подключение к веб-сайту. Если базу данных больше никто не использует, то вы можете не спать. Если вам нужно убедиться, что вы обрабатываете каждую запись один раз и только один раз, вам нужно будет создать дополнительную таблицу (или поле) для хранения, какие записи вы обработали, чтобы обеспечить устойчивость при перезапусках.

Экономия производительности здесь значительна, возможно, на порядки быстрее, чем все, что вы могли бы сделать в мире JPA / Hibernate / AppServer, и ваш сервер базы данных, скорее всего, будет иметь собственный механизм курсора на стороне сервера для эффективной обработки больших наборов результатов. Снижение производительности связано с тем, что вам не нужно отправлять данные с сервера базы данных на сервер приложений, где вы обрабатываете данные, а затем отправляете их обратно.

У использования хранимых процедур есть некоторые существенные недостатки, которые могут полностью исключить это для вас, но если у вас есть этот навык в своем личном наборе инструментов и вы можете использовать его в такой ситуации, вы можете довольно быстро избавиться от таких вещей. .


1
-2 отрицательных голоса - не мог бы следующий отрицательный голос защитить ваш отрицательный голос?
Danger

1
Я думал о том же, читая эти строки. Вопрос указывает на пакетное задание большого объема без пользовательского интерфейса. Если предположить, что вам не нужны ресурсы, специфичные для сервера приложений, зачем вообще использовать сервер приложений? Хранимая процедура была бы намного эффективнее.
jdessey 08

@jdessey В зависимости от ситуации, допустим, у нас есть средство импорта, где при импорте он должен что-то делать с какой-то другой частью системы, например, добавлять строки в другую таблицу на основе некоторых бизнес-правил, которые уже были закодированы как EJB. Тогда запуск на сервере приложений будет иметь больше смысла, если вы не можете заставить EJB работать во встроенном режиме.
Архимед Траяно,

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

Используйте PaginationConcept для получения результата


4
Пагинация очень хороша для графического интерфейса. Но для обработки огромных объемов данных ScrollableResultSet был изобретен очень давно. Это просто не в JPA.
extraneon

0

Я сам задавался вопросом. Вроде имеет значение:

  • насколько велик ваш набор данных (строки)
  • какую реализацию JPA вы используете
  • какую обработку вы выполняете для каждой строки.

Я написал Iterator, чтобы упростить замену обоих подходов (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();
    }

}

В итоге я не использовал свой итератор фрагментов (так что, возможно, он не был протестирован). Кстати, вам понадобятся коллекции Google, если вы хотите его использовать.


Что касается «какой обработки вы выполняете для каждой строки» - если количество строк исчисляется миллионами, я подозреваю, что даже простой объект с одним столбцом id вызовет проблемы. Я тоже думал о написании своего собственного Iterator, который обернул бы setFirstResult / setMaxResult, но решил, что это должна быть общая (и, надеюсь, решенная!) Проблема.
Джордж Армхолд

@Caffeine Coma Я опубликовал свой Iterator, вы, вероятно, могли бы сделать еще немного JPA, адаптируясь к нему. Подскажите, поможет ли. В итоге я не использовал (нашел все).
Адам Гент

0

С гибернацией есть 4 разных способа добиться желаемого. У каждого есть компромиссы в дизайне, ограничения и последствия. Я предлагаю изучить каждый и решить, какой из них подходит для вашей ситуации.

  1. Используйте сеанс без сохранения состояния с помощью scroll ()
  2. Используйте session.clear () после каждой итерации. Когда необходимо присоединить другие объекты, загрузите их в отдельном сеансе. фактически первый сеанс имитирует сеанс без сохранения состояния, но сохраняет все функции сеанса с сохранением состояния до тех пор, пока объекты не будут отсоединены.
  3. Используйте iterate () или list (), но получите только идентификаторы в первом запросе, затем в отдельном сеансе на каждой итерации, выполните session.load и закройте сеанс в конце итерации.
  4. Используйте Query.iterate () с EntityManager.detach () aka 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, получающими каждый раз размер элементов с использованием смещений

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;
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.