Перейти к основному содержимому

Общие исключения гибернации

· 15 мин. чтения

1. Введение

В этом руководстве мы обсудим некоторые распространенные исключения, с которыми мы можем столкнуться при работе с Hibernate.

Мы рассмотрим их назначение и некоторые распространенные причины. Кроме того, мы рассмотрим их решения.

2. Обзор исключений гибернации

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

Эти исключения в основном распространяются на HibernateException . Однако, если мы используем Hibernate в качестве поставщика сохраняемости JPA, эти исключения могут быть завернуты в PersistenceException .

Оба этих базовых класса расширяются от RuntimeException . Поэтому они все не проверены. Следовательно, нам не нужно перехватывать или объявлять их везде, где они используются.

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

Давайте теперь рассмотрим каждый из них, один за другим.

3. Ошибки отображения

Объектно-реляционное сопоставление — главное преимущество Hibernate. В частности, это освобождает нас от ручного написания операторов SQL.

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

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

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

Давайте рассмотрим эти ошибки на нескольких примерах.

3.1. MappingException

Проблема с объектно-реляционным сопоставлением вызывает исключение MappingException :

public void whenQueryExecutedWithUnmappedEntity_thenMappingException() {
thrown.expectCause(isA(MappingException.class));
thrown.expectMessage("Unknown entity: java.lang.String");

Session session = sessionFactory.getCurrentSession();
NativeQuery<String> query = session
.createNativeQuery("select name from PRODUCT", String.class);
query.getResultList();
}

В приведенном выше коде метод createNativeQuery пытается сопоставить результат запроса с указанным типом Java String. Он использует неявное отображение класса String из Metamodel для выполнения отображения.

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

Для подробного анализа возможных причин и решений ознакомьтесь с Hibernate Mapping Exception — Unknown Entity .

Точно так же другие ошибки также могут вызывать это исключение:

  • Смешивание аннотаций к полям и методам
  • Невозможно указать @JoinTable для ассоциации @ManyToMany
  • Конструктор сопоставленного класса по умолчанию выдает исключение во время обработки сопоставления.

Кроме того, у MappingException есть несколько подклассов, которые могут указывать на определенные проблемы с отображением:

  • AnnotationException — проблема с аннотацией
  • DuplicateMappingException — повторяющееся сопоставление для класса, таблицы или имени свойства.
  • InvalidMappingException — сопоставление недопустимо
  • MappingNotFoundException — ресурс сопоставления не найден .
  • PropertyNotFoundException — ожидаемый метод получения или установки не может быть найден в классе.

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

3.2. AnnotationException

Чтобы понять AnnotationException, давайте создадим объект без аннотации идентификатора для любого поля или свойства:

@Entity
public class EntityWithNoId {
private int id;
public int getId() {
return id;
}

// standard setter
}

Поскольку Hibernate ожидает, что каждая сущность будет иметь идентификатор , мы получим исключение AnnotationException при использовании сущности:

public void givenEntityWithoutId_whenSessionFactoryCreated_thenAnnotationException() {
thrown.expect(AnnotationException.class);
thrown.expectMessage("No identifier specified for entity");

Configuration cfg = getConfiguration();
cfg.addAnnotatedClass(EntityWithNoId.class);
cfg.buildSessionFactory();
}

Кроме того, некоторые другие вероятные причины:

  • Неизвестный генератор последовательности, используемый в аннотации @GeneratedValue
  • Аннотация @Temporal, используемая с классом даты / времени Java 8
  • Целевой объект отсутствует или не существует для @ManyToOne или @OneToMany
  • Необработанные классы коллекций, используемые с аннотациями отношений @OneToMany или @ManyToMany
  • Конкретные классы, используемые с аннотациями коллекций @OneToMany , @ManyToMany или @ElementCollection , поскольку Hibernate ожидает интерфейсы коллекций

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

4. Ошибки управления схемой

Автоматическое управление схемой базы данных — еще одно преимущество Hibernate. Например, он может генерировать операторы DDL для создания или проверки объектов базы данных.

Чтобы использовать эту функцию, нам нужно правильно установить свойство hibernate.hbm2ddl.auto .

Если возникают проблемы при выполнении управления схемой, мы получаем исключение. Разберем эти ошибки.

4.1. SchemaManagementException

Любая проблема, связанная с инфраструктурой, при выполнении управления схемой вызывает исключение SchemaManagementException .

