В данной статье я не ставлю цель подробно описать Hibernate, такого материала полно в сети. Это скорее справочник, в который можно заглянуть и увидеть возможные проблемные места и их решение, который позволит вам не допустить грубых ошибок при использовании Hibernate. Статья рассчитана на читателя уже знакомого с Hibernate и Spring.

Дисклеймер: я не претендую на полноту необходимых действий, ни на их уникальность, если я что-то упустил или исказил, комментарии приветствуются.

Содержание

Уникальный идентификатор сущности

Начнем с самого очевидного, - с определения сущности. Всегда задавайте уникальный идентификатор (далее id) для всех сущностей, и желательно что бы это было какое нибудь число, например, у вас есть сущность account у которого есть уникальное поле email вам может показаться что данное поле может выступать в качестве id, и поначалу это может оказаться вполне рабочим решением. Но если вкратце, оно не идеально и в будущем может наложить некоторые ограничения на приложение, к тому же по производительности могут возникнуть вопросы.

Уникальный идентификатор
Уникальный идентификатор

И так, мы определились что id будет целочисленным, далее на сцену выходи вопрос генерации этого id. В случае если СУБД поддерживает тип SEQUENCE(автоинкремент) то всегда предпочтительно выбирать именно его, в некоторых случаях это позволяет сильно повысить производительность системы.

К примеру, если у вас 5 операций вставки новых общностей, Spring Data или сам Hibernate могут быть достаточно умны, что бы поместить из в так называемый Batch и отдать их в СУБД за один раз, но для этого id новых записей надо знать заранее, вот тут то и вступает в игру тот самый автоинкремент, запросив текущее значение счетчика, не трудно догадаться какие id будут у всех пяти записей.

Автоинкремент
Автоинкремент

Equals и HashCode методы

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

Буде внимательны при выборе полей кандидатов в Equals и HashCode, это критично если ваши сущности будут храниться в коллекции типа Set или Map, либо вам приходится работать с detached объектами. В основном рекомендуют использовать уникальный идентификатор и/или так называемые «натуральные ключи», т. е. Поле однозначно идентифицирующее объект и которое является уникальным.

Избегайте использования @Data и @ToString аннотаций Lombok. Обе эти аннотации использую все поля объектов по умолчанию, что может послужить причиной проблем. При большом желании, можно использовать @ToString, главное не забывайте исключить LAZY поля вышей сущности из обработки при помощи @ToString.Exclude.

@OneToMany(mappedBy = "account", cascade = CascadeType.ALL)
@ToString.Exclude
private Set<RelatedPlatformEntity> relatedPlatforms = new HashSet<>();

Обработка исключений

В случае если вы вдруг используете голый Hibernate, то никогда не обрабатывайте исключение брошенное Jdbc как recoverable, т. е. не пытайтесь продолжать транзакцию, просто откатите её и закройте EntityManager. Иначе Hibernate не гарантирует актуальность загруженных данных!

Если же вы используете Spring Data, то можете быть спокойны, он позаботился об этом за вас.

Обработка исключений
Обработка исключений

Двухсторонние отношение сущностей

Как вы знаете в Hibernate существуют два типа отношений между сущностями, одно и двустороннее. Не вникая глубоко в детали, скажу что желательно всегда использовать двустороннее, если вы конечно на 100 % уверены, что обратная связи вам никогда не понадобится, то и одностороннее сойдет. Разница между ними не сильно большая (если все сделать грамотно), а пользы гораздо больше от двусторонней.

И так, как же это сделать грамотно? Тут все просто, дабы Hibernate создал именно двустороннее отношение (а не два односторонних) нужно уточнить, какая из сторон является владельцем отношений, а какая является обратной стороной. В этом нам поможет атрибут mappedBy. Он указывается в аннотации, которая находится на противоположной стороне отношения и указывает на владельца.

Двухсторонние отношение сущностей
Двухсторонние отношение сущностей

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

@OneToMany(mappedBy = "platform", cascade = {CascadeType.PERSIST})
private Set<RelatedPlatformEntity> statuses = new HashSet<>();

Хорошей привычкой является инициализация коллекции, иногда может спасти вас от неприятного NullPointerException, либо от постоянной проверки на null коллекции. Так же, вы можете заметить что вместо List, я использую Set, данный трюк может помочь где-то выиграть в ресурсах (при условии что Equals и HashCode переопределены), хотя и List вполне справляется.

Ленивая загрузка

В продолжение темы отношений между сущностями, в зависимости от типа отношений, Hibernate применяет LAZY или EAGER тип загрузки связанных сущностей. Если у вас нет особой причины для обратного, то убедитесь что выставлен тип LAZY.

Параметризованные запросы

Если вдруг вам случается писать JPQL или Native запросы, не забывайте передавать критерии поиска через параметры, никогда не пишите их на прямую в запросе, т.к. это создаёт уязвимость для атаки SQL injection

Параметризованные запросы
Параметризованные запросы

Кэш второго уровня

