Как обращаться с большим набором данных с JPA (или, по крайней мере, с Hibernate)?

Мне нужно, чтобы мое веб-приложение работало с действительно огромными наборами данных. На данный момент я получаю либо OutOfMemoryException, либо вывод, который генерируется 1-2 минуты.

Пусть это просто и предположим, что у нас есть две таблицы в БД: Worker и WorkLog с примерно 1000 строк в первом и 10 000 000 строк во втором. В последнем столе есть несколько полей, включая поля "workerId" и "hoursWorked". Нам нужно:

  • подсчитывает общее количество часов, затрачиваемых каждым пользователем;

  • список рабочих периодов для каждого пользователя.

Самый простой подход (IMO) для каждой задачи в обычном SQL:

1)

select Worker.name, sum(hoursWorked) from Worker, WorkLog 
   where Worker.id = WorkLog.workerId 
   group by Worker.name;

//results of this query should be transformed to Multimap<Worker, Long>

2)

select Worker.name, WorkLog.start, WorkLog.hoursWorked from Worker, WorkLog
   where Worker.id = WorkLog.workerId;

//results of this query should be transformed to Multimap<Worker, Period>
//if it was JDBC then it would be vitally 
//to set resultSet.setFetchSize (someSmallNumber), ~100

Итак, у меня есть два вопроса:

  • как реализовать каждый из моих подходов с JPA (или, по крайней мере, с Hibernate);
  • Как бы вы справились с этой проблемой (с JPA или Hibernate, конечно)?

Ответ 1

предположим, что у нас есть две таблицы в DB: Worker и WorkLog с примерно 1000 строк в первом и 10 000 000 строк во втором

Для больших объемов, как это, моя рекомендация будет заключаться в использовании StatelessSession interface из Hibernate:

В качестве альтернативы, Hibernate предоставляет API, ориентированный на команду, который можно использовать для потоковой передачи данных в и из базы данных в виде отдельных объекты. A StatelessSession не имеет контекст персистентности, связанный с ним и не обеспечивает многие из семантика жизненного цикла более высокого уровня. В в частности, сеанс без гражданства не реализовать кеш первого уровня и взаимодействовать с любым уровнем второго уровня или кеш запросов. Он не реализует транзакционная запись или автоматическая грязная проверка. операции выполняется с использованием сеанса без состояния никогда не каскад к связанным экземплярам. Коллекции игнорируются лицами без гражданства сессия. Операции, выполняемые через сеанс обхода сеанса спящего режима для спящего режима Hibernate модели событий и перехватчиков. Из-за отсутствие кеша первого уровня, Сеансы без гражданства уязвимы для эффекты сглаживания данных. Без гражданства session - это абстракция более низкого уровня что намного ближе к JDBC.

StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();

ScrollableResults customers = session.getNamedQuery("GetCustomers")
    .scroll(ScrollMode.FORWARD_ONLY);
while ( customers.next() ) {
    Customer customer = (Customer) customers.get(0);
    customer.updateStuff(...);
    session.update(customer);
}

tx.commit();
session.close();

В этом примере кода Customerэкземпляры, возвращаемые запросом, сразу снят. Они никогда связанных с любым сохранением контекст.

insert(), update() и delete() операции, определенные Интерфейс StatelessSessionсчитается прямой базой данных операции на уровне строки. Они приводят к немедленное выполнение SQL INSERT, UPDATE или DELETEсоответственно. У них разные семантики к save(), saveOrUpdate() и delete()операции, определенные Sessionинтерфейс.

Ответ 2

Кажется, вы тоже можете это сделать с EclipseLink. Проверьте это: http://wiki.eclipse.org/EclipseLink/Examples/JPA/Pagination:

Query query = em.createQuery...
query.setHint(QueryHints.CURSOR, true)
     .setHint(QueryHints.SCROLLABLE_CURSOR, true)
ScrollableCursor scrl = (ScrollableCursor)q.getSingleResult();
Object o = null;
while ((o = scrl.next()) != null) { ... }

Ответ 3

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

Ответ 4

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

Однако, если у вас есть большой результирующий набор, который не может быть обработан в базе данных, вам действительно нужно просто использовать простой JDBC, поскольку JPA (стандарт) не поддерживает потоковую передачу больших наборов данных.

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

Ответ 5

Я использую что-то подобное, и он работает очень быстро. Я также ненавижу использовать собственный SQL, поскольку наше приложение должно работать с любой базой данных.

Привязка возвращает в очень оптимизированный sql и возвращает список записей, которые являются картами.

