Pull to refresh

Comments 23

А почему бы не сделать VIEW, в которую заложить расчёт и работать уже с этими данными как с единой таблицей?

<sarcasm> потому что это перенос бизнес логики в базу, код ревью не пройдет </sarcasm>
Вообще жесть какая-то, что для простейшей в общем-то задачи надо разбираться в кишках hibernate.

дырявые абстракции, что поделать

  1. По архитектуре может не быть возможности хранить бизнес-логику в БД - по многим причинам.

  2. Даже если использовать VIEW, без расчётов - вы просто перенесёте JOINы из вашего приложения в данный VIEW. Что также не решит проблему. В примере из поста (рассматриваю крайний случай), необходимо получить данные из двадцати зависимых таблиц. В каждой из таблиц есть две записи для одного клиента. В результате SQL запроса для получения одного клиента с зависимыми данными вы получите декартово произведение - миллион строк, которые необходимо передать по сети, переварить в приложении и преобразовать в один объект!

    Если же вы будете использовать BatchSize в результате SQL запросов вы получите 41 строку которая преобразуется в один объект, с меньшей нагрузкой на БД приложение и сеть.

нет, все не так.

Во view вы можете использовать человеческий sql запрос с inner select (subselect) и cte

А неважно что вы там использовать будете — всё равно пагинацию придётся "навешивать" поверх непонятно каким образом, да и от декартова произведения в одном-единственном VIEW никуда не деться.


Если уж и выносить логику выборки в БД — то в хранимую процедуру, но никак не во VIEW.


Но, вообще-то, такие вещи должна сама ORM уметь генерировать, а не заявлять "у меня лапки, сделаю всё в памяти". Может, однажды даже научится. Лет через 20.

а можно просто использовать cte и inner select, получая списки сразу как массив с нужной паджинацией как основной таблицы, так и вложенных списков.

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

На самом деле проблема более общая - декартово произведение и преобразование реляционной модели данных в объектную. Как раз таки Hibernate и позволяет ёё решить в отличии от нативных запросов.

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

подозреваю, что это будет работать бысрее всего предложенного. Но это нативный запрос.

Что-то аж загрустил...

На дворе 23 год, а я такую же проблему решал еще в далеком 2008. И толком решения нормального так и нет ни у кого.

Наоборот, стало только хуже. Теперь по канонам у вас должны быть отдельные микросервисы для клиентов и для счетов. У каждого микросервиса своя база и SELECT N+1 превращается в HTTP GET+SELECT N+1.

Действительно, как отметили в комментариях - нативный запрос в статье упомянут, но не сказано, почему это решение не подошло. Но в целом очень круто и интересно, спасибо!

Как показала практика отношения на уровне сущностей это хорошо для понимания модели данных, но какой-то изврат для выборки данных из таблиц. В своих проектах во избежание n+1 и загрузки ненужных данных используем native запросы с json_agg вложенных сущностей. Также динамические запросы собираем и выполняем через entity-manager обертку. Может быть такой подход покажется "грубым", но он хорош по производительности и более понятен разработчикам.

Мне кажется, EM излишен.

JdbcTemplate гораздо легче и проще. + можно навесить удобное логгирование

Мы на проекте, чтобы избежать всего этого, включили sping-hibernate-query-utils в режиме exception, чтобы никаких N+1 не прошло. Сейчас надежда на тесты и QA, чтобы исключение не дошло до боя.

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

Пока наше решение это одновременно и включённые батчи в пропертях глобально в режиме PADDING, и 2х запросы.

В указанном примере с Entity graph проблема декартова произведения не ушла, а только переместилась в запрос getAllByIdIn. Из-за этого и OutOfMemoryError.

Есть более эффективное решение с Entity graph/Join fetch:

  1. Запрос с пагинацией должен возвращать сущность, с непроинициализированными дочерними коллекциями.

  2. Для каждой дочерней коллекции выполняется отдельный запрос.

@Repository
interface ClientRepository : JpaRepository<ClientEntity, Long> {

    fun getAll(pageable: Pageable): Page<ClientEntity>

