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 Persistent 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, тому ми до цього часу дотримуємося цього, і, отже, остання сплячка, яку ми можемо використовувати, - це 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 та прокруткою. Без них виклик "прокрутки" намагається завантажити все в пам'ять і ніколи не закінчується або працює на OutOfMemoryError.


2
Вітаю, ЗДЗ, ваш звичайний випадок сканування мільйонів рядків для мене, безумовно, звичайний, і ДЯКУЮ Вам за публікацію остаточного коду. У моєму випадку я заношу записи в Solr, щоб індексувати їх для повнотекстового пошуку. І через правила бізнесу, в які я не буду входити, мені потрібно перейти через Hibernate, порівняно з просто вбудованими модулями JDBC або Solr.
Марк Беннетт

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

Здається, що з MySQL вам дійсно доведеться пройти всі ці обручі: stackoverflow.com/a/20900045/32453 (інші БД можуть бути менш жорсткими, я б міг уявити ...)
rogerdpack

32

Дійсно зробити це не можна в прямому JPA, однак Hibernate має підтримку сеансів без стану та прокручуваних наборів результатів.

Ми регулярно обробляємо мільярди рядків з її допомогою.

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


17
Дякую. Приємно знати, що хтось робить мільярди рядків через сплячку. Деякі люди тут стверджують, що це неможливо. :-)
Джордж Армхолд

2
Чи можна додати приклад і тут? Я припускаю, що це схоже на приклад Zds?
rogerdpack

19

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

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

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


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

1
Привіт Томас. У мене є маса причин скаржитися на JPA / Hibernate, але з повагою я дійсно сумніваюся, що вони "не розроблені для роботи на багатьох об'єктах". Я підозрюю, що мені просто потрібно вивчити правильну схему для цього випадку використання.
Джордж Армхолд

4
Що ж, я можу придумати лише два зразки: сторінкинки (згадувані кілька разів) та flush()/ clear(). Перший - IMHO, який не призначений для пакетної обробки, в той час, коли використовується послідовність рум'яних () / clear () запахів, як неміцна абстракція .
Томаш Нуркевич

Так, це було поєднанням сторінок і флеш / ясного, як ви згадали. Дякую!
Джордж Армхолд

7

Якщо ви використовуєте 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();  
}  

Закрити Метод

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

6
Nice jQuery об’єкт
usr-local-ΕΨΗΕΛΩΝ

Я спробував ваш код, але все-таки отримаю OOM - він видається, що всі об'єкти T (і всі об'єднані об’єкти таблиці, згадані з T) ніколи не є GC. Профілювання показує, що вони посилаються на "таблицю" в org.eclipse.persistence.internal.sesions.RepeatableWriteUnitOfWork разом з org.eclipse.persistence.internal.identitymaps.CacheKey. Я переглянув кеш-пам'ять, і мої налаштування за замовчуванням (Вимкнути вибірковий, Слабкий з м'яким підкаштом, Розмір кешу 100, Падіння недійсне). Я вивчу вимкнені сеанси і побачу, чи допомагає це. BTW Я просто повторюю курсор повернення, використовуючи "для (T o: результати)".
Еді Біс

Badum tssssssss
dctremblay

5

Це залежить від типу операції, яку ви повинні зробити. Чому ти циркулюєш понад мільйон рядків? Ви щось оновлюєте в пакетному режимі? Ви збираєтесь відображати клієнтам усі записи? Чи обчислюєте ви якусь статистику для отриманих об'єктів?

Якщо ви збираєтеся відображати мільйон записів клієнтові, перегляньте свій користувальницький інтерфейс. У цьому випадку відповідним рішенням є страгіфікація результатів та використання setFirstResult()та setMaxResult().

Якщо ви запустили оновлення великої кількості записів, вам краще буде оновити просто та користуватися Query.executeUpdate(). За бажанням, ви можете виконати оновлення в асинхронному режимі за допомогою диспетчера керованих повідомленнями робочого менеджера.

Якщо ви обчислюєте деяку статистику для отриманих об'єктів, ви можете скористатися функціями групування, визначеними специфікацією JPA.

У будь-якому іншому випадку будьте більш конкретними :)