String hql = "select distinct " +
            "t.uuid as uuid, t.title as title, t.code as code, t.date as date, t.dueDate as dueDate, " +
            "t.startDate as startDate, t.endDate as endDate, t.constraintDate as constraintDate, t.closureDate as closureDate, t.creationDate as creationDate, " +
            "sc.category as category, sp.priority as priority, sd.difficulty as difficulty, t.progress as progress, st.type as type, " +
            "ss.status as status, ss.color as rowColor, (p.rKey || ' ' || p.name) as project, ps.status as projectstatus, (r.code || ' ' || r.title) as requirement, " +
            "t.estimate as estimate, w.title as workgroup, o.name || ' ' || o.surname as owner, " +
            "ROUND(sum(COALESCE(a.duration, 0)) * 100 / case when ((COALESCE(t.estimate, 0) * COALESCE(t.progress, 0)) = 0) then 1 else (COALESCE(t.estimate, 0) * COALESCE(t.progress, 0)) end, 2) as factor " +
            "from " + Task.class.getName() + " t " +
            "left join t.category sc " +
            "left join t.priority sp " +
            "left join t.difficulty sd " +
            "left join t.taskType st " +
            "left join t.status ss " +
            "left join t.project p " +
            "left join t.owner o " +
            "left join t.workgroup w " +
            "left join p.status ps " +
            "left join t.requirement r " +
            "left join p.status sps " +
            "left join t.iterationTasks it " +
            "left join t.taskActivities a " +
            "left join it.iteration i " +
            "where sps.active = true and " +
            "ss.done = false and " +
            "(i.uuid <> :iterationUuid or it.uuid is null) " + filterHql +
            "group by t.uuid, t.title, t.code, t.date, t.dueDate, " +
            "t.startDate, t.endDate, t.constraintDate, t.closureDate, t.creationDate, " +
            "sc.category, sp.priority, sd.difficulty, t.progress, st.type, " +
            "ss.status, ss.color, p.rKey, p.name, ps.status, r.code, r.title, " +
            "t.estimate, w.title, o.name, o.surname " + sortHql;

    if (logger.isDebugEnabled()) {
        logger.debug("Executing hql: " + hql );
    }

    Query query =  hibernateTemplate.getSessionFactory().getCurrentSession().getSession(EntityMode.MAP).createQuery(hql);
    for(String key: filterValues.keySet()) {
        Object valueSet = filterValues.get(key);

        if (logger.isDebugEnabled()) {
            logger.debug("Setting query parameter for " + key );
        }

        if (valueSet instanceof java.util.Collection<?>) {
            query.setParameterList(key, (Collection)filterValues.get(key));
        } else {
            query.setParameter(key, filterValues.get(key));
        }
    }       
    query.setString("iterationUuid", iteration.getUuid());
    query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);

    if (logger.isDebugEnabled()) {
        logger.debug("Query building complete.");
        logger.debug("SQL: " + query.getQueryString());
    }

    return query.list();

Ответ 6

Я согласен с тем, что делать расчет на сервере базы данных - это ваш лучший вариант в конкретном случае, о котором вы упомянули. HQL и JPAQL могут обрабатывать оба этих запроса:

1)

select w, sum(wl.hoursWorked) 
from Worker w, WorkLog wl
where w.id = wl.workerId 
group by w

или, если ассоциация сопоставлена:

select w, sum(wl.hoursWorked) 
from Worker w join w.workLogs wl
group by w

оба или которые возвращают вам список, где Object [] s являются Рабочим и Длинные. Или вы также можете использовать запросы "динамической инстанцирования", чтобы обернуть это, например:

select new WorkerTotal( select w, sum(wl.hoursWorked) )
from Worker w join w.workLogs wl
group by w

или (в зависимости от необходимости), вероятно, даже просто:

select new WorkerTotal( select w.id, w.name, sum(wl.hoursWorked) )
from Worker w join w.workLogs wl
group by w.id, w.name

WorkerTotal - это просто класс. Он должен иметь соответствующий конструктор (ы).

2)

select w, new Period( wl.start, wl.hoursWorked )
from Worker w join w.workLogs wl

это вернет вам результат для каждой строки в таблице WorkLog... Бит new Period(...) называется "динамическим экземпляром" и используется для переноса кортежей из результата в объекты (более легкое потребление).

Для манипуляции и общего использования я рекомендую StatelessSession, как указывает Паскаль.

Ответ 7

Существует несколько методов, которые могут использоваться совместно с другими для создания и обработки запросов для больших наборов данных, где память является ограничением:

  • Использовать setFetchSize (некоторое значение, возможно, 100+) по умолчанию (через JDBC) - 10. Это больше о производительности и является одним из самых больших связанных факторов. Может быть сделано в JPA, используя queryHint, доступный от поставщика (Hibernate и т.д.). По какой-либо причине нет (по какой-либо причине) метода JPA Query.setFetchSize(int).
  • Не пытайтесь сортировать весь набор результатов для записей 10K +. Применяется несколько стратегий: для графических интерфейсов используйте пейджинг или фреймворк, который выполняет пейджинг. Рассмотрим Lucene или коммерческие поисковые/индексирующие двигатели (Endeca, если у компании есть деньги). Для отправки данных где-нибудь, поместите его и очистите буфер каждые N записей, чтобы ограничить, сколько памяти используется. Поток может быть сброшен в файл, сеть и т.д. Помните, что под ними JPA использует JDBC, а JDBC хранит результирующий набор на сервере, а только выбирает N-строки в группе с набором строк за раз. Это разложение можно манипулировать, чтобы облегчить сбор данных в группах.
  • Рассмотрим, что такое прецедент. Как правило, приложение пытается ответить на вопросы. Когда ответ будет сорняться через строки 10K +, тогда проект должен быть пересмотрен. Опять же, подумайте об использовании движков индексирования, таких как Lucene, уточните запросы, подумайте о том, как использовать BloomFilters, включая контрольные кеши, чтобы найти иглы в стоге сена, не переходя в базу данных и т.д.