Pull to refresh

Comments 25

Спасибо, очень полезная статья.

Как по мне, SQL код в Java коде лучше по двум причинам: во-первых все в одном месте - захотел использовать метод и сразу видишь что он там вытаскивает из бд, и не надо бегать по папкам проекта и искать мапинг этого запроса. Во-вторых - этот ужасный xml. В чем удобство его читать? Столько лишнего всего - схемы, ненужная мне метаинформация, ради одной строчки кода я должен глазами видеть в 3 раза больше бесполезных данных, в течение дня это сильно утомляет. Как вспомню доаннотационные времена спринга или javaee так вздрагиваю.

Маппинг, xml. Вы из какого года пишите?

Прежде чем задавать вопрос, статью прочтите, мой коммент относится к разделу «избавляемся от sql в джава коде».

Всё бы так, но есть пара нюансов

  1. Далеко не всегда SQL запросы занимают одну строчку

  2. При использовании db-specific native query при переключении с СУБД на СУБД (что очень актуально в наше время) придётся править java код, в случае же сохранения запросов в xml ресурсах переключение делается на уровне конфигов.

Со вторым соглашусь отчасти. Это действительно удобнее, но только в случае если есть необходимость менять БД. Но вот сколько я уже проектов сменил и ни разу не было надобности переключаться с одной БД на другую. И я стараюсь специфичные запросы не писать. И если вдруг такая необходимость возникла бы, то я думаю эта задача очень серьезная, которая потребует внимания всей команды, независимо от того, где прописаны запросы, в конфигах или аннотациях. Потому как потребуется миграция самих данных, а также менять файлы миграции схем и таблиц (всякие флайвей и т.д.), тестирование этого перехода. Короче, сродни пожару.

А с первым все равно не согласен. Но это индивидуально и дело вкуса.

И спасибо за статью, давно такой хорошей информации по хиберу не было. Хотя я сам стараюсь его в проектах не использовать, полюбил jooq, но все же когда-нибудь наверное придется с ним работать.

Обычно меняют бд когда меняют сам продукт, какое нибудь импортозамещение.

Ну и как быть с миграциями? Если их пишут на flyway, то скорее всего они под определённую бд

Спасибо, отличный набор хороших решений!

Дополнительно про Criteria API и JPA Metamodel: если по какой-то причине не хочется использовать JPA Metamodel, то можно заиспользовать @FieldNameConstants от Lombok, что также даст ошибку компиляции при изменении имени поля.

Спасибо за статью! А использование stream при чтении большой таблицы работает с нативными запросами?

Работает без проблем

А можете подробней расписать как с точки зрения потребления памяти работает. Если не используем стримы, то все объекты сохраняются в коллекцию и может возникнуть OOM? А если стрим, то достается по одному объекту у Oracle или в количестве fetch size для Postgres, обрабатывается и потом удалится сборщиком?
И если не использовать fetch для Postgres, то все сущности загрузятся в память?

Если не указать @BatchSize, то в Postgres всё считается в коллекцию, потом получите стрим. В Oracle нет такого. Проверял на разных версиях

Вы на какой версии hibernate FetchMode.SUBSELECT используете? Полагаю что 6.3, который идёт в spring boot 3.

Я пробовал в 5.6.4 - у меня не работало, в итоге написал динамический энтити граф, тяну им.

На 5-й версии SUBSELECT  работал, но не работала пагинаци.

Да, все примеры для spring boot 3.

По поводу @DynamicUpdate -- спорно.

  1. Во-первых, в MVCC базах скорее пишется новое состояние записи целиком, а не отдельные поля.

  2. Во-вторых, "дефолтный" update-запрос по всем полям кешируется, а динамические будут каждый раз парсится -- правда, не уверен, на какой нагрузке это будет актульно.

  3. Если не записывать обновленный LOB -- тогда и читать его не надо, либо вынести его в наследуемый класс, либо `@Basic(fetch = FetchType.LAZY)` c hibernate-enhance-maven-plugin.

  4. Отслеживание изменений полей -- dirty checking -- происходит всегда, если транзакция не read-only, @DynamicUpdate тут не влияет.

  5. @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-скаляров. Когда они у вас заработают, посмотрите, как ведёт себя обновление.

Опять же, с 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-ах, а и в большом количестве полей, например, которые редко меняются. Частая ситуация, когда у сущности меняется только статус - например заказ в интернет магазине, или количество, как например товар в розничной торговой точке. А если учесть, что таких операций может быть несколько тысяч в секунду, то выигрыш приличный получается.

И ещё надо учитывать, что БД одна, а приложений тысячи, так что проще вытащить весь объём из БД сразу, чем бомбить её бедную-несчастную лишними запросами.

Sign up to leave a comment.