Exceções comuns do Hibernate
1. Introdução
Neste tutorial, discutiremos algumas exceções comuns que podemos encontrar ao trabalhar com o Hibernate.
Analisaremos seu propósito e algumas causas comuns. Além disso, examinaremos suas soluções.
2. Visão geral da exceção do Hibernate
Muitas condições podem causar exceções ao usar o Hibernate. Podem ser erros de mapeamento, problemas de infraestrutura, erros de SQL, violações de integridade de dados, problemas de sessão e erros de transação.
These exceptions mostly extend from HibernateException. No entanto, se estivermos usando o Hibernate como um provedor de persistência JPA, essas exceções podem ser agrupadas emPersistenceException.
Ambas as classes base se estendem deRuntimeException. Portanto, eles estão todos desmarcados. Portanto, não precisamos pegá-los ou declará-los em todos os lugares em que são usados.
Além disso,most of these are unrecoverable. As a result, retrying the operation would not help. Isso significa que temos que abandonar a sessão atual ao encontrá-los.
Vamos agora dar uma olhada em cada um deles, um de cada vez.
3. Erros de mapeamento
O mapeamento objeto-relacional é um grande benefício do Hibernate. Especificamente, isso nos liberta da criação manual de instruções SQL.
Ao mesmo tempo, é necessário especificar o mapeamento entre objetos Java e tabelas de banco de dados. Assim, nós os especificamos usando anotações ou através de documentos de mapeamento. Esses mapeamentos podem ser codificados manualmente. Como alternativa, podemos usar ferramentas para gerá-los.
Ao especificar esses mapeamentos, podemos cometer erros. Eles podem estar na especificação de mapeamento. Ou, pode haver uma incompatibilidade entre um objeto Java e a tabela de banco de dados correspondente.
Esses erros de mapeamento geram exceções. Nós os encontramos frequentemente durante o desenvolvimento inicial. Além disso, podemos topar com eles durante a migração de alterações entre ambientes.
Vejamos esses erros com alguns exemplos.
3.1. MappingException
A problem with the object-relational mapping causes a MappingException to be thrown:
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();
}
No código acima, o métodocreateNativeQuery tenta mapear o resultado da consulta para o tipo Java especificadoString.. Ele usa o mapeamento implícito da classeString deMetamodel para fazer o mapeamento.
No entanto, a classeString não tem nenhum mapeamento especificado. Portanto, o Hibernate não sabe como mapear a colunaname paraStringe lança a exceção.
Para uma análise detalhada das possíveis causas e soluções, verifiqueHibernate Mapping Exception – Unknown Entity.
Da mesma forma, outros erros também podem causar esta exceção:
-
Misturando anotações em campos e métodos
-
Falha ao especificar@JoinTable para uma associação@ManyToMany
-
O construtor padrão da classe mapeada lança uma exceção durante o processamento do mapeamento
Além disso,MappingException has a few subclasses which can indicate specific mapping problems:
-
AnnotationException – a problema com uma anotação
-
DuplicateMappingException – mapeamento duplicado para uma classe, tabela ou nome de propriedade
-
InvalidMappingException – mapeamento inválido
-
MappingNotFoundException – recurso de mapeamento não foi encontrado
-
PropertyNotFoundException – an método getter ou setter esperado não pôde ser encontrado em uma classe
Portanto,if we come across this exception, we should first verify our mappings.
3.2. AnnotationException
Para entender osAnnotationException,, vamos criar uma entidade sem uma anotação de identificador em qualquer campo ou propriedade:
@Entity
public class EntityWithNoId {
private int id;
public int getId() {
return id;
}
// standard setter
}
ComoHibernate expects every entity to have an identifier, obteremos umAnnotationException quando usarmos a entidade:
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();
}
Além disso, algumas outras causas prováveis são:
-
Gerador de sequência desconhecido usado na anotação@GeneratedValue
-
Anotação@Temporal usada com uma classe Java 8Date /Time
-
Entidade de destino ausente ou inexistente para@ManyToOne ou@OneToMany
-
Raw collection classes usado com anotações de relacionamento@OneToMany ou@ManyToMany
-
Classes concretas usadas com as anotações de coleção@OneToMany,@ManyToMany ou@ElementCollection, pois o Hibernate espera as interfaces de coleção
Para resolver essa exceção, primeiro verifique a anotação específica mencionada na mensagem de erro.
4. Erros de gerenciamento de esquema
O gerenciamento automático de esquema de banco de dados é outro benefício do Hibernate. Por exemplo, ele pode gerar instruções DDL para criar ou validar objetos de banco de dados.
Para usar este recurso, precisamos definir ahibernate.hbm2ddl.auto property de forma adequada.
Se houver problemas ao executar o gerenciamento de esquema, obteremos uma exceção. Vamos examinar esses erros.
4.1. SchemaManagementException
Qualquer problema relacionado à infraestrutura na execução do gerenciamento de esquema causaSchemaManagementException.
Para demonstrar, vamos instruir o Hibernate para validar o esquema do banco de dados:
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();
}
Como a tabela correspondente aProduct não está presente no banco de dados, obtemos a exceção de validação de esquema ao construir o SessionFactory.
Além disso, existem outros cenários possíveis para essa exceção:
-
incapaz de conectar-se ao banco de dados para executar tarefas de gerenciamento de esquema
-
o esquema não está presente no banco de dados
4.2. CommandAcceptanceException
Qualquer problema ao executar um DDL correspondente a um comando de gerenciamento de esquema específico pode causarCommandAcceptanceException.
Como exemplo, vamos especificar o dialeto errado ao configurar oSessionFactory:
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();
}
Aqui, especificamos o dialeto errado:MySQLDialect. Além disso, estamos instruindo o Hibernate a atualizar os objetos de esquema. Consequentemente, as instruções DDL executadas pelo Hibernate para atualizar o banco de dados H2 falharão e obteremos uma exceção.
By default, Hibernate silently logs this exception and moves on. Quando usarmos mais tarde oSessionFactory, we obteremos a exceção.
Para garantir que uma exceção seja lançada neste erro, definimos a propriedadeHBM2DDL_HALT_ON_ERROR paratrue.
Da mesma forma, existem outras causas comuns para esse erro:
-
Há uma incompatibilidade nos nomes das colunas entre o mapeamento e o banco de dados
-
Duas classes são mapeadas para a mesma tabela
-
O nome usado para uma classe ou tabela é uma palavra reservada no banco de dados, comoUSER, por exemplo
-
O usuário usado para se conectar ao banco de dados não possui o privilégio necessário
5. Erros de execução SQL
When we insert, update, delete or query data using Hibernate, it executes DML statements against the database using JDBC. Esta API gera umSQLException se a operação resultar em erros ou avisos.
O Hibernate converte esta exceção emJDBCException ou uma de suas subclasses adequadas:
-
ConstraintViolationException
-
DataException
-
JDBCConnectionException
-
LockAcquisitionException
-
PessimisticLockException
-
QueryTimeoutException
-
SQLGrammarException
-
GenericJDBCException
Vamos discutir erros comuns.
5.1. JDBCException
JDBCException é sempre causado por uma instrução SQL específica. Podemos chamar o métodogetSQL para obter a instrução SQL ofensiva.
Além disso, podemos recuperar oSQLException subjacente com o métodogetSQLException.
5.2. SQLGrammarException
SQLGrammarException indica que o SQL enviado ao banco de dados era inválido. Pode ser devido a um erro de sintaxe ou a uma referência de objeto inválida.
Por exemplo,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();
}
Além disso, podemos obter esse erro ao salvar dados se a tabela estiver ausente:
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);
}
}
Algumas outras causas possíveis são:
-
A estratégia de nomenclatura usada não mapeia as classes para as tabelas corretas
-
A coluna especificada em@JoinColumn não existe
5.3. ConstraintViolationException
UmConstraintViolationException indica que a operação DML solicitada causou a violação de uma restrição de integridade. Podemos obter o nome dessa restrição chamando o métodogetConstraintName.
Uma causa comum para essa exceção é tentar salvar registros duplicados:
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);
}
}
}
Além disso, salvar um valornull em uma colunaNOT NULL no banco de dados pode gerar esse erro.
Para resolver este erro,we should perform all validations in the business layer. Além disso, as restrições do banco de dados não devem ser usadas para fazer validações de aplicativos.
5.4. DataException
DataException indica que a avaliação de uma instrução SQL resultou em alguma operação ilegal, incompatibilidade de tipo ou cardinalidade incorreta.
Por exemplo, o uso de dados de caracteres em uma coluna numérica pode causar este erro:
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 id='wrongTypeId'", Product.class);
query.getResultList();
}
Para corrigir esse erro,we should ensure that the data types and length match between the application code and the database.
5.5. JDBCConnectionException
UmJDBCConectionException indica problemas de comunicação com o banco de dados.
Por exemplo, um banco de dados ou rede em queda pode fazer com que essa exceção seja lançada.
Além disso, uma configuração incorreta do banco de dados pode causar essa exceção. Um desses casos é a conexão do banco de dados sendo fechada pelo servidor porque ficou inativa por um longo tempo. Isso pode acontecer se estivermos usandoconnection poolinge a configuração de tempo limite de inatividade no pool for maior do que o valor de tempo limite de conexão no banco de dados.
Para resolver este problema, devemos primeiro garantir que o host do banco de dados esteja presente e ativo. Em seguida, devemos verificar se a autenticação correta é usada para a conexão com o banco de dados. Por fim, devemos verificar se o valor do tempo limite está definido corretamente no conjunto de conexões.
5.6. QueryTimeoutException
Quando uma consulta ao banco de dados atinge o tempo limite, obtemos essa exceção. Também podemos vê-lo devido a outros erros, como o espaço de tabela ficando cheio.
Este é um dos poucos erros recuperáveis, o que significa que podemos tentar novamente a instrução na mesma transação.
Para corrigir esse problema,we can increase the query timeout for long-running queries de várias maneiras:
-
Defina o elementotimeout em uma anotação@NamedQuery ou@NamedNativeQuery
-
Invoque o método setHint da interfacethe Query
-
Chame o métodosetTimeout da interfaceTransaction
-
Invoque o métodosetTimeout da interfaceQuery
6. Erros relacionados ao estado da sessão
Vamos agora dar uma olhada nos erros devido a erros de uso da sessão do Hibernate.
6.1. NonUniqueObjectException
O Hibernate não permite dois objetos com o mesmo identificador em uma única sessão.
Se tentarmos associar duas instâncias da mesma classe Java com o mesmo identificador em uma única sessão, obteremos umNonUniqueObjectException. Podemos obter o nome e o identificador da entidade chamando os métodosgetEntityName()egetIdentifier().
Para reproduzir este erro, vamos tentar salvar duas instâncias deProduct com o mesmo id com uma sessão:
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);
}
}
Teremos umNonUniqueObjectException, conforme o esperado.
This exception occurs frequently while reattaching a detached object with a session by calling the update method. Se a sessão tiver outra instância com o mesmo identificador carregado, obteremos este erro. Para corrigir isso,we can use the merge method para reconectar o objeto destacado.
6.2. StaleStateException
O Hibernate lançaStaleStateExceptions quando o número da versão ou verificação de carimbo de data / hora falha. Indica que a sessão continha dados obsoletos.
Às vezes, isso fica embrulhado emOptimisticLockException.
Este erro geralmente ocorre durante o uso de transações de longa duração com controle de versão.
Além disso, também pode acontecer ao tentar atualizar ou excluir uma entidade se a linha do banco de dados correspondente não existir:
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);
}
}
Alguns outros cenários possíveis são:
-
não especificamos uma estratégia de valor não salvo adequada para a entidade
-
dois usuários tentaram excluir a mesma linha quase ao mesmo tempo
-
definimos manualmente um valor no campo ID ou versão gerada automaticamente
7. Erros de inicialização lenta
Geralmente, configuramos associações a serem carregadas com preguiça para melhorar o desempenho do aplicativo. As associações são obtidas apenas quando são usadas pela primeira vez.
No entanto, o Hibernate requer uma sessão ativa para buscar dados. Se a sessão já estiver fechada quando tentamos acessar uma associação não inicializada, obtemos uma exceção.
Vejamos essa exceção e as várias maneiras de corrigi-la.
7.1. LazyInitializationException
LazyInitializationException indicates an attempt to load uninitialized data outside an active session. Podemos obter esse erro em muitos cenários.
Primeiro, podemos obter essa exceção ao acessar um relacionamento lento na camada de apresentação. O motivo é que a entidade foi parcialmente carregada na camada de negócios e a sessão foi fechada.
Em segundo lugar, podemos obter esse erro comSpring Data se usarmos o métodogetOne. Esse método busca preguiçosamente a instância.
Existem várias maneiras de resolver essa exceção.
Primeiro de tudo, podemos fazer todos os relacionamentos ansiosamente carregados. Porém, isso afetaria o desempenho do aplicativo porque carregaremos dados que não serão usados.
Em segundo lugar, podemos manter a sessão aberta até que a exibição seja renderizada. Isso é conhecido como “https://vladmihalcea.com/the-open-session-in-view-anti-pattern/[Open Session in View]” e é um antipadrão. Devemos evitar isso, pois tem várias desvantagens.
Em terceiro lugar, podemos abrir outra sessão e reconectar a entidade para buscar os relacionamentos. Podemos fazer isso usando o métodomerge na sessão.
Por fim, podemos inicializar as associações necessárias nas camadas de negócios. Discutiremos isso na próxima seção.
7.2. Inicializando relacionamentos preguiçosos relevantes na camada de negócios
Existem muitas maneiras de inicializar relacionamentos preguiçosos.
Uma opção é inicializá-los chamando os métodos correspondentes na entidade. Nesse caso, o Hibernate emitirá várias consultas ao banco de dados, causando desempenho degradado. Nós o referimos como o problema “N + 1 SELECT”.
Em segundo lugar, podemos usarFetch Join para obter os dados em uma única consulta. No entanto, precisamos escrever um código personalizado para conseguir isso.
Finalmente,we can use entity graphs to define all the attributes to be fetched. Podemos usar as anotações@NamedEntityGraph, @NamedAttributeNode e@NamedEntitySubgraph para definir declarativamente o gráfico da entidade. Também podemos defini-los programaticamente com a API JPA. Então,we retrieve the entire graph in a single call by specifying it in the fetch operation.
8. Problemas de transação
Transactions define unidades de trabalho e isolamento entre atividades simultâneas. Podemos demarcá-los de duas maneiras diferentes. Primeiro, podemos defini-los declarativamente usando anotações. Em segundo lugar, podemos gerenciá-los programaticamente usandoHibernate Transaction API.
Além disso, o Hibernate delega o gerenciamento de transações a um gerenciador de transações. If a transaction could not be started, committed or rolled back due to any reason, Hibernate throws an exception.
Normalmente obtemos umTransactionException or umIllegalArgumentException dependendo do gerenciador de transações.
Como ilustração, vamos tentar confirmar uma transação que foi marcada para 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);
}
}
Da mesma forma, outros erros também podem causar uma exceção:
-
Combinando transações declarativas e programáticas
-
Tentativa de iniciar uma transação quando outra já estiver ativa na sessão
-
Tentando confirmar ou retroceder sem iniciar uma transação
-
Tentando confirmar ou reverter uma transação várias vezes
9. Problemas de simultaneidade
O Hibernate suporta duas estratégiaslocking para evitar inconsistência do banco de dados devido a transações simultâneas -optimisticepessimistic. Ambos levantam uma exceção em caso de conflito de bloqueio.
Para oferecer suporte a alta simultaneidade e alta escalabilidade, normalmente usamos controle otimista de simultaneidade com verificação de versão. Isso usa números de versão ou carimbos de data e hora para detectar atualizações conflitantes.
OptimisticLockingException is thrown to indicate an optimistic locking conflict. Por exemplo, obtemos esse erro se realizarmos duas atualizações ou exclusões da mesma entidade sem atualizá-lo após a primeira operação:
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);
}
}
Da mesma forma, também podemos obter esse erro se dois usuários tentarem atualizar a mesma entidade quase ao mesmo tempo. Nesse caso, o primeiro pode ter êxito e o segundo gera esse erro.
Portanto,we cannot completely avoid this error without introducing pessimistic locking. No entanto, podemos minimizar a probabilidade de sua ocorrência, fazendo o seguinte:
-
Mantenha as operações de atualização o mais curtas possível
-
Atualizar representações da entidade no cliente o mais rápido possível
-
Não armazene em cache a entidade ou qualquer objeto de valor que a represente
-
Sempre atualize a representação da entidade no cliente após a atualização
10. Conclusão
Neste artigo, analisamos algumas exceções comuns encontradas ao usar o Hibernate. Além disso, investigamos suas prováveis causas e resoluções.
Como de costume, o código-fonte completo pode ser encontradoover on GitHub.