From d8f8e767917954650439bb514e57c42967bde7c2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Mar 2025 15:53:24 +0100 Subject: [PATCH] Check potentially more specific HibernateException cause as well Closes gh-34633 --- .../orm/jpa/vendor/HibernateJpaDialect.java | 55 ++++++++++-------- .../orm/jpa/DefaultJpaDialectTests.java | 29 ++++++---- .../hibernate/HibernateJpaDialectTests.java | 58 +++++++++++++++++++ 3 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java index 12a9d75c97b..22635099112 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -253,14 +253,18 @@ public class HibernateJpaDialect extends DefaultJpaDialect { * @return the corresponding DataAccessException instance */ protected DataAccessException convertHibernateAccessException(HibernateException ex) { - if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException jdbcEx) { + return convertHibernateAccessException(ex, ex); + } + + private DataAccessException convertHibernateAccessException(HibernateException ex, HibernateException exToCheck) { + if (this.jdbcExceptionTranslator != null && exToCheck instanceof JDBCException jdbcEx) { DataAccessException dae = this.jdbcExceptionTranslator.translate( "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); if (dae != null) { return dae; } } - if (this.transactionExceptionTranslator != null && ex instanceof org.hibernate.TransactionException) { + if (this.transactionExceptionTranslator != null && exToCheck instanceof org.hibernate.TransactionException) { if (ex.getCause() instanceof SQLException sqlEx) { DataAccessException dae = this.transactionExceptionTranslator.translate( "Hibernate transaction: " + ex.getMessage(), null, sqlEx); @@ -270,74 +274,77 @@ public class HibernateJpaDialect extends DefaultJpaDialect { } } - if (ex instanceof JDBCConnectionException) { + if (exToCheck instanceof JDBCConnectionException) { return new DataAccessResourceFailureException(ex.getMessage(), ex); } - if (ex instanceof SQLGrammarException hibEx) { + if (exToCheck instanceof SQLGrammarException hibEx) { return new InvalidDataAccessResourceUsageException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof QueryTimeoutException hibEx) { + if (exToCheck instanceof QueryTimeoutException hibEx) { return new org.springframework.dao.QueryTimeoutException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof LockAcquisitionException hibEx) { + if (exToCheck instanceof LockAcquisitionException hibEx) { return new CannotAcquireLockException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof PessimisticLockException hibEx) { + if (exToCheck instanceof PessimisticLockException hibEx) { return new PessimisticLockingFailureException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof ConstraintViolationException hibEx) { + if (exToCheck instanceof ConstraintViolationException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]; constraint [" + hibEx.getConstraintName() + "]", ex); } - if (ex instanceof DataException hibEx) { + if (exToCheck instanceof DataException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } // end of JDBCException subclass handling - if (ex instanceof QueryException) { + if (exToCheck instanceof QueryException) { return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); } - if (ex instanceof NonUniqueResultException) { + if (exToCheck instanceof NonUniqueResultException) { return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); } - if (ex instanceof NonUniqueObjectException) { + if (exToCheck instanceof NonUniqueObjectException) { return new DuplicateKeyException(ex.getMessage(), ex); } - if (ex instanceof PropertyValueException) { + if (exToCheck instanceof PropertyValueException) { return new DataIntegrityViolationException(ex.getMessage(), ex); } - if (ex instanceof PersistentObjectException) { + if (exToCheck instanceof PersistentObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof TransientObjectException) { + if (exToCheck instanceof TransientObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof ObjectDeletedException) { + if (exToCheck instanceof ObjectDeletedException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof UnresolvableObjectException hibEx) { + if (exToCheck instanceof UnresolvableObjectException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof WrongClassException hibEx) { + if (exToCheck instanceof WrongClassException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleObjectStateException hibEx) { + if (exToCheck instanceof StaleObjectStateException hibEx) { return new ObjectOptimisticLockingFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleStateException) { + if (exToCheck instanceof StaleStateException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof OptimisticEntityLockException) { + if (exToCheck instanceof OptimisticEntityLockException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof PessimisticEntityLockException) { + if (exToCheck instanceof PessimisticEntityLockException) { if (ex.getCause() instanceof LockAcquisitionException) { return new CannotAcquireLockException(ex.getMessage(), ex.getCause()); } return new PessimisticLockingFailureException(ex.getMessage(), ex); } - // fallback + // Fallback: check potentially more specific cause, otherwise JpaSystemException + if (exToCheck.getCause() instanceof HibernateException causeToCheck) { + return convertHibernateAccessException(ex, causeToCheck); + } return new JpaSystemException(ex); } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java index ac934635a2c..14afaa64a83 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.orm.jpa; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; import org.junit.jupiter.api.Test; import org.springframework.transaction.TransactionDefinition; @@ -33,33 +34,37 @@ import static org.mockito.Mockito.mock; /** * @author Costin Leau * @author Phillip Webb + * @author Juergen Hoeller */ class DefaultJpaDialectTests { - private JpaDialect dialect = new DefaultJpaDialect(); + private final JpaDialect dialect = new DefaultJpaDialect(); - @Test - void testDefaultTransactionDefinition() { - DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); - definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); - assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> - dialect.beginTransaction(null, definition)); - } @Test void testDefaultBeginTransaction() throws Exception { TransactionDefinition definition = new DefaultTransactionDefinition(); EntityManager entityManager = mock(); EntityTransaction entityTx = mock(); - given(entityManager.getTransaction()).willReturn(entityTx); dialect.beginTransaction(entityManager, definition); } + @Test + void testCustomIsolationLevel() { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + + assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> + dialect.beginTransaction(null, definition)); + } + @Test void testTranslateException() { - OptimisticLockException ex = new OptimisticLockException(); - assertThat(dialect.translateExceptionIfPossible(ex).getCause()).isEqualTo(EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex).getCause()); + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java new file mode 100644 index 00000000000..02993f5061b --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.jpa.hibernate; + +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.HibernateException; +import org.hibernate.dialect.lock.OptimisticEntityLockException; +import org.junit.jupiter.api.Test; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaOptimisticLockingFailureException; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class HibernateJpaDialectTests { + + private final JpaDialect dialect = new HibernateJpaDialect(); + + + @Test + void testTranslateException() { + // Plain JPA exception + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); + + // Hibernate-specific exception + ex = new OptimisticEntityLockException("", ""); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + + // Nested Hibernate-specific exception + ex = new HibernateException(new OptimisticEntityLockException("", "")); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + } + +}