Комментарии 25
Спасибо, очень полезная статья.
Как по мне, SQL код в Java коде лучше по двум причинам: во-первых все в одном месте - захотел использовать метод и сразу видишь что он там вытаскивает из бд, и не надо бегать по папкам проекта и искать мапинг этого запроса. Во-вторых - этот ужасный xml. В чем удобство его читать? Столько лишнего всего - схемы, ненужная мне метаинформация, ради одной строчки кода я должен глазами видеть в 3 раза больше бесполезных данных, в течение дня это сильно утомляет. Как вспомню доаннотационные времена спринга или javaee так вздрагиваю.
Маппинг, xml. Вы из какого года пишите?
Всё бы так, но есть пара нюансов
Далеко не всегда SQL запросы занимают одну строчку
При использовании db-specific native query при переключении с СУБД на СУБД (что очень актуально в наше время) придётся править java код, в случае же сохранения запросов в xml ресурсах переключение делается на уровне конфигов.
Со вторым соглашусь отчасти. Это действительно удобнее, но только в случае если есть необходимость менять БД. Но вот сколько я уже проектов сменил и ни разу не было надобности переключаться с одной БД на другую. И я стараюсь специфичные запросы не писать. И если вдруг такая необходимость возникла бы, то я думаю эта задача очень серьезная, которая потребует внимания всей команды, независимо от того, где прописаны запросы, в конфигах или аннотациях. Потому как потребуется миграция самих данных, а также менять файлы миграции схем и таблиц (всякие флайвей и т.д.), тестирование этого перехода. Короче, сродни пожару.
А с первым все равно не согласен. Но это индивидуально и дело вкуса.
И спасибо за статью, давно такой хорошей информации по хиберу не было. Хотя я сам стараюсь его в проектах не использовать, полюбил jooq, но все же когда-нибудь наверное придется с ним работать.
Спасибо, отличный набор хороших решений!
Дополнительно про Criteria API и JPA Metamodel: если по какой-то причине не хочется использовать JPA Metamodel, то можно заиспользовать @FieldNameConstants от Lombok, что также даст ошибку компиляции при изменении имени поля.
Спасибо за статью! А использование stream при чтении большой таблицы работает с нативными запросами?
А можете подробней расписать как с точки зрения потребления памяти работает. Если не используем стримы, то все объекты сохраняются в коллекцию и может возникнуть OOM? А если стрим, то достается по одному объекту у Oracle или в количестве fetch size для Postgres, обрабатывается и потом удалится сборщиком?
И если не использовать fetch для Postgres, то все сущности загрузятся в память?
Вы на какой версии hibernate FetchMode.SUBSELECT используете? Полагаю что 6.3, который идёт в spring boot 3.
Я пробовал в 5.6.4 - у меня не работало, в итоге написал динамический энтити граф, тяну им.
По поводу @DynamicUpdate -- спорно.
Во-первых, в MVCC базах скорее пишется новое состояние записи целиком, а не отдельные поля.
Во-вторых, "дефолтный" update-запрос по всем полям кешируется, а динамические будут каждый раз парсится -- правда, не уверен, на какой нагрузке это будет актульно.
Если не записывать обновленный LOB -- тогда и читать его не надо, либо вынести его в наследуемый класс, либо `@Basic(fetch = FetchType.LAZY)` c
hibernate-enhance-maven-plugin
.Отслеживание изменений полей -- dirty checking -- происходит всегда, если транзакция не read-only,
@DynamicUpdate
тут не влияет.@DynamicUpdate
может быть актуален, если "дефолтный" update по всем полям зацепляет поле, которое а) не изменилось б) но в БД на него повешен триггер и происходит какой-то side effect.
В общем, без измерений под нагрузкой от @DynamicUpdate
скорее всего будет псевдо-радость от "оптимизированных" SQL-запросов в логах и незначительная деградация производительности.
>Во-первых, в MVCC базах скорее пишется новое состояние записи целиком
Все примеры проверялись на Oracle и Postgres. Писались только изменённый поля.
>Во-вторых, "дефолтный" update-запрос по всем полям кешируется, а динамические будут каждый раз парсится
Современные СУБД решают эту проблему
>Если не записывать обновленный LOB -- тогда и читать его не над
Странное утверждение. Читают данные не с целью потом их записать. Проблему с записью LOB конечно можно решить переносом их в отдельную таблицу, но данная статья не про проектирование БД.
>Отслеживание изменений полей -- dirty checking -- происходит всегда
Я обратного и не утверждал. dirty checking упоминался в другом разделе, просьба внимательнее читать статью
>но в БД на него повешен триггер и происходит какой-то side effect.
Триггерная логика - это зло, там более если она приводит к побочкам)
>В общем, без измерений под нагрузкой от @DynamicUpdate
скорее всего будет псевдо-радость от "оптимизированных" SQL-запросов
Я описал ситуации, когда данная технология будет уместна, равно как и проблемы. Опять же, просьба внимательнее читать статью. Ещё хочу сказать, что все примеры взяты из реальной жизни и я лично встречался с ситуациями, когда @DynamicUpdate резко повышал производительность.
Гм, раз вы меня дважды упрекаете, что я невнимательно читаю статью, придётся нарушить правило "overquoting -- зло".
Все примеры проверялись на Oracle и Postgres. Писались только изменённый поля.
Конечно, в любом случае пишутся только изменённые поля. Вопрос в том, что происходит под капотом БД. В случае Postgres мы имеем Vacuum, подробнее https://rbranson.medium.com/10-things-i-hate-about-postgresql-20dbab8c2791 раздел "#4: MVCC Garbage Frequently Painful". Для Oracle это не так, поэтому я и вставил оговорку "скорее всего".
>Во-вторых, "дефолтный" update-запрос по всем полям кешируется, а динамические будут каждый раз парсится
Современные СУБД решают эту проблему
Это очень смелое утверждение. Можете, пожалуйста, привести аргументы/доказательства?
Вы же сами начинаете с
Hibernate генерирует операторы SQL для операций CRUD всех объектов. Эти инструкции SQL генерируются один раз и кэшируются в памяти для повышения производительности.
Мой поинт в том, что "дефолтный" update по всем полям был сделан так именно из кеширования. Более того, даже in-clause
(если уж приходится им пользоваться) на разном количество параметров оптимизуют с помощью hibernate.query.in_clause_parameter_padding
, подробнее https://vladmihalcea.com/improve-statement-caching-efficiency-in-clause-parameter-padding/
Разница в поведении будет заметна, конечно, только под нагрузкой.
>Если не записывать обновленный LOB -- тогда и читать его не надо
Странное утверждение.
Вы пишете
...операция обновления может стоить очень дорого... размер полей большой (например, LOB). Решить эту проблему поможет аннотация для сущности @DynamicUpdate.
Действительно, если в сущности есть LOB, он будет а) вычитываться из БД всегда, б) попадать в дефолтный update, даже если не изменился. Это не оптимально. Вы предлагаете @DynamicUpdate
, а я предлагаю сделать его LAZY
. Для этого не обязательно выносить его в отдельную таблицу.
>Отслеживание изменений полей -- dirty checking -- происходит всегда
Я обратного и не утверждал.
Тем не менее, в статье есть
Или накладные расходы на отслеживание, или на запрос, содержащий все столбцы.
Накладные расходы на отслеживание -- dirty checking -- происходит всегда*, @DynamicUpdate
на них не влияет.
*всегда -- имеется в виду а) транзакция не read-only, б) сессия не stateless
Я описал ситуации, когда данная технология будет уместна, равно как и проблемы.
А я пишу возражения к этими ситуациям, что, прежде чем браться за @DynamicUpdate
:
если поле не обновляется вообще никогда, лучше его аннотировать
@Column(updateable = false)
если в сущности LOB, есть смысл задуматься, не загружать ли его лениво
если в сущности много полей, а в разных бизнес-кейсах обновляются только некоторые из них, возможно,
@DynamicUpdate
будет оправдан. Осталось разобраться и померять, что такое "много" и "некоторые", и какой такой дизайн и кейсы получились.
я лично встречался с ситуациями, когда @DynamicUpdate резко повышал производительность
При всём уважении к вашему авторитету, ссылка на маленький тест демонстрирующий разницу между обычным обновлением и динамическим, была бы куда более весома. Пока я предполагаю, что таким образом (вместо lazy) боролись с LOB-ами.
Повторюсь,
без измерений под нагрузкой от
@DynamicUpdate
скорее всего будет псевдо-радость от "оптимизированных" SQL-запросов в логах и незначительная деградация производительности.
>Гм, раз вы меня дважды упрекаете, что я невнимательно читаю статью
Да, невнимательно.
>Отслеживание изменений полей -- dirty checking -- происходит всегда
Я обратного и не утверждал.
>Тем не менее, в статье есть
Ко вот только это относится не DynamicUpdate, а к Stateless Session. Просьба таки прочитать статью целиком)
Современные СУБД решают эту проблему
>Это очень смелое утверждение. Можете, пожалуйста, привести аргументы/доказательства?
Почитайте Тома Кайта, например. Там всё популярно описано
>Вы предлагаете @DynamicUpdate
, а я предлагаю сделать его LAZY
Lazy - это не панацея. Если это поле таки прочиталось, то получите бесполезное обновление (а обновление LOB-ов это дорогостоящая операция). Если вынести а отдельную таблицу, то этих проблем избегаем. Но опять же, статья не про проектирование.
>ссылка на маленький тест демонстрирующий разницу между обычным обновлением и динамическим, была бы куда более весом
Могу даже большой тест привести). Например магазины "Магнит" работают в том числе благодаря такой оптимизации. Что может быть весомей? )
Опять же, с Lazy имеем кучу проблем, таких как n+1 и работа за пределами транзакций. Тем более Lazy для скалярных свойств без танцев с бубнами не особо работает. Вот пример для h2:
@Column(name = "LOB")@Lob @Basic(fetch = FetchType.LAZY)private String lob;
При чтении получаем:
"select bt1_0."id",bt1_0."LOB",bt1_0."NAME" from "BIG_TABLE" bt1_0"
Видно, что никакой ленивой загрузки нет
Опять же, с Lazy имеем кучу проблем, таких как n+1
Любая Lazy-загрузка -- это потенциальная N+1 проблема. Но одновременно и очевидный прирост производительности. `fetch = FetchType.EAGER` вы же над коллекциями не ставите, верно?
и работа за пределами транзакций
Я не вполне понимаю, что вы имеете в виду, хотя и догадываюсь. Как вы работаете с lazy-коллекциями "за пределами транзакций"? C lazy-скалярами всё то же самое.
пример для h2
БД значения не имеет, lazy-loading делает хибернейт.
без танцев с бубнами
Byte enhancement -- это валидный инструмент, а не танцы с бубном. Поскольку String
-- final class, а не интерфейс, по-другому, увы, никак, хотя ещё ленивую загрузку LOB-a можно сымитировать с наследуемым классом (но там свои компромиссы), если byte enhancement почему-то смущает.
Видно, что никакой ленивой загрузки нет
Это пока нет) А внимательно перечитаете мой самый первый комментарий, погуглите -- уверен, заработает. Хинт: IDEA компилирует сама, byte enhancement не делает, поэтому запускайте тест напрямую с maven/gradle, вот "баг" на эту тему https://youtrack.jetbrains.com/issue/IDEA-159903/Hibernate-bytecode-instrumentation-code-is-being-overridden-by-IDEA
>Любая Lazy-загрузка -- это потенциальная N+1 проблема.
А если этих полей несколько, то будет ещё "веселее".
>fetch = FetchType.EAGER` вы же над коллекциями не ставите,
Потому что при работе с коллекциями возникают проблемы при join-ах. Для скалярных свойств нет таких проблем.
Более того, в ManyToOne EAGER по умолчанию, а это неспроста.
>Как вы работаете с lazy-коллекциями "за пределами транзакций"?
Ну да, мало гемора с коллекциями, обретаем проблемы для скалярных свойств
С коллекциями Lazy используется не только и не столько из-за уменьшения объёма, но и по другим причинам.
>А внимательно перечитаете мой самый первый комментарий, погуглите -- уверен, заработает.
Ещё раз говорю, что если вынести в отдельную таблицу, то все проблемы исчезают сами собой без плагинов и наследования. Причём в оракле, например, даже предпочтительней хранить LOB-ы в отдельной таблицы, легче их удалять.
Хочу подытожить, что универсальных решений не бывает и утверждать, что DynamicUpdate - это всегда плохо, по крайней мере неправильно.
Равно как и Lazy загрузка может быть полезной в некоторых случаях
А в целом, спасибо за совет, конечно. Я не юзал подобный подход, возьму на заметку
Ну и опять же, причина может быть не только в LOB-ах, а и в большом количестве полей, например, которые редко меняются. Частая ситуация, когда у сущности меняется только статус - например заказ в интернет магазине, или количество, как например товар в розничной торговой точке. А если учесть, что таких операций может быть несколько тысяч в секунду, то выигрыш приличный получается.
И ещё надо учитывать, что БД одна, а приложений тысячи, так что проще вытащить весь объём из БД сразу, чем бомбить её бедную-несчастную лишними запросами.
Рецепты «приготовления» Hibernate, или решаем 5 проблем работы с фреймворком