Это очень полезная фитча Hibernate. На этапе изучения фреймворка, этому не уделяется много внимания, да и в принципе маленький проект может вполне существовать без кэша второго уровня. Но если вы метите в Enterprise разработку, ну или просто пилите проект с более или менее серьезной нагрузкой то обязательно ознакомитесь с этим механизмом. Тут требуется тонкая настройка, тема эта обширная и цена ошибки здесь высока. По этому, пока вы в начале своего пути и еще совсем не «волшебник», вы можете применить как минимум «безопасный» подход, который темнее менее так же эффективен и может быть вполне достаточным для проекта. Безопасность метода заключается в том, что работать мы будем только со статическими сущностями, т. е. кандидаты в кэш второго уровня, это те сущности которые не будут меняться в вашем приложении, они либо создаются, либо удаляются. При таком раскладе у вас меньше шансов ошибиться.

Hibernate не содержит реализации данной фитчи, по этому вам придется использовать сторонние реализации, коих множество, я использую Ehcache 3. Так же что бы подружить Hibernate 5 с Ehcache 3 необходимо подключить Hibernate-jcache зависимость.

Необходимые зависимости
Необходимые зависимости

После подключения зависимостей необходимо указать Spring что с ними надо работать, а в частности задать свойства jpa persitence sharedCache и hibernate cache.

Файл свойств приложения
Файл свойств приложения

Далее выбранную сущность кандидат в кэш необходимо пометить тремя аннотациями @Cacheable @Cache и @Immutable

Кэшируемая сущность
Кэшируемая сущность

Вот и все, вы реализовали потокобезопасный кэш второго уровня! Остаются еще такие тонкости как настройка времени жизни объектов в кэше, размер кэша и т. д. Тем не менее на дефолтных настройках это тоже работает. Начните это использовать, ну а далее погружайтесь в детали, Гугл вам в помощь ;)

Аннотация @Column и @Table

При использовании данных аннотаций так же является хорошей практикой всегда задавать имя поля/таблицы. Если вы этого не сделаете, то Hibernate заделает это за вас и имя может не всегда соответствовать ожиданиям.

Имена полей и таблиц
Имена полей и таблиц

Количество загруженных сущностей

Это не очень насущная проблема и вы скорее с ней не столкнетесь, но я считаю, что вам стоит о ней знать. Дело в том что управляемые сущности не только занимают память, но еще потребляют процессорное время. Hibernate периодически проверяет состояние загруженных объектов на соответствие в базе данных, за это отвечает так называемый dirty checking. В рамках много пользовательского приложения это может быть значимой нагрузкой. Здесь очень полезна ленивая загрузка.

Проблема N+1 Select

Универсального решения этой проблемы нет, многое зависит от проекта. Я опешу самые явные случаи.

При получении сущности или списка общностей, Hibernate сделает один select и все бы ничего, но если данные сущности имеют EAGER отношения (ManyToOne и OneToOne по умолчанию EAGER) то для каждого из них будет сделан дополнительный select!

Выходом из этой ситуации может быть изменение дефолтного EAGER на LAZY.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "status")
private StatusEntity status;

Все же это решение подходит не всегда, допустим вы получаете список сущностей AccountEntiy, который содержит в себе связь с StatusEntity, и вам необходим проверить статус каждой сущности (разумеется вы делаете это в рамках одной транзакции, иначе вы получите LazyInitializationException), это приведет снова к той же проблеме, при обращении к статусу каждой сущности Hibernate сделает отдельный select, и вот проблема снова вернулась.

В этом случае проблему поможет решить инструкция JOIN FETCH, которая укажет Hibernate что связанные сущности тоже надо загрузить, вот пример JPQL запроса:

select account from AccountEntity account left join fetch account.status

В этом решении есть одна проблема, если вдруг список сущностей очень сильно вырастит. Представьте что количество записей AccountEntity равно пятидесяти тысячам, загрузить такое количество общностей будет не легко, а тут в нагрузку еще пятидесяти тысяч StatusEntity, это ужу совсем непростая задача. Дабы избежать этих трудностей нужно вернуться к схеме с ленивой загрузкой, в итоге система за раз загрузит только AccountEntity, что уменьшит нагрузку, но в то же время вернет проблему N+1 запросов. Без паники, выход найдется и из этой ситуации, так сказать «золотая середина».

Стратегия загрузки Batch Fetching позволяет получать связанные сущности не одиночными запросами, а пакетно. Размер пакета задается аннотацией @BatchSize.

@ManyToOne
@JoinColumn(name = "status")
@BatchSize(size = 10)
private StatusEntity status;

Использование данной аннотации с параметром «size=10» приведет к тому, что Hibernate за раз будет загружать ту сущность, что вы хотите прочитать и девять следующих по порядку.

Заключение

И так, я показал вам подводные камни и как их можно обходить. Но ни одно решение не является универсальным, я призываю вас не заниматься капипастом, а вникать в суть проблемы. Как я и написал выше, это справочник с примерами, а не подробная инструкция.

На примере уникального идентификатора, я покажу почему в вашем конкретном случае решение приведенное в данной статье может быть не самым эффективным.

