JPA: каков правильный шаблон для итерации по большим наборам результатов?

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

Например, я подозреваю, что следующее число будет взорвано, если таблица большая:

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

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

Является ли pagination (циклическое и ручное обновление setFirstResult()/setMaxResult()) действительно лучшим решением?

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

Ответ 1

Страница 537 of Сохранение Java с 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();
    }
}

Ответ 2

Я попробовал ответы, представленные здесь, но 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 и прокруткой. Без них вызов "прокрутки" пытается загрузить все в память и либо никогда не заканчивается, либо работает с OutOfMemoryError.

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

Если вы используете 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();  
}  

close Метод

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

Ответ 6

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

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

Если вы запустили обновление большого количества записей, вам лучше сохранить обновление простым и использовать Query.executeUpdate(). При желании вы можете выполнить обновление в асинхронном режиме с помощью Message-Driven Bean o Work Manager.

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

Для любого другого случая, пожалуйста, будьте более конкретными:)

Ответ 7

Нет "правильного", что делать, это не то, что предназначено для 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) вытащить и обработать их - это единственный способ выполнить вашу задачу на машине с любым "нормальным" объемом ОЗУ, поскольку у вас уже не хватает памяти.

См. этот ответ.

Ответ 8

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

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

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

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

Ответ 9

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

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

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

Ответ 10

Развернуть на @Tomasz Nurkiewicz ответ. У вас есть доступ к DataSource, который, в свою очередь, может предоставить вам соединение

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

В вашем коде есть

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

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

Ответ 11

Я сам это задался. Кажется, имеет значение:

  • насколько велик ваш набор данных (строки)
  • какая реализация 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();
    }

}

Я закончил тем, что не использовал мой итератор chunk (так что, возможно, это не было проверено). Кстати, вам понадобятся сборники google, если вы хотите их использовать.

Ответ 12

Использовать Pagination Концепция для получения результата

Ответ 13

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

  • Использовать сеанс без состояния с прокруткой()
  • Используйте session.clear() после каждой итерации. Когда необходимо присоединить другие объекты, загрузите их в отдельный сеанс. фактически первый сеанс эмулирует сессию без сохранения состояния, но сохраняет все функции сеанса с сохранением состояния, пока объекты не будут отсоединены.
  • Использовать iterate() или list(), но получить только идентификаторы в первом запросе, затем в отдельном сеансе на каждой итерации, запустите session.load и закройте сеанс в конце итерации.
  • Использовать Query.iterate() с EntityManager.detach() aka Session.evict();