From 453f879af32c727c764159f72d03b746c8a58627 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 28 Jan 2022 14:34:22 +0100 Subject: [PATCH] Upgrade to Hibernate and Hibernate Envers 6. TypedQuery inspection through its String representations seems to be a bit flaky in Hibernate 6 still [0]. Tweaked the code to extract a query string from a query object to try the new way first but fall back to the old way, as this seems to work under some conditions, too. Adapted the test case in which we could rather inspect the new SqmQuery API for test result verification. Re-bootstrapping the EntityManagerFactory for the same persistence unit causes the second bootstrap to fail als apparently foreign key names are randomized and the second bootstrap doesn't create a new constraint but tries to work with a new name. Tweaked the offending test case to reuse the existing EMF declaration as it actually only tests the qualified wiring into clients. Applying an entity graph is causing a StackOverflow in current Hibernate 6. Filed an issue [1] and disabled the test case for now. Dial back on the flip to use String as parameter type for like expression escape characters as Eclipselink rejects that. The JPA spec chapter 4.2.10 allows both Character and String to be used. Filed [2] with Hibernate to ask for reintroduction of the support for characters and commented out the test cases for now. Deprecated CustomHsqlHibernateJpaVendorAdapter as it's not needed on Hibernate 6 anymore. Rewrote HibernateJpaParametersParameterAccessor to use Hibernate 6 API. Add Hibernate 6 upgrade information to the reference docs. Related ticket: #2423. [0] https://hibernate.atlassian.net/browse/HHH-15389 [1] https://hibernate.atlassian.net/browse/HHH-15391 [2] https://hibernate.atlassian.net/browse/HHH-15392 --- pom.xml | 4 +- spring-data-envers/pom.xml | 4 +- spring-data-jpa/pom.xml | 15 +++-- ...bernateJpaParametersParameterAccessor.java | 29 +++++++--- .../data/jpa/provider/HibernateUtils.java | 15 ++++- .../jpa/provider/PersistenceProvider.java | 16 ++++-- .../CustomHsqlHibernateJpaVendorAdaptor.java | 24 +++----- ...raphRepositoryMethodsIntegrationTests.java | 7 ++- .../repository/UserRepositoryFinderTests.java | 18 ++++++ .../jpa/repository/UserRepositoryTests.java | 2 +- .../JpaCountQueryCreatorIntegrationTests.java | 27 ++++++--- .../JpaParametersParameterAccessorTests.java | 16 +++--- .../ParameterExpressionProviderTests.java | 7 +-- .../PartTreeJpaQueryIntegrationTests.java | 43 ++++++++------- .../jpa/repository/sample/UserRepository.java | 4 +- ...egistrarPostProcessorIntegrationTests.java | 55 ++++--------------- .../src/test/resources/hibernate.xml | 2 +- src/main/asciidoc/new-features.adoc | 24 ++++++++ 18 files changed, 184 insertions(+), 128 deletions(-) diff --git a/pom.xml b/pom.xml index 6a6275432..8a1565932 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 3.0.2 - 5.6.9.Final + 6.1.1.Final 4.3 8.0.23 42.2.19 @@ -44,7 +44,7 @@ - + spring-data-envers spring-data-jpa spring-data-jpa-distribution diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 59eb8eab8..540834bb6 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -62,8 +62,8 @@ - org.hibernate - hibernate-envers-jakarta + org.hibernate.orm + hibernate-envers ${hibernate.envers} diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index ba9220cc2..3ad9c39a2 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -145,19 +145,26 @@ - ${hibernate.groupId} - hibernate-core-jakarta + ${hibernate.groupId}.orm + hibernate-core ${hibernate} true - ${hibernate.groupId} - hibernate-jpamodelgen-jakarta + ${hibernate.groupId}.orm + hibernate-jpamodelgen ${hibernate} provided + + jakarta.xml.bind + jakarta.xml.bind-api + ${jaxb} + provided + + jakarta.annotation jakarta.annotation-api diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateJpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateJpaParametersParameterAccessor.java index 918ec58af..38eba8d73 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateJpaParametersParameterAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateJpaParametersParameterAccessor.java @@ -16,14 +16,15 @@ package org.springframework.data.jpa.provider; import jakarta.persistence.EntityManager; -import org.hibernate.SessionFactory; -import org.hibernate.TypeHelper; -import org.hibernate.jpa.TypedParameterValue; -import org.hibernate.type.Type; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.TypedParameterValue; +import org.hibernate.type.BasicTypeRegistry; import org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.lang.Nullable; /** * {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance. In @@ -33,11 +34,13 @@ import org.springframework.data.repository.query.ParametersParameterAccessor; * @author Wonchul Heo * @author Jens Schauder * @author Cedomir Igaly + * @author Robert Wilson + * @author Oliver Drotbohm * @since 2.7 */ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAccessor { - private final TypeHelper typeHelper; + private final BasicTypeRegistry typeHelper; /** * Creates a new {@link ParametersParameterAccessor}. @@ -50,21 +53,29 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce super(parameters, values); - this.typeHelper = em.getEntityManagerFactory().unwrap(SessionFactory.class).getTypeHelper(); + this.typeHelper = em.getEntityManagerFactory() + .unwrap(SessionFactoryImplementor.class) + .getTypeConfiguration() + .getBasicTypeRegistry(); } @Override + @Nullable + @SuppressWarnings("unchecked") public Object getValue(Parameter parameter) { - Object value = super.getValue(parameter.getIndex()); + var value = super.getValue(parameter.getIndex()); + if (value != null) { return value; } - Type type = typeHelper.basic(parameter.getType()); + var type = typeHelper.getRegisteredType(parameter.getType()); + if (type == null) { return null; } - return new TypedParameterValue(type, null); + + return new TypedParameterValue<>(type, null); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java index 3c6a3556f..2056db61d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.provider; import org.hibernate.query.Query; +import org.hibernate.query.spi.SqmQuery; import org.springframework.lang.Nullable; /** @@ -41,8 +42,20 @@ public abstract class HibernateUtils { @Nullable public static String getHibernateQuery(Object query) { + try { + + // Try the new Hibernate implementation first + if (query instanceof SqmQuery) { + return ((SqmQuery) query).getSqmStatement().toHqlString(); + } + + // Couple of cases in which this still breaks, see HHH-15389 + } catch (RuntimeException o_O) {} + + // Try the old way, as it still works in some cases (haven't investigated in which exactly) + if (query instanceof Query) { - return ((Query) query).getQueryString(); + return ((Query) query).getQueryString(); } else { throw new IllegalArgumentException("Don't know how to extract the query string from " + query); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index f2d90b0c8..f3ef2f047 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -24,7 +24,9 @@ import jakarta.persistence.metamodel.IdentifiableType; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; +import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.NoSuchElementException; import java.util.Set; @@ -186,6 +188,8 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer } }; + private static final Collection ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA); + static ConcurrentReferenceHashMap, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>(); private final Iterable entityManagerClassNames; private final Iterable metamodelClassNames; @@ -233,7 +237,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer return cachedProvider; } - for (PersistenceProvider provider : values()) { + for (PersistenceProvider provider : ALL) { for (String entityManagerClassName : provider.entityManagerClassNames) { if (isEntityManagerOfType(em, entityManagerClassName)) { return cacheAndReturn(entityManagerType, provider); @@ -317,9 +321,9 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager"; String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager"; // needed as Spring only exposes that interface via the EM proxy - String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.jpa.HibernateEntityManager"; + String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor"; - String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.internal.MetamodelImpl"; + String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; } @@ -337,7 +341,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer */ private static class HibernateScrollableResultsIterator implements CloseableIterator { - private final @Nullable ScrollableResults scrollableResults; + private final @Nullable ScrollableResults scrollableResults; /** * Creates a new {@link HibernateScrollableResultsIterator} for the given {@link Query}. @@ -346,7 +350,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer */ HibernateScrollableResultsIterator(Query jpaQuery) { - org.hibernate.query.Query query = jpaQuery.unwrap(org.hibernate.query.Query.class); + org.hibernate.query.Query query = jpaQuery.unwrap(org.hibernate.query.Query.class); this.scrollableResults = query.setReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())// .scroll(ScrollMode.FORWARD_ONLY); } @@ -359,7 +363,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer } // Cast needed for Hibernate 6 compatibility - Object[] row = (Object[]) scrollableResults.get(); + Object[] row = scrollableResults.get(); return row.length == 1 ? row[0] : row; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomHsqlHibernateJpaVendorAdaptor.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomHsqlHibernateJpaVendorAdaptor.java index b4dda054b..820123828 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomHsqlHibernateJpaVendorAdaptor.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomHsqlHibernateJpaVendorAdaptor.java @@ -15,8 +15,6 @@ */ package org.springframework.data.jpa.repository; -import java.sql.Types; - import org.hibernate.dialect.HSQLDialect; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -24,26 +22,22 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; /** * Fix for missing type declarations for HSQL. * - * @see https://www.codesmell.org/blog/2008/12/hibernate-hsql-native-queries-and-booleans/ + * @see https://www.codesmell.org/blog/2008/12/hibernate-hsql-native-queries-and-booleans/ * @author Oliver Gierke + * @deprecated since 3.0 without replacement as it's not needed anymore. */ +@Deprecated public class CustomHsqlHibernateJpaVendorAdaptor extends HibernateJpaVendorAdapter { @Override protected Class determineDatabaseDialectClass(Database database) { - - if (Database.HSQL.equals(database)) { - return CustomHsqlDialect.class; - } - return super.determineDatabaseDialectClass(database); } - public static class CustomHsqlDialect extends HSQLDialect { - - public CustomHsqlDialect() { - registerColumnType(Types.BOOLEAN, "boolean"); - registerHibernateType(Types.BOOLEAN, "boolean"); - } - } + /** + * @deprecated since 3.0 without replacement as it's not needed anymore. + */ + @Deprecated + public static class CustomHsqlDialect extends HSQLDialect {} } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java index 67368194f..224a3e251 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java @@ -18,8 +18,6 @@ package org.springframework.data.jpa.repository; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.jpa.support.EntityManagerTestUtils.*; -import java.util.List; - import jakarta.persistence.EntityManager; import jakarta.persistence.Persistence; import jakarta.persistence.PersistenceUtil; @@ -28,12 +26,14 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import java.util.List; + import org.assertj.core.api.SoftAssertions; import org.junit.Assume; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -94,6 +94,7 @@ public class EntityGraphRepositoryMethodsIntegrationTests { } @Test // DATAJPA-612 + @Disabled // HHH-15391 void shouldRespectConfiguredJpaEntityGraph() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 6d89b1def..3241a237f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -16,8 +16,11 @@ package org.springframework.data.jpa.repository; import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; import static org.springframework.data.domain.Sort.Direction.*; +import jakarta.persistence.EntityManager; + import java.util.Arrays; import java.util.List; @@ -34,6 +37,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.sample.RoleRepository; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -55,6 +59,9 @@ public class UserRepositoryFinderTests { @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + PersistenceProvider provider; private User dave; private User carter; @@ -73,6 +80,8 @@ public class UserRepositoryFinderTests { dave = userRepository.save(new User("Dave", "Matthews", "dave@dmband.com", singer)); carter = userRepository.save(new User("Carter", "Beauford", "carter@dmband.com", singer, drummer)); oliver = userRepository.save(new User("Oliver August", "Matthews", "oliver@dmband.com")); + + provider = PersistenceProvider.fromEntityManager(em); } @AfterEach @@ -226,6 +235,9 @@ public class UserRepositoryFinderTests { @Test // DATAJPA-1519 void escapingInLikeSpels() { + // HHH-15392 + assumeFalse(provider.equals(PersistenceProvider.HIBERNATE)); + User extra = new User("extra", "Matt_ew", "extra"); userRepository.save(extra); @@ -236,6 +248,9 @@ public class UserRepositoryFinderTests { @Test // DATAJPA-1522 void escapingInLikeSpelsInThePresenceOfEscapeCharacters() { + // HHH-15392 + assumeFalse(provider.equals(PersistenceProvider.HIBERNATE)); + User withEscapeCharacter = userRepository.save(new User("extra", "Matt\\xew", "extra1")); userRepository.save(new User("extra", "Matt\\_ew", "extra2")); @@ -245,6 +260,9 @@ public class UserRepositoryFinderTests { @Test // DATAJPA-1522 void escapingInLikeSpelsInThePresenceOfEscapedWildcards() { + // HHH-15392 + assumeFalse(provider.equals(PersistenceProvider.HIBERNATE)); + userRepository.save(new User("extra", "Matt\\xew", "extra1")); User withEscapedWildcard = userRepository.save(new User("extra", "Matt\\_ew", "extra2")); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 25dcfcdf6..877c8c905 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -2670,7 +2670,7 @@ public class UserRepositoryTests { @Test // DATAJPA-1233 void handlesCountQueriesWithLessParametersMoreThanOneIndexed() { - repository.findAllOrderedBySpecialNameMultipleParamsIndexed("Oliver", "x", PageRequest.of(2, 3)); + repository.findAllOrderedBySpecialNameMultipleParamsIndexed("x", "Oliver", PageRequest.of(2, 3)); } // DATAJPA-928 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java index f42756621..ef48b68fc 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java @@ -17,19 +17,22 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import java.lang.reflect.Method; -import java.util.List; - import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.TypedQuery; +import java.lang.reflect.Method; +import java.util.List; + +import org.hibernate.query.spi.SqmQuery; +import org.hibernate.query.sqm.tree.expression.SqmDistinct; +import org.hibernate.query.sqm.tree.expression.SqmFunction; +import org.hibernate.query.sqm.tree.select.SqmSelectClause; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.jpa.provider.HibernateUtils; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; @@ -68,10 +71,20 @@ public class JpaCountQueryCreatorIntegrationTests { TypedQuery query = entityManager.createQuery(creator.createQuery()); - assertThat(HibernateUtils.getHibernateQuery(query)).startsWith("select distinct count(distinct"); + SqmQuery sqmQuery = ((SqmQuery) query); + SqmSelectStatement select = (SqmSelectStatement) sqmQuery.getSqmStatement(); + + // Verify distinct (should this even be there for a count query?) + SqmSelectClause clause = select.getQuerySpec().getSelectClause(); + assertThat(clause.isDistinct()).isTrue(); + + // Verify count(distinct(…)) + SqmFunction function = ((SqmFunction) clause.getSelectionItems().get(0)); + assertThat(function.getFunctionName()).isEqualTo("count"); + assertThat(function.getArguments().get(0)).isInstanceOf(SqmDistinct.class); } interface SomeRepository extends Repository { - void findDistinctByRolesIn(List roles); + List findDistinctByRolesIn(List roles); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java index 059fc78ec..4577e8145 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java @@ -4,14 +4,13 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.lang.reflect.Method; - import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; -import org.hibernate.jpa.TypedParameterValue; -import org.hibernate.type.StandardBasicTypes; +import java.lang.reflect.Method; + +import org.hibernate.query.TypedParameterValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -44,7 +43,8 @@ class JpaParametersParameterAccessorTests { Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class); Object[] values = { null }; JpaParameters parameters = new JpaParameters(withNativeQuery); - JpaParametersParameterAccessor accessor = PersistenceProvider.GENERIC_JPA.getParameterAccessor(parameters, values, em); + JpaParametersParameterAccessor accessor = PersistenceProvider.GENERIC_JPA.getParameterAccessor(parameters, values, + em); bind(parameters, accessor); @@ -62,10 +62,10 @@ class JpaParametersParameterAccessorTests { bind(parameters, accessor); - ArgumentCaptor captor = ArgumentCaptor.forClass(TypedParameterValue.class); + ArgumentCaptor> captor = ArgumentCaptor.forClass(TypedParameterValue.class); verify(query).setParameter(eq(1), captor.capture()); - TypedParameterValue captorValue = captor.getValue(); - assertThat(captorValue.getType()).isEqualTo(StandardBasicTypes.INTEGER); + TypedParameterValue captorValue = captor.getValue(); + assertThat(captorValue.getType().getBindableJavaType()).isEqualTo(Integer.class); assertThat(captorValue.getValue()).isNull(); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java index 04b9e53b5..c8a1ddead 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java @@ -17,16 +17,15 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import java.lang.reflect.Method; - import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.ParameterExpression; +import java.lang.reflect.Method; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.repository.query.DefaultParameters; import org.springframework.data.repository.query.Parameters; @@ -60,7 +59,7 @@ class ParameterExpressionProviderTests { ParameterMetadataProvider provider = new ParameterMetadataProvider(builder, accessor, EscapeCharacter.DEFAULT); ParameterExpression expression = provider.next(part, Comparable.class).getExpression(); - assertThat(expression.getParameterType()).isEqualTo(int.class); + assertThat(expression.getParameterType()).isEqualTo(Integer.class); } interface SampleRepository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index e0177b213..980cef7b4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -20,6 +20,11 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; import static org.springframework.test.util.ReflectionTestUtils.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import jakarta.persistence.TemporalType; + import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -27,14 +32,9 @@ import java.util.Date; import java.util.Iterator; import java.util.List; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import jakarta.persistence.TemporalType; - import org.hibernate.Version; -import org.hibernate.query.internal.QueryImpl; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.data.domain.Page; @@ -65,6 +65,7 @@ import org.springframework.util.Assert; public class PartTreeJpaQueryIntegrationTests { private static String PROPERTY = "h.target." + getQueryProperty(); + private static Class HIBERNATE_NATIVE_QUERY = org.hibernate.query.Query.class; @PersistenceContext EntityManager entityManager; @@ -82,7 +83,7 @@ public class PartTreeJpaQueryIntegrationTests { PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); - jpaQuery.createQuery((getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) }))); + jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); } @Test @@ -100,18 +101,19 @@ public class PartTreeJpaQueryIntegrationTests { } @Test // DATAJPA-121 + @Disabled // HHH-15389 void recreatesQueryIfNullValueIsGiven() throws Exception { JpaQueryMethod queryMethod = getQueryMethod("findByFirstname", String.class, Pageable.class); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - Query query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) }))); + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(QueryImpl.class))).endsWith("firstname=:param0"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("firstname=:param0"); - query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) }))); + query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) })); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(QueryImpl.class))).endsWith("firstname is null"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("firstname is null"); } @Test // DATAJPA-920 @@ -120,42 +122,45 @@ public class PartTreeJpaQueryIntegrationTests { JpaQueryMethod queryMethod = getQueryMethod("existsByFirstname", String.class); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - Query query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] { "Matthews" }))); + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews" })); assertThat(query.getMaxResults()).isEqualTo(1); } @Test // DATAJPA-920 + @Disabled // HHH-15389 void shouldSelectAliasedIdForExistsProjectionQueries() throws Exception { JpaQueryMethod queryMethod = getQueryMethod("existsByFirstname", String.class); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - Query query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] { "Matthews" }))); + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews" })); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(QueryImpl.class))).contains(".id from User as"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).contains(".id from User as"); } @Test // DATAJPA-1074 + @Disabled // HHH-15389 void isEmptyCollection() throws Exception { JpaQueryMethod queryMethod = getQueryMethod("findByRolesIsEmpty"); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - Query query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] {}))); + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {})); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(QueryImpl.class))).endsWith("roles is empty"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is empty"); } @Test // DATAJPA-1074 + @Disabled // HHH-15389 void isNotEmptyCollection() throws Exception { JpaQueryMethod queryMethod = getQueryMethod("findByRolesIsNotEmpty"); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - Query query = jpaQuery.createQuery((getAccessor(queryMethod, new Object[] {}))); + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {})); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(QueryImpl.class))).endsWith("roles is not empty"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is not empty"); } @Test // DATAJPA-1074 @@ -236,7 +241,7 @@ public class PartTreeJpaQueryIntegrationTests { JpaQueryMethod queryMethod = getQueryMethod(methodName, parameterTypes); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); - jpaQuery.createQuery((getAccessor(queryMethod, values))); + jpaQuery.createQuery(getAccessor(queryMethod, values)); } private JpaQueryMethod getQueryMethod(String methodName, Class... parameterTypes) throws Exception { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index f9fd6b986..51659d968 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -582,8 +582,8 @@ public interface UserRepository // DATAJPA-1233 @Query( - value = "SELECT u FROM User u WHERE ?2 = 'x' ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END, u.firstname") - Page findAllOrderedBySpecialNameMultipleParamsIndexed(String name, String other, Pageable page); + value = "SELECT u FROM User u WHERE ?1 = 'x' ORDER BY CASE WHEN (u.firstname >= ?2) THEN 0 ELSE 1 END, u.firstname") + Page findAllOrderedBySpecialNameMultipleParamsIndexed(String other, String name, Pageable page); // DATAJPA-928 Page findByNativeNamedQueryWithPageable(Pageable pageable); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java index 5ac718189..5776d82b0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java @@ -17,27 +17,18 @@ package org.springframework.data.jpa.repository.support; import static org.assertj.core.api.Assertions.*; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - import jakarta.persistence.EntityManager; -import javax.sql.DataSource; +import jakarta.persistence.EntityManagerFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportResource; import org.springframework.context.annotation.Primary; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.stereotype.Component; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -62,59 +53,35 @@ public class EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests { assertThat(target.primaryEm).isNotNull(); } - /** - * Annotation to demarcate test components. - * - * @author Oliver Gierke - */ - @Component - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - private static @interface TestComponent { - - } - @Configuration + @Import(EntityManagerInjectionTarget.class) @ImportResource("classpath:infrastructure.xml") - @ComponentScan(includeFilters = @Filter(TestComponent.class), useDefaultFilters = false) static class Config { - @Autowired DataSource dataSource; - @Autowired JpaVendorAdapter vendorAdapter; + @Autowired @Qualifier("entityManagerFactory") EntityManagerFactory emf; @Bean public static EntityManagerBeanDefinitionRegistrarPostProcessor processor() { return new EntityManagerBeanDefinitionRegistrarPostProcessor(); } - private LocalContainerEntityManagerFactoryBean emf() { - - LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); - factoryBean.setPersistenceUnitName("spring-data-jpa"); - factoryBean.setDataSource(dataSource); - factoryBean.setJpaVendorAdapter(vendorAdapter); - - return factoryBean; + @Bean + EntityManagerFactory firstEmf() { + return emf; } @Bean - LocalContainerEntityManagerFactoryBean firstEmf() { - return emf(); - } - - @Bean - LocalContainerEntityManagerFactoryBean secondEmf() { - return emf(); + EntityManagerFactory secondEmf() { + return emf; } @Primary @Bean - LocalContainerEntityManagerFactoryBean thirdEmf() { - return emf(); + EntityManagerFactory thirdEmf() { + return emf; } } - @TestComponent static class EntityManagerInjectionTarget { private final EntityManager firstEm; diff --git a/spring-data-jpa/src/test/resources/hibernate.xml b/spring-data-jpa/src/test/resources/hibernate.xml index fafd2e120..05420a9be 100644 --- a/spring-data-jpa/src/test/resources/hibernate.xml +++ b/spring-data-jpa/src/test/resources/hibernate.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - + diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 1c71bcf5d..7d4b34656 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,30 @@ [[new-features]] = New & Noteworthy +[[new-features.3-0]] +== What's New in Spring Data JPA 3.0 +* Upgrade to Hibernate 6. +See <> for what to consider when upgrading. +* Support for null handling definitions via `Sort`. + +[[new-features.3-0.hibernate-6]] +=== Upgrading to Hibernate 6 +Spring Data 3.0 upgrades its Hibernate baseline to Hibernate 6. +As quite a few things have changed in that version, a couple of things that have worked before might need some tweaks. + +* _Using JPA named queries with pagination_ -- Pagination requires Spring Data to derive a count query from the originally declared one loading the actual content of the Page. +For queries declared as JPA named queries we have relied on provider-specific API to obtain the original source query and tweak it accordingly. +On Hibernate 6, in certain arrangements that query extraction might fail. +We recommend to either rather declare the queries on the repository methods directly using `@Query`. +* _Using positional parameters with pagination_ -- When using positional parameters with pagination queries you need to make sure that the parameter indexes still start with 1, even with a potential `ORDER BY` clause removed from the query. +This is because, the count query derived from the original one will have that clause removed from the query and Hibernate 6 rejects queries parameter indexes not starting at 1. +We generally recommend to use named parameters anyway. +* _Applying JPA entity graphs_ -- Under certain model conditions, the application of entity graphs might fail on Hibernate 6. +See https://hibernate.atlassian.net/browse/HHH-15391[this ticket] for details. +We generally recommend to rather use <> instead of entity graphs. +* _Using `… like … escape ?#{escapeCharacter()}` in queries_ -- If you have customized the global default escape character (via `@EnableJpaRepositories(escapeCharacter = '…')`) the application of that through the corresponding SpEL expression currently fails. +See https://hibernate.atlassian.net/browse/HHH-15392[this ticket] for details. + [[new-features.2-5-0]] == What's New in Spring Data JPA 2.5