Pull to refresh
1
0
Andriy Slobodyanyk @Slobodator

Java Developer

Send message

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

Чуток "причесал" вашу реализацию.

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

  • entities не должны покидать границы транзакций

  • тогда и equals()/hashCode() переопредлять не нужно

Ну и посколько статья про Lombok:

  • как вы указываете, toString() можно только с явно указанными полями

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

  • к @NoArgsConstructor лучше добавить access=AccessLevel.PROTECTED, JPA достаточно, а разработчикам он не нужен по причине выше

  • @AllArgsConstructor скорее не нужен, потому как он включает id, а он обычно генерируется sequence

  • @Setter тоже не нужен всё по той же причине необходимости бизнес-целостности

  • @Data отпадает, уже объяснено выше

Итого от Lombok-a остаются только `@ToString(onlyExplicitlyIncluded = true)` c явно указанными полями.

И @Getter. Геттеры нужны для а) конвертации entity в DTO, б) для тестов. В бизнес коде их быть не должно.

Vlad Mihalchea предлагает способ, как писать select new PostWithAuthorFlatDto без указания полного имени класса.

Крайне интересно. Поделитесь техническими подробностями, пожалуйста.

Можете показать код, воспроизводящий баг? Который в баг-репорт прикладывали?

И вот эти комментарии

Если вы сюда попали, то делаете что-то уникальное

и

Если это вам нужно, то сделайте, пожалуйста, сами

... они в каких конкретно проектах/файлах/строчках?

Предполагаю, что вы ещё в контексте проблемы, раз

Недавно посмотрел код - ничего не поменялось :(

Вы можете подтвердить баг тестовым проектом, демонстрирующем проблему? Отправили баг-репорт в Spring?

Атрибут transactionManager в `@Transactional` конечно же использовали?

А, кстати, я правильно предполагаю, что entityManager.createStoredProcedureQuery вне транзакции не работает? Т.е. границы транзакции всё таки были где-то "выше"?

Но транзакция не закрывалась, потому что наружу выходило checked exception EDeliveryException?

И проблема решилась не столько добавлением @Transactional к createInboxMessage методу — это какбэ принципиально не верно — задавать границы транзакции в слое репозитория, а потому что перестали checked exception бросать

} catch (PersistenceException ex) {
    logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
}

...?

Глушить эксепшены тоже, кстати, нехорошая практика.

В общем, вопросы есть к изложенному в статье.

Вопрос не по теме (по теме напишу отдельный комментарий) -- вы как новый счёт клиенту добавляете?

clientEntity.addAccount(
    new AccountEntity(...)
);

или

accountRepository.save(
    new AccountEntity(
        clientEntity, 
        ...
    )
);

Если это поле таки прочиталось, то получите бесполезное обновление

Это не так в случае lazy-скаляров. Когда они у вас заработают, посмотрите, как ведёт себя обновление.

Опять же, с 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

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

С in-clause вообще лучше быть осторожным. В постгресе вроде бы ограничений нет, а в оракле, например, по дефолту не более 1000 аргументов -- соответственно, надо разбивать на чанки и конкатенировать результат.

Если бизнес-логика позволяет (как в данном конкретном случае), имеет смысл предварить в репозитории

interface ArticleRepository extends CrudRepository<Article, UUID> {
  default List<Article> findByPublisherId(List<UUID> ids) {
    if (ids.isEmpty()) {
     return Collections.emtpyList();
    }
    return _findByPublisherId(ids);
  }
  
  @Query("from Article where publisherId in :ids")
  List<Article> _findByPublisherId(List<UUID> ids);
}

Если метод совсем безобразно могут вызывать, ещё и проверку на null добавить.

По поводу @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-запросов в логах и незначительная деградация производительности.

Information

Rating
Does not participate
Location
Абу Даби, Абу Даби, О.А.Э.
Registered
Activity