Давайте представим что на вашем проекте вы используете в качестве базы данных PostgreSQL и вы решили задать стратегию генерации SEQUENCE, так же как это описано в статье. Это будет работать без проблем. Одно лишь только «но», Hibernate в таком случае сам настроит генерацию индексов на стороне БД и настройка это будет не самой оптимальной.

Он создаст один общий индекс на всю вашу базу.

Представите что у вас 10 таблиц (сущностей) в базе, а тип индекса Integre, тогда в итоге у вас остается Integre.MAX_SIZE / 10 на каждую таблицу.

Избежать этого недостатка вам поможет аннотация @SequenceGenerator

SequenceGenerator
SequenceGenerator

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

Комментарии (15)


  1. oxff
    27.07.2022 05:05
    +4

    Spring Data или сам Hibernate могут быть достаточно умны, что бы поместить из в так называемый Bath и отдать их в СУБД за один раз, но для этого id новых записей надо знать за ранее, вот тут то и вступает в игру тот самый автоинкремент, запросив текущее значение счетчика, не трудно догадаться какие id будут у всех пяти записей.

    1. "Bath" переводится на русский как "ванна". Скорее всего, вы имели в виду слово "batch".

    2. "за ранее" -> "заранее"

    3. На самом деле "догадаться" очень трудно, потому что в общем случае СУБД обслуживает несколько коннектов одновременно, и в момент когда несколько сессий запросят несколько значений из sequence, каждая из них может получить монотонно нарастающий набор пересекающихся значений. Например, сессия 1 получит "1, 2, 5, 6" и сессия 2 получит "3, 4, 7, 8".

      Дальше не стал читать, сорри.


    1. Dred222 Автор
      27.07.2022 09:38
      +1

      Благодарю за конструктивную критику. Опечатки поправлю.


  1. KirovA
    27.07.2022 08:18
    +2

    Не вникая глубока в детали, скажу что желательно всегда использовать двухсторонние, если вы конечно на 100 % уверены что обратная связи вам

    Я опешу самые явные случаи.

    И тд. Если абстрагироваться от диалекта "рускага езыка")), то статья понравилась. Пишите еще, но вычитывайте, пожалуйста.


    1. Dred222 Автор
      27.07.2022 09:33
      -1

      Ну это моя первая статья, хотя я старался. Буду улучшаться, в любом случае спасибо за отзыв.


  1. vshiyan
    27.07.2022 09:45

    Есть онлайн проверка на грамматику. Просто перед публикацией прогоняйте текст. За статью спасибо.


  1. qjnzero
    27.07.2022 09:45

    1. MayToOne и OneToOne по умолчанию EAGER -> ManyToOne и OneToOne по умолчанию EAGER

    2. решении есть одно проблема -> решении есть однa проблема


    1. Dred222 Автор
      27.07.2022 09:47

      Благодарю, поправлю.


  1. Johnvolt
    27.07.2022 09:45
    +1

    Избегайте использования @Data и @ToString аннотаций Lombok. Обе эти аннотации использую все поля объектов по умолчанию, что может послужить причиной проблем.

    Отличное замечание, но я так и не понял почему именно не стоит использовать @Data.

    Буду благодарен, если объясните


    1. Dred222 Автор
      27.07.2022 09:55

      @Data по умолчанию использует все поля объекта в equals и hashcode, отсюда и корень проблем. Самая очевидная из них это инициализация Lazy полей при вызове этих методов. Менее очевидная, это мутирующее поля из-за которых hashcode может меняться, это чревато тем что в какой-то момент вы можете не найти объект в коллекции несмотря на то что он там есть.


      1. dblur
        27.07.2022 18:08

        так а если есть необходимость использовать все поля для equals и hashcode?
        например, сущность не имеет связей и коллекций. все равно плохо?


        1. Dred222 Автор
          27.07.2022 18:16

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


    1. dididididi
      28.07.2022 17:59
      +1

      Stackoverfor будет при сериализации ещё. Когда json будешь делать, чтоб на фронт отдать)


  1. ermadmi78
    27.07.2022 17:10
    +3

    Как вы знаете в Hibernate существуют два типа отношений между сущностями, оно и двустороннее. Не вникая глубока в детали, скажу что желательно всегда использовать двустороннее, если вы конечно на 100 % уверены что обратная связи вам никогда не понадобится то и одностороннее сойдет. Разница между ними не сильно большая (если все сделать грамотно), а пользы гораздо больше от двусторонней.

    Я бы здесь все таки добавил ссылку на теорию реляционных баз данных. Просто в последние годы я вижу все больше и больше молодых программистов, которые "благодаря" Hibernate не знают и не понимают реляционные СУБД. Печальное зрелище. Добавив такую ссылку вы дадите этим людям хоть мизерный, но шанс, стать настоящими инженерами.


    1. Dred222 Автор
      27.07.2022 18:33
      -1

      Обсалютно согласен с Вами, это минус всех фреймворков за "магией" каторых теряется реальный мир. И особенно это отражается на молодых специалистах. Но все же, я надеюсь на их пытливый ум.


  1. slayeeer
    29.07.2022 11:02
    +1

    спасибо за статью , топ. на граматику пофигу