Простіше кажучи, мені потрібно зробити щось "для кожного" ряду. Звичайно, це звичайний випадок використання. У конкретному випадку, над яким я зараз працюю, мені потрібно запитати зовнішню веб-службу, яка повністю знаходиться поза моєю базою даних, використовуючи ідентифікатор (ПК) з кожного рядка. Результати не відображаються в жодному веб-переглядачі клієнта, тому немає інтерфейсу користувача, про який можна говорити. Іншими словами, це пакетна робота.
Джордж Армхолд

Якщо вам потрібен ідентифікатор друку для кожного рядка, іншого способу, як отримати кожен рядок, отримати і надрукувати, не існує. Найкраще рішення залежить від того, що вам потрібно зробити.
Даїній

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

1
@ Йорн Хорстманн - якщо мільйони рядків, це дійсно матиме значення? Моя думка полягає в тому, що ArrayList з мільйонами об'єктів (хоча і малих) не буде корисним для купи JVM.
Джордж Армхолд

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

5

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

Інструменти ORM не розроблені для масової обробки, вони призначені для маніпулювання об'єктами та спроби зробити RDBMS, в якій зберігаються дані, бути максимально прозорими, більшість не спрацьовує в прозорій частині хоча б певною мірою. У такому масштабі немає способу обробити сотні тисяч рядків (Об'єкти), набагато менше мільйонів за допомогою будь-якої ОРМ та дозволити його виконувати в будь-який розумний проміжок часу із-за простого та простого опису об'єкта.

Скористайтеся відповідним інструментом. Прямі JDBC та зберігаються процедури, безумовно, мають місце в 2011 році, особливо в тому, що їм краще робити в порівнянні з цими рамками ORM.

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

