Статьи

5 распространенных исключений Hibernate и как их исправить

Посмотрите, как вы можете легко решить наиболее распространенные проблемы с Hibernate

Hibernate, вероятно, самая популярная реализация JPA на рынке, и вы можете увидеть это во многих местах, например:

  • Количество проектов, в которых вы использовали его сами,
  • Количество рабочих мест, которые требуют опыта Hibernate и, конечно,
  • Количество вопросов и исключений, размещенных в интернете

В Takipi основное внимание уделяется поиску и устранению исключений. Поэтому я остановлюсь на последнем пункте в списке и поделюсь с вами 5 исключениями Hibernate, которые я, вероятно, исправил, объяснил, написал в блоге и пожаловался на большинство за более чем 15 лет работы с Hibernate.

И хотя они не попали в топ- 10 типов исключений , быстрый поиск в Google сказал мне, что я не единственный, кто сталкивается с этими проблемами.

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

1. LazyInitializationException

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

1
2
3
4
5
6
7
8
9
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
 
Author a = em.find(Author.class, 1L);
 
em.getTransaction().commit();
em.close();
         
log.info(a.getFirstName() + " " + a.getLastName() + " wrote "+a.getBooks().size() + " books.");

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

Самый популярный способ сделать это — получить доступ к отношениям с FetchType.LAZY на уровне презентации, который вы не инициализировали на уровне бизнеса. Вы можете найти множество таких проблем на популярных форумах с множеством плохих советов о том, как их исправить.

Пожалуйста, не используйте открытую сессию в виде анти-паттерна. Это приносит больше вреда, чем приносит пользу.

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

JPA и Hibernate предлагают различные варианты инициализации лениво извлеченных отношений . Мой личный фаворит — @NamedEntityGraph, который обеспечивает независимый от запроса способ определения графа сущностей, которые будут выбраны с запросом.

Вы можете увидеть пример простого графика в следующем фрагменте кода. Извлекает отношение Book для объекта Author.

1
@NamedEntityGraph(name = "graph.AuthorBooks", attributeNodes = @NamedAttributeNode("books"))

Вы можете определить @NamedEntityGraph в любом файле, доступном для Hibernate. Я предпочитаю делать это на объекте, с которым я намереваюсь его использовать.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
 
EntityGraph<?> graph = em.getEntityGraph("graph.AuthorBooks");
HashMap<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", graph);
 
Author a = em.find(Author.class, 1L, properties);
 
em.getTransaction().commit();
em.close();
         
log.info(a.getFirstName() + " " + a.getLastName() + " wrote "+a.getBooks().size() + " books.");

Как вы можете видеть, я сначала вызываю метод getEntityGraph (String name) в EntityManager, чтобы получить экземпляр графа сущностей. На следующем шаге я создаю HashMap с подсказками запросов и добавляю график в виде javax.persistence.fetchgraph.

На последнем шаге я предоставляю подсказки запроса в качестве дополнительного параметра к методу find. Это говорит Hibernate инициализировать отношения с сущностями Book, и я могу вызвать метод getBooks () без активного сеанса Hibernate.

2. OptimisticLockException

Другое очень распространенное исключение — исключение OptimisticLockException. Hibernate выдает его при использовании оптимистической блокировки и обнаруживает конфликтующее обновление объекта. Это чаще всего происходит по одной из двух причин:

  1. 2 пользователя пытаются обновить один и тот же объект практически в один и тот же момент времени.
  2. 1 пользователь выполняет 2 обновления одной и той же сущности, и вы не обновили представление сущности в клиенте, чтобы значение версии не обновлялось после первого обновления.

Вы можете увидеть тестовый пример с 2 одновременными обновлениями в следующем фрагменте кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// EntityManager and transaction 1
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
         
// EntityManager and transaction 2
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
 
// update 1
Author a = em.find(Author.class, 1L);
a.setFirstName("changed");
         
// update 2
Author a2 = em2.find(Author.class, 1L);
a2.setFirstName("changed");
 
// commit transaction 1
em.getTransaction().commit();
em.close();
         
// commit transaction 2
try {
    em2.getTransaction().commit();
    Assert.fail();
    } catch (RollbackException e) {
        Assert.assertTrue(e.getCause() instanceof OptimisticLockException);
        log.info("2nd transaction failed with an OptimisticLockException");
    }
         
