JPA: каков правильный шаблон для перебора больших наборов результатов? - PullRequest
109 голосов
/ 21 февраля 2011

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

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

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

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

Является ли нумерация страниц (циклическое обновление и обновление вручную setFirstResult() / setMaxResult()) действительно лучшим решением?* Редактировать : основной вариант использования, на который я нацеливаюсь, является своего рода пакетной работой.Это хорошо, если это займет много времени, чтобы бежать.Там нет веб-клиента;Мне просто нужно «сделать что-то» для каждого ряда, по одному (или небольшому N) за раз.Я просто стараюсь не хранить их все в памяти одновременно.

Ответы [ 13 ]

53 голосов
/ 22 февраля 2011

Страница 537 из Сохранение 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();
    }
}
34 голосов
/ 03 марта 2011

Я попробовал ответы, представленные здесь, но JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.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.

29 голосов
/ 22 февраля 2011

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

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

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

17 голосов
/ 21 февраля 2011

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

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

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

7 голосов
/ 05 февраля 2013

Если вы используете EclipseLink, я использую этот метод для получения результата как итерируемого

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();
    }
}
5 голосов
/ 21 февраля 2011

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

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

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

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

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

4 голосов
/ 21 февраля 2011

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

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

Используйте соответствующий инструмент.Простые JDBC и хранимые процедуры определенно имеют место в 2011 году, особенно в том, что они умеют делать лучше по сравнению с этими средами ORM.эффективно независимо от того, как вы это делаете.Правильный способ сделать то, что вы просите, - это простой SELECT id FROM table, установить SERVER SIDE (зависит от поставщика) и курсор на FORWARD_ONLY READ-ONLY и выполнить итерацию.

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

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

3 голосов
/ 16 октября 2013

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

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

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

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

1 голос
/ 01 октября 2015

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

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

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

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

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

1 голос
/ 01 января 2013

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...