Якщо ви дійсно затягуєте мільйони ідентифікаторів, щоб обробити, зателефонувавши до якогось веб-сервера, вам доведеться виконати одночасну обробку, щоб це запустилося в будь-яку розумну кількість часу. Перетягування курсором JDBC та розміщення декількох з них одночасно у ConcurrentLinkedQueue та невеликий пул потоків (# CPU / Cores + 1) витягують та обробляють їх - єдиний спосіб виконати завдання на машині з будь-яким " нормальний "об'єм оперативної пам'яті, враховуючи, що у вас вже не вистачає пам'яті.

Дивіться також цю відповідь .


1
Отже, ви говорите, що жодна компанія ніколи не потребує відвідування кожного ряду своїх користувачів? Їх програмісти просто викидають сплячку у вікно, коли настає час це зробити? " немає способу обробити сотні тисяч рядків " - у своєму запитанні я вказав setFirstResult / setMaxResult, тому явно є спосіб. Я запитую, чи є кращий.
Джордж Армхолд

"Витягнути мільйон будь-чого, навіть у простий Список <Integer>, не буде дуже ефективно, незалежно від того, як ви це зробите." Це саме моя думка. Я запитую, як не створити гігантський список, а скоріше повторити набір результатів.
Джордж Армхолд

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

1
Я повністю згоден з відповіддю. Найкраще рішення залежить від проблеми. Якщо проблема завантажується декількома об'єктами, то JPA добре. Якщо проблема полягає в ефективному використанні величезних обсягів даних, прямий JDBC краще.
екстранеон

4
Сканування через мільйони записів поширене з кількох причин, наприклад, їх індексація в пошуковій системі. І хоча я згоден, що JDBC зазвичай є більш прямим маршрутом, ти іноді запускаєшся в проект, який вже має дуже складну бізнес-логіку, згруповану в шарі сплячки. Якщо обійти його і перейти до JDBC, ви обійдете бізнес-логіку, яку іноді нетривіально повторно впроваджувати та підтримувати. Коли люди розміщують питання щодо нетипових випадків використання, вони часто знають, що це трохи дивно, але вони можуть успадковувати щось із будівництва з нуля, і, можливо, не можуть розкрити деталі.
Марк Беннетт

4

Можна скористатися ще одним «трюком». Завантажте лише колекцію ідентифікаторів об'єктів, які вас цікавлять. Скажімо, ідентифікатор типу long = 8 байт, тоді 10 ^ 6 список таких ідентифікаторів становить близько 8 Мбіт. Якщо це пакетний процес (по одному екземпляру за один раз), то це можна переносити. Потім просто повторіть і зробіть роботу.

Ще одне зауваження - у будь-якому випадку вам слід це робити шматками - особливо якщо ви змінюєте записи, інакше сегмент відкату в базі даних буде рости.

Що стосується встановлення стратегії firstResult / maxRows - вона буде ДУЖЕ ДУЖЕ повільною для результатів, далеких від верху.

Також врахуйте, що база даних, ймовірно, працює в ізоляції , зчитуваної з читанням , щоб уникнути фантомного зчитування ідентифікаторів навантаження, а потім завантажувати об'єкти по одному (або 10 на 10 чи будь-що інше).


Привіт @Marcin, чи можете ви чи хтось інший надати посилання на приклад коду, застосовуючи цей пошаговий підхід з першим ідентифікатором, бажано, використовуючи потоки Java8?
krevelen

2

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

Економія продуктивності тут значна, можливо, на порядок швидше, ніж все, що ви могли зробити на землі JPA / Hibernate / AppServer, і ваш сервер баз даних, швидше за все, матиме свій власний тип курсору механізму для ефективної обробки великих наборів результатів. Економія продуктивності полягає в тому, що не потрібно пересилати дані з сервера баз даних на сервер додатків, де ви обробляєте дані, а потім пересилаєте їх назад.

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


1
-2 downvotes - будь-який наступний downvoter, будь ласка, захистить ваш downvote?
Небезпека

1
Я думав те саме, читаючи це. Питання вказує на велику пакетну роботу без інтерфейсу. Якщо припустити, що вам не потрібні певні ресурси сервера додатків, навіщо взагалі використовувати сервер додатків? Збережена процедура була б набагато ефективнішою.
jdessey

@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

Використовуйте Paginationконцепцію для отримання результату


4
Пагинація дуже хороша для графічного інтерфейсу. Але для обробки величезної кількості даних ScrollableResultSet був винайдений давно. Це просто не в JPA.
екстранеон

0

Я сам це задумався. Здається, має значення:

  • наскільки великий ваш набір даних (рядки)
  • яку реалізацію 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, якщо ви хочете ним користуватися.


Щодо "яку обробку ви робите для кожного ряду" - якщо кількість рядків у мільйонах, я підозрюю, що навіть простий об'єкт із стовпцем ідентифікатора буде створювати проблеми. Я теж думав над тим, щоб написати власний ітератор, який завершив setFirstResult / setMaxResult, але я зрозумів, що це має бути поширеною (і, сподіваюся, вирішеною!) Проблемою.
Джордж Армхолд

@Caffeine Coma Я опублікував свій ітератор, ймовірно, ви могли б зробити ще кілька пристосувань JPA до нього. Скажіть, чи допоможе це. Я в кінцевому рахунку не використовував (зробив findAll).
Адам Гент

0

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

  1. Використовувати сеанс без стану за допомогою прокрутки ()
  2. Використовуйте session.clear () після кожної ітерації. Коли інші об'єкти потрібно приєднати, завантажте їх в окремий сеанс. фактично перший сеанс імітує сеанс без стану, але зберігає всі особливості стану, що належить до стану, поки об'єкти не відокремлюються.
  3. Використовуйте iterate () або list (), але отримуйте лише ідентифікатори в першому запиті, потім в окремому сеансі в кожній ітерації, виконайте session.load та закрийте сеанс в кінці ітерації.
  4. Використовуйте Query.iterate () з EntityManager.detach () aka Session.evict ();

0

Ось простий, прямий приклад JPA (у Котліні), який показує, як можна додати до сторінки довільно великий набір результатів, читаючи шматки 100 елементів одночасно, не використовуючи курсор (кожен курсор споживає ресурси в базі даних). Він використовує розбиття набору клавіш.

Дивіться https://use-the-index-luke.com/no-offset щодо концепції пагінації клавіш та https://www.citusdata.com/blog/2016/03/30/five-ways-to- пагінат / для порівняння різних способів хворобливості разом з їх недоліками.

/*
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.