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


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

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

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

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

это разбиение на страницы (цикл и обновление вручную setFirstResult()/setMaxResult()) действительно лучшее решение?

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

13 102

13 ответов:

страница 537 из Java Persistence с Hibernate дает решение с помощью 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();
    }
}

я попробовал ответы, представленные здесь, но JBoss 5.1 + MySQL Connector/J 5.1.15 + Hibernate 3.3.2 не работал с ними. Мы только что мигрировали из JBoss 4.x для JBoss 5.1, поэтому мы застряли с ним на данный момент, и поэтому последний спящий режим, который мы можем использовать, - 3.3.2.

добавление нескольких дополнительных параметров выполнило эту работу, и такой код работает без Ooms:

        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

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

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

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

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

close метод

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

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

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

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

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

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

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

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

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

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

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

посмотреть этот ответ как хорошо.

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

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

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

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

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

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

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

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

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

в коде у вас есть

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

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

использовать

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

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

}

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

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

  1. используйте сеанс без сохранения состояния со scroll ()
  2. использовать сессии.очистить() после каждой итерации. Когда другие объекты должны быть присоединены, а затем загрузить их в отдельном сеансе. фактически первый сеанс эмулирует сеанс без сохранения состояния, но сохраняет все особенности сеанса с отслеживанием состояния, пока объекты не будут отсоединены.
  3. используйте iterate() или list (), но получите только идентификаторы в первом запросе, а затем в отдельном сеансе в каждой итерации выполните сеанс.загрузите и закройте сеанс в конце итерации.
  4. Запрос Использовать.iterate () с помощью EntityManager.отсоединить () aka сессии.выселить();