Често срещани изключения за хибернация

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 query = session .createNativeQuery("select name from PRODUCT", String.class); query.getResultList(); }

В горния код методът createNativeQuery се опитва да приведе резултата от заявката към посочения Java тип String. Той използва неявното картографиране на класа String от Metamodel, за да извърши картографирането.

Въпреки това, String класа не разполага с никаква картографиране е посочено. Следователно Hibernate не знае как да преобразува колоната с имена в String и изхвърля изключението.

За подробен анализ на възможни причини и решения, вижте 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, не присъства в базата данни, получаваме изключение за проверка на схемата, докато изграждаме S essionFactory .

Освен това има и други възможни сценарии за това изключение:

  • не може да се свърже с базата данни за изпълнение на задачи за управление на схеми
  • the schema is not present in the database

4.2. CommandAcceptanceException

Any problem executing a DDL corresponding to a specific schema management command can cause a CommandAcceptanceException.

As an example, let's specify the wrong dialect while setting up the 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(); }

Here, we've specified the wrong dialect: MySQLDialect. Also, we're instructing Hibernate to update the schema objects. Consequently, the DDL statements executed by Hibernate to update the H2 database will fail and we'll get an exception.

By default, Hibernate silently logs this exception and moves on. When we later use the SessionFactory, we get the exception.

To ensure that an exception is thrown on this error, we've set the property HBM2DDL_HALT_ON_ERROR to true.

Similarly, these are some other common causes for this error:

  • There is a mismatch in column names between mapping and the database
  • Two classes are mapped to the same table
  • The name used for a class or table is a reserved word in the database, like USER, for example
  • The user used to connect to the database does not have the required privilege

5. SQL Execution Errors

When we insert, update, delete or query data using Hibernate, it executes DML statements against the database using JDBC. This API raises an SQLException if the operation results in errors or warnings.

Hibernate converts this exception into JDBCException or one of its suitable subclasses:

  • ConstraintViolationException
  • DataException
  • JDBCConnectionException
  • LockAcquisitionException
  • PessimisticLockException
  • QueryTimeoutException
  • SQLGrammarException
  • GenericJDBCException

Let's discuss common errors.

5.1. JDBCException

JDBCException is always caused by a particular SQL statement. We can call the getSQL method to get the offending SQL statement.

Furthermore, we can retrieve the underlying SQLException with the getSQLException method.

5.2. SQLGrammarException

SQLGrammarException indicates that the SQL sent to the database was invalid. It could be due to a syntax error or an invalid object reference.

For example, a missing table can result in this error while querying data:

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 query = session.createNativeQuery( "select * from NON_EXISTING_TABLE", Product.class); query.getResultList(); }

Also, we can get this error while saving data if the table is missing:

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); } }

Some other possible causes are:

  • The naming strategy used doesn't map the classes to the correct tables
  • The column specified in @JoinColumn doesn't exist

5.3. ConstraintViolationException

A ConstraintViolationException indicates that the requested DML operation caused an integrity constraint to be violated. We can get the name of this constraint by calling the getConstraintName method.

A common cause of this exception is trying to save duplicate records:

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); } } }

Also, saving a null value to a NOT NULL column in the database can raise this error.

In order to resolve this error, we should perform all validations in the business layer. Furthermore, database constraints should not be used to do application validations.

5.4. DataException

DataException indicates that the evaluation of an SQL statement resulted in some illegal operation, type mismatch or incorrect cardinality.

For instance, using character data against a numeric column can cause this error:

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 query = session.createNativeQuery( "select * from PRODUCT where", Product.class); query.getResultList(); }

To fix this error, we should ensure that the data types and length match between the application code and the database.

5.5. JDBCConnectionException

A JDBCConectionException indicates problems communicating with the database.

For example, a database or network going down can cause this exception to be thrown.

Additionally, an incorrect database setup can cause this exception. One such case is the database connection being closed by the server because it was idle for a long time. This can happen if we're using connection pooling and the idle timeout setting on the pool is more than the connection timeout value in the database.

To solve this problem, we should first ensure that the database host is present and that it's up. Then, we should verify that the correct authentication is used for the database connection. Finally, we should check that the timeout value is correctly set on the connection pool.

5.6. QueryTimeoutException

When a database query times out, we get this exception. We can also see it due to other errors, such as the tablespace becoming full.

This is one of the few recoverable errors, which means that we can retry the statement in the same transaction.