Чтобы продемонстрировать, давайте проинструктируем Hibernate проверить схему базы данных:

public void givenMissingTable_whenSchemaValidated_thenSchemaManagementException() {
thrown.expect(SchemaManagementException.class);
thrown.expectMessage("Schema-validation: missing table");

Configuration cfg = getConfiguration();
cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "validate");
cfg.addAnnotatedClass(Product.class);
cfg.buildSessionFactory();
}

Поскольку таблица, соответствующая Product , отсутствует в базе данных, мы получаем исключение проверки схемы при построении SessionFactory .

Кроме того, существуют другие возможные сценарии для этого исключения:

  • невозможно подключиться к базе данных для выполнения задач управления схемой
  • схема отсутствует в базе данных

4.2. CommandAcceptanceException

Любая проблема с выполнением DDL, соответствующего конкретной команде управления схемой, может вызвать CommandAcceptanceException .

В качестве примера укажем неправильный диалект при настройке SessionFactory :

public void whenWrongDialectSpecified_thenCommandAcceptanceException() {
thrown.expect(SchemaManagementException.class);

thrown.expectCause(isA(CommandAcceptanceException.class));
thrown.expectMessage("Halting on error : Error executing DDL");

Configuration cfg = getConfiguration();
cfg.setProperty(AvailableSettings.DIALECT,
"org.hibernate.dialect.MySQLDialect");
cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "update");
cfg.setProperty(AvailableSettings.HBM2DDL_HALT_ON_ERROR,"true");
cfg.getProperties()
.put(AvailableSettings.HBM2DDL_HALT_ON_ERROR, true);

cfg.addAnnotatedClass(Product.class);
cfg.buildSessionFactory();
}

Здесь мы указали неправильный диалект: MySQLDialect. Кроме того, мы указываем Hibernate обновлять объекты схемы. Следовательно, операторы DDL, выполняемые Hibernate для обновления базы данных H2, завершатся ошибкой, и мы получим исключение.

По умолчанию Hibernate молча регистрирует это исключение и продолжает работу. Когда мы позже используем SessionFactory, мы получаем исключение.

Чтобы убедиться, что при этой ошибке возникает исключение, мы установили для свойства HBM2DDL_HALT_ON_ERROR значение true .

Аналогично, вот некоторые другие распространенные причины этой ошибки:

  • Несоответствие имен столбцов между сопоставлением и базой данных
  • Два класса сопоставлены с одной и той же таблицей
  • Имя, используемое для класса или таблицы, является зарезервированным словом в базе данных, например USER .
  • Пользователь, используемый для подключения к базе данных, не имеет необходимых прав

5. Ошибки выполнения SQL

Когда мы вставляем, обновляем, удаляем или запрашиваем данные с помощью Hibernate, он выполняет операторы DML для базы данных с помощью JDBC . Этот API вызывает исключение SQLException , если операция приводит к ошибкам или предупреждениям.

Hibernate преобразует это исключение в JDBCException или один из его подходящих подклассов:

  • ConstraintViolationException
  • Исключение данных
  • JDBCConnectionException
  • LockAcquisitionException
  • Пессимистиклокксцептион
  • QueryTimeoutException
  • SQLGrammarException
  • ОбщееJDBCException

Обсудим распространенные ошибки.

5.1. JDBCException

JDBCException всегда вызывается конкретным оператором SQL. Мы можем вызвать метод getSQL , чтобы получить ошибочный оператор SQL.

Кроме того, мы можем получить базовое SQLException с помощью метода getSQLException .

5.2. SQLGrammarException

SQLGrammarException указывает, что SQL, отправленный в базу данных, недействителен. Это может быть связано с синтаксической ошибкой или недопустимой ссылкой на объект.

Например, отсутствие таблицы может привести к этой ошибке при запросе данных :

public void givenMissingTable_whenQueryExecuted_thenSQLGrammarException() {
thrown.expect(isA(PersistenceException.class));
thrown.expectCause(isA(SQLGrammarException.class));
thrown.expectMessage("SQLGrammarException: could not prepare statement");

Session session = sessionFactory.getCurrentSession();
NativeQuery<Product> query = session.createNativeQuery(
"select * from NON_EXISTING_TABLE", Product.class);
query.getResultList();
}

Также мы можем получить эту ошибку при сохранении данных, если таблица отсутствует:

public void givenMissingTable_whenEntitySaved_thenSQLGrammarException() {
thrown.expect(isA(PersistenceException.class));
thrown.expectCause(isA(SQLGrammarException.class));
thrown
.expectMessage("SQLGrammarException: could not prepare statement");

Configuration cfg = getConfiguration();
cfg.addAnnotatedClass(Product.class);

SessionFactory sessionFactory = cfg.buildSessionFactory();
Session session = null;
Transaction transaction = null;
try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Product product = new Product();
product.setId(1);
product.setName("Product 1");
session.save(product);
transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
closeSessionFactoryQuietly(sessionFactory);
}
}

Некоторые другие возможные причины:

  • Используемая стратегия именования не сопоставляет классы с правильными таблицами.
  • Столбец, указанный в @JoinColumn , не существует

5.3. ConstraintViolationException

ConstraintViolationException указывает, что запрошенная операция DML вызвала нарушение ограничения целостности . Мы можем получить имя этого ограничения, вызвав метод getConstraintName .

Распространенной причиной этого исключения является попытка сохранить повторяющиеся записи:

public void whenDuplicateIdSaved_thenConstraintViolationException() {
thrown.expect(isA(PersistenceException.class));
thrown.expectCause(isA(ConstraintViolationException.class));
thrown.expectMessage(
"ConstraintViolationException: could not execute statement");

Session session = null;
Transaction transaction = null;

for (int i = 1; i <= 2; i++) {
try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();
Product product = new Product();
product.setId(1);
product.setName("Product " + i);
session.save(product);
transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
}
}
}

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

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

5.4. Исключение данных

DataException указывает на то, что оценка оператора SQL привела к какой-либо недопустимой операции, несоответствию типов или неправильной кардинальности.

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

public void givenQueryWithDataTypeMismatch_WhenQueryExecuted_thenDataException() {
thrown.expectCause(isA(DataException.class));
thrown.expectMessage(
"org.hibernate.exception.DataException: could not prepare statement");

Session session = sessionFactory.getCurrentSession();
NativeQuery<Product> query = session.createNativeQuery(
"select * from PRODUCT where id='wrongTypeId'", Product.class);
query.getResultList();
}

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

5.5. JDBCConnectionException

JDBCConectionException указывает на проблемы при обмене данными с базой данных .

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

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

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

5.6. QueryTimeoutException

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

Это одна из немногих исправимых ошибок, что означает, что мы можем повторить оператор в той же транзакции.

Чтобы решить эту проблему, мы можем увеличить время ожидания для длительных запросов несколькими способами:

  • Установите элемент тайм -аута в аннотации @NamedQuery или @NamedNativeQuery.
  • Вызвать метод setHint интерфейса Query.
  • Вызовите метод setTimeout интерфейса Transaction .
  • Вызовите метод setTimeout интерфейса Query .

6. Ошибки, связанные с состоянием сеанса

Давайте теперь рассмотрим ошибки из-за ошибок использования сеанса Hibernate.

6.1. NonUniqueObjectException

Спящий режим не позволяет использовать два объекта с одинаковым идентификатором в одном сеансе.

Если мы попытаемся связать два экземпляра одного и того же класса Java с одним и тем же идентификатором в одном сеансе, мы получим исключение NonUniqueObjectException . Мы можем получить имя и идентификатор сущности, вызвав методы getEntityName() и getIdentifier() .

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

public void 
givenSessionContainingAnId_whenIdAssociatedAgain_thenNonUniqueObjectException() {
thrown.expect(isA(NonUniqueObjectException.class));
thrown.expectMessage(
"A different object with the same identifier value was already associated with the session");

Session session = null;
Transaction transaction = null;

try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();

Product product = new Product();
product.setId(1);
product.setName("Product 1");
session.save(product);

product = new Product();
product.setId(1);
product.setName("Product 2");
session.save(product);

transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
}
}

Как и ожидалось, мы получим исключение NonUniqueObjectException .

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

6.2. StaleStateException

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

Иногда это заворачивается в OptimisticLockException .

Эта ошибка обычно возникает при использовании длительных транзакций с управлением версиями.

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

public void whenUpdatingNonExistingObject_thenStaleStateException() {
thrown.expect(isA(OptimisticLockException.class));
thrown.expectMessage(
"Batch update returned unexpected row count from update");
thrown.expectCause(isA(StaleStateException.class));

Session session = null;
Transaction transaction = null;

try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();

Product product = new Product();
product.setId(15);
product.setName("Product1");
session.update(product);
transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
}
}

Некоторые другие возможные сценарии:

  • мы не указали правильную стратегию несохраненной стоимости для объекта
  • два пользователя пытались удалить одну и ту же строку практически одновременно
  • мы вручную устанавливаем значение в автоматически сгенерированном идентификаторе или поле версии

7. Ошибки ленивой инициализации

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

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

Давайте рассмотрим это исключение и различные способы его исправления.

7.1. LazyInitializationException

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

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

Во- вторых, мы можем получить эту ошибку с помощью Spring Data , если воспользуемся методом getOne . Этот метод лениво извлекает экземпляр.

Есть много способов решить это исключение.

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

Во-вторых, мы можем держать сеанс открытым до тех пор, пока представление не будет отображено. Это известно как « Открытая сессия в представлении » и является антишаблоном. Мы должны избегать этого, так как он имеет несколько недостатков.

В-третьих, мы можем открыть еще один сеанс и повторно подключить объект, чтобы получить отношения. Мы можем сделать это, используя метод слияния в сеансе.

Наконец, мы можем инициализировать необходимые ассоциации на бизнес-уровнях. Мы обсудим это в следующем разделе.

7.2. Инициализация соответствующих ленивых отношений на бизнес-уровне

Есть много способов инициализировать ленивые отношения.

Один из вариантов — инициализировать их, вызвав соответствующие методы объекта. В этом случае Hibernate выдаст несколько запросов к базе данных, что приведет к снижению производительности. Мы называем это проблемой «N+1 SELECT».

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

Наконец, мы можем использовать графы сущностей для определения всех атрибутов, которые нужно извлечь . Мы можем использовать аннотации @NamedEntityGraph, @NamedAttributeNode и @NamedEntitySubgraph для декларативного определения графа сущностей. Мы также можем определить их программно с помощью JPA API. Затем мы извлекаем весь граф за один вызов, указав его в операции выборки .

8. Проблемы с транзакциями

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

Кроме того, Hibernate делегирует управление транзакциями менеджеру транзакций. Если по какой-либо причине транзакцию не удалось запустить, зафиксировать или откатить, Hibernate выдает исключение.

Обычно мы получаем TransactionException или IllegalArgumentException в зависимости от диспетчера транзакций.

В качестве иллюстрации попробуем зафиксировать транзакцию, отмеченную для отката:

public void 
givenTxnMarkedRollbackOnly_whenCommitted_thenTransactionException() {
thrown.expect(isA(TransactionException.class));
thrown.expectMessage(
"Transaction was marked for rollback only; cannot commit");

Session session = null;
Transaction transaction = null;
try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();

Product product = new Product();
product.setId(15);
product.setName("Product1");
session.save(product);
transaction.setRollbackOnly();

transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
}
}

Точно так же другие ошибки также могут вызывать исключение:

  • Смешивание декларативных и программных транзакций
  • Попытка начать транзакцию, когда в сеансе уже активна другая
  • Попытка фиксации или отката без запуска транзакции
  • Попытка зафиксировать или откатить транзакцию несколько раз

9. Проблемы параллелизма

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

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

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

public void whenDeletingADeletedObject_thenOptimisticLockException() {
thrown.expect(isA(OptimisticLockException.class));
thrown.expectMessage(
"Batch update returned unexpected row count from update");
thrown.expectCause(isA(StaleStateException.class));

Session session = null;
Transaction transaction = null;

try {
session = sessionFactory.openSession();
transaction = session.beginTransaction();

Product product = new Product();
product.setId(12);
product.setName("Product 12");
session.save(product1);
transaction.commit();
session.close();

session = sessionFactory.openSession();
transaction = session.beginTransaction();
product = session.get(Product.class, 12);
session.createNativeQuery("delete from Product where id=12")
.executeUpdate();
// We need to refresh to fix the error.
// session.refresh(product);
session.delete(product);
transaction.commit();
} catch (Exception e) {
rollbackTransactionQuietly(transaction);
throw (e);
} finally {
closeSessionQuietly(session);
}
}

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

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

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

10. Заключение

В этой статье мы рассмотрели некоторые распространенные исключения, возникающие при использовании Hibernate. Кроме того, мы исследовали их вероятные причины и решения.

Как обычно, полный исходный код можно найти на GitHub .