em2.close();

Как видите, я использую два независимых EntityManager и запускаю транзакцию с обоими, получаю сущность Author с идентификатором 1 и обновляю атрибут имени.

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

Если вы используете Takipi , вы можете видеть состояние всех переменных, когда произошло исключение, что может быть полезно для определения источника второго вызова обновления.

Takipi-RollBack

Экран анализа ошибок Такипи

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

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

3. org.hibernate.AnnotationException: Неизвестный Id.generator

Это вызвано неправильным отображением сущностей, и вы можете столкнуться с ним во время разработки. Причина этого довольно проста: вы ссылаетесь на неизвестный генератор последовательностей в аннотации @GeneratedValue, как в следующем фрагменте кода.

1
2
3
4
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "authorSequence")
@Column(name = "id", updatable = false, nullable = false)
private Long id;

Аннотация @GeneratedValue позволяет вам определить стратегию генерации значений первичного ключа. В предыдущем фрагменте кода я хотел использовать последовательность базы данных и указать в качестве имени генератора «authorSequence».

Многие разработчики ожидают, что «authorSequence» будет именем последовательности базы данных, которую должен использовать Hibernate. Это не тот случай. Это имя @SequenceGenerator, которое вы можете использовать для предоставления дополнительной информации о последовательности базы данных, которую Hibernate будет использовать.

Но определение @SequenceGenerator отсутствует, поэтому Hibernate выдает исключение AnnotationException. Чтобы исправить это, вы должны добавить аннотацию @SequenceGenerator, как я сделал в следующем фрагменте кода.

1
2
3
4
5
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "authorSequence")
@SequenceGenerator(name = "authorSequence", sequenceName = "author_seq", initialValue = 1000)
@Column(name = "id", updatable = false, nullable = false)
private Long id;

Аннотация @SequenceGenerator позволяет предоставить больше информации о последовательности базы данных и о том, как Hibernate будет ее использовать. В этом фрагменте кода я установил имя последовательности, которая называется «author_seq» и ​​1000 в качестве начального значения.

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

4. QuerySyntaxException: таблица не отображается

Это еще одна типичная ошибка отображения. В большинстве проектов схема базы данных уже существует или определяется независимо от вашего соответствия сущностей. И это хорошо. Пожалуйста, спроектируйте схему базы данных правильно и не позволяйте Hibernate генерировать ее для вас!

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

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

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

1
2
3
4
5
@Entity
@Table(name = "author", schema = "bookstore")
public class Author implements Serializable {
    
}

5. org.hibernate.PersistentObjectException: отдельная сущность передана для сохранения

Последнее исключение в этом списке может иметь несколько причин, и все они являются ошибками:

  1. Вы пытаетесь сохранить новую сущность и предоставить значение первичного ключа, но сопоставление сущности определяет стратегию для ее генерации.
  2. Вы пытаетесь сохранить новую сущность, и контекст постоянства уже содержит сущность с заданным идентификатором.
  3. Вы пытаетесь сохранить отдельную сущность, а не объединять ее.

Первый легко исправить, не задавайте значение первичного ключа и не удаляйте стратегию генерации первичного ключа.

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

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

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

Кроме того, есть некоторые вещи, которые вы можете сделать на стороне сервера, чтобы избежать подобных проблем, например, использование определенных объектов-значений для сценариев использования create и не обработка сценариев использования create и update в одном и том же методе сервера. Это облегчает разработчику клиента поиск и вызов правильного метода и позволяет избежать подобных проблем.

Резюме и шпаргалка

Это были мои 5 самых распространенных исключений Hibernate и как их можно исправить. Как вы видели, исключения и их причины очень разные. Некоторые из них происходят только во время разработки, а другие поразят вас в процессе производства. Так что лучше следите и убедитесь, что вы знакомы с такими проблемами. Чтобы сделать это немного легче для вас, я подготовил шпаргалку, объясняющую 5 исключений, упомянутых в этом посте .

Ссылка: 5 распространенных исключений Hibernate и как их исправить от нашего партнера JCG Торбена Янссена в блоге Takipi .