    @EntityGraph(attributePaths = ["accounts"])
    fun getWithAccountsByIdIn(clientIds: List<Long>): List<ClientEntity>

    @EntityGraph(attributePaths = ["deposits"])
    fun getWithDepositsByIdIn(clientIds: List<Long>): List<ClientEntity>
}

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

1. select * from client c limit 50 offset 50
2. select count(c.id) from client c
3. select * from client c left outer join account a on c.id=a.client_id where c.id in (51, 52, ...)
4. select * from client c left outer join deposit d on c.id=d.client_id where c.id in (51, 52, ...)

В большинстве случаев этот подход эффективнее, чем использование @BatchSize и нагляднее, чем использование Native SQL, особенно когда пейджинг выполняется с критериями.

Подход тоже не идеален:

  • Запросы приходится вызывать явно.

  • Лишний раз извлекаются родительские сущности вместе с дочками.

  • Код не очевидный. Результаты 3-го и 4-го запросов используются неявно. На самом деле, в родительских сущностях наполняются Lazy-коллекции.

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



а чем native sql не нагляден? В особенности при использовании to_jsonb /строительством своего jsonb и маппинг через jackson?

Объём кода больше. Тестировать сложнее.

не понимаю.

Обьем меньше, у вас 1 запрос через jdbcTemplate, маппинг одной строки через objectMapper. Вместо энтитей нормалтные DTO.

Никаких замысловатых конвертеров на уровне hibernate, никаких эвентов, никаких хранений состояния.

Согласен, если Hibernate нет, то спорить не о чем. Статья-то про специфику JPA/Hibernate. Автор поднял правильную тему, но сделал неправильный вывод, якобы @BatchSize всегда лучше, чем Entity graph/Join fetch.

Проблему с декартовым произведением видел и на чистом JDBC. Было несколько дочерних таблиц, все они join-ами извлекались одним запросом. В каждой по нескольку записей, до 30. Иногда запрос извлекал свыше 100тыс.записей из БД, когда суммарно в дочках содержалось около 200 записей.

Пагинация SpingData часто используется с критериями поиска. Не думаю, что есть тривиальная возможность совместить спецификации SpringData либо JPA CriteriaAPI с Native SQL. Бывает прикладной фильтр для поиска по 20-30 необязательным параметрам. Решать такие задачки на NativeSQL с JdbcTemplate можно, но дорого. Но это немного не по теме.

В плане эффективности тоже не всё однозначно. JSON добавляет в трафик метаданные.

В указанном примере с Entity graph проблема декартова произведения не
ушла, а только переместилась в запрос getAllByIdIn. Из‑за этого и
OutOfMemoryError.

Об этом описано в посте:

Проблема декартова произведения решается частично, JOIN все-таки остаются в запросе.

И причина:

Если посмотреть, какая часть запроса привела к OutOfMemoryError (так
сказать, «взорвалась»), то это именно та часть, где происходят JOIN c
помощью @EntityGraph

По поводу:

Есть более эффективное решение с Entity graph/Join fetch

В большинстве случаев этот подход эффективнее, чем использование @BatchSize

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

@BatchSize не нравится тем, что поведением управляет скрытая магия, которая на этапе компиляции прошита в аннотации. При всей его простоте. Как следствие, перестаёт управляться программно по месту. Количество запросов уменьшается, но остаётся недетерминированным.

Hibernate берёт некие N записей из сессии, у которых коллекция непроинициализирована, и запрашивает их. Возможно, вовсе не те, что требуется. Хотя в рамках пагинации для всех записей ClientEntity стоило бы извлекать все без исключения дочерние коллекции. Для чего тогда лимитировать? Только ли в ограничении БД дело? Неясно.

Кроме пагинации есть другая логика, на которую @BatchSize наложит этот же эффект, который програмно уже не отключить.

Было бы удобно, если бы в JPA/Hibernate был штатный метод по заполнию указанных коллекций. Что-то вроде: entityManager.loadAttributes(clients, "accounts", "deposits").

Sign up to leave a comment.