To fix this issue, we can increase the query timeout for long-running queries in multiple ways:

  • Set the timeout element in a @NamedQuery or @NamedNativeQuery annotation
  • Invoke the setHint method of the Query interface
  • Call the setTimeout method of the Transaction interface
  • Invoke the setTimeout method of the Query interface

6. Session-State-Related Errors

Let's now look into errors due to Hibernate session usage errors.

6.1. NonUniqueObjectException

Hibernate doesn't allow two objects with the same identifier in a single session.

If we try to associate two instances of the same Java class with the same identifier in a single session, we get a NonUniqueObjectException. We can get the name and identifier of the entity by calling the getEntityName() and getIdentifier() methods.

To reproduce this error, let's try to save two instances of Product with the same id with a session:

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); } }

We'll get a NonUniqueObjectException, as expected.

This exception occurs frequently while reattaching a detached object with a session by calling the update method. If the session has another instance with the same identifier loaded, then we get this error. In order to fix this, we can use the merge method to reattach the detached object.

6.2. StaleStateException

Hibernate throws StaleStateExceptions when the version number or timestamp check fails. It indicates that the session contained stale data.

Sometimes this gets wrapped into an OptimisticLockException.

This error usually happens while using long-running transactions with versioning.

In addition, it can also happen while trying to update or delete an entity if the corresponding database row doesn't exist:

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); } }

Some other possible scenarios are:

  • we did not specify a proper unsaved-value strategy for the entity
  • two users tried to delete the same row at almost the same time
  • we manually set a value in the autogenerated ID or version field

7. Lazy Initialization Errors

We usually configure associations to be loaded lazily in order to improve application performance. The associations are fetched only when they're first used.

However, Hibernate requires an active session to fetch data. If the session is already closed when we try to access an uninitialized association, we get an exception.

Let's look into this exception and the various ways to fix it.

7.1. LazyInitializationException

LazyInitializationException indicates an attempt to load uninitialized data outside an active session. We can get this error in many scenarios.

First, we can get this exception while accessing a lazy relationship in the presentation layer. The reason is that the entity was partially loaded in the business layer and the session was closed.

Secondly, we can get this error with Spring Data if we use the getOne method. This method lazily fetches the instance.

There are many ways to solve this exception.

First of all, we can make all relationships eagerly loaded. But, this would impact the application performance because we'll be loading data that won't be used.

Secondly, we can keep the session open until the view is rendered. This is known as the “Open Session in View” and it's an anti-pattern. We should avoid this as it has several disadvantages.

Thirdly, we can open another session and reattach the entity in order to fetch the relationships. We can do so by using the merge method on the session.

Finally, we can initialize the required associations in the business layers. We'll discuss this in the next section.

7.2. Initializing Relevant Lazy Relationships in the Business Layer

There are many ways to initialize lazy relationships.

One option is to initialize them by invoking the corresponding methods on the entity. In this case, Hibernate will issue multiple database queries causing degraded performance. We refer to it as the “N+1 SELECT” problem.

Secondly, we can use Fetch Join to get the data in a single query. However, we need to write custom code to achieve this.

Finally, we can use entity graphs to define all the attributes to be fetched. We can use the annotations @NamedEntityGraph, @NamedAttributeNode, and @NamedEntitySubgraph to declaratively define the entity graph. We can also define them programmatically with the JPA API. Then, we retrieve the entire graph in a single call by specifying it in the fetch operation.

8. Transaction Issues

Transactions define units of work and isolation between concurrent activities. We can demarcate them in two different ways. First, we can define them declaratively using annotations. Second, we can manage them programmatically using the Hibernate Transaction API.

Furthermore, Hibernate delegates the transaction management to a transaction manager. If a transaction could not be started, committed or rolled back due to any reason, Hibernate throws an exception.

We usually get a TransactionException or an IllegalArgumentException depending on the transaction manager.

As an illustration, let's try to commit a transaction which has been marked for rollback:

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); } }

Similarly, other errors can also cause an exception:

  • Mixing declarative and programmatic transactions
  • Attempting to start a transaction when another one is already active in the session
  • Trying to commit or rollback without starting a transaction
  • Trying to commit or rollback a transaction multiple times

9. Concurrency Issues

Hibernate supports two locking strategies to prevent database inconsistency due to concurrent transactions – optimistic and pessimistic. Both of them raise an exception in case of a locking conflict.

To support high concurrency and high scalability, we typically use optimistic concurrency control with version checking. This uses version numbers or timestamps to detect conflicting updates.

OptimisticLockingException is thrown to indicate an optimistic locking conflict. For instance, we get this error if we perform two updates or deletes of the same entity without refreshing it after the first operation:

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.