«1. Введение

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

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

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

Многие условия могут вызывать исключения при использовании 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 – 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. аннотации коллекции @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 DataException JDBCConnectionException LockAcquisitionException PessimisticLockException QueryTimeoutException SQLGrammarException GenericJDBCException

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

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

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

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

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

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

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.