Pull to refresh

Hibernate Best Practices для начинающих

Reading time7 min
Views24K

В данной статье я не ставлю цель подробно описать 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

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

Tags:
Hubs:
+7
Comments25

Articles

Change theme settings