spring-projects / spring-data-jpa

Simplifies the development of creating a JPA-based data access layer.
https://spring.io/projects/spring-data-jpa/
Apache License 2.0
3.02k stars 1.42k forks source link

Support for sorting by subclass properties inside repository methods #3593

Closed agrancaric closed 2 months ago

agrancaric commented 3 months ago

Hello, when trying to sort by subclass property using Spring Data Jpa repository an exception is thrown, however when using standard JPA the query executes fine. The problem seems to be in PropertyPath that only looks at the parent properties and throws an exception if the property is not found. For now I've added a workaround to also look at the subclasses in our project (https://github.com/croz-ltd/nrich/blob/dbd018ed73bc49f0f0bfd53c59435defcf1fed49/nrich-search/src/main/java/org/springframework/data/jpa/repository/query/NrichQueryUtils.java#L63) but I would prefer not to have to override and copy parts of QueryUtils. Is there an option to add such support in spring-data-jpa? The example project and exception stacktrace are given bellow.

spring-data-jpa-sorting-error.zip

No property 'childProperty' found for type 'Parent'
org.springframework.data.mapping.PropertyReferenceException: No property 'childProperty' found for type 'Parent'
  at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:94)
  at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:455)
  at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:431)
  at org.springframework.data.mapping.PropertyPath.lambda$from$0(PropertyPath.java:384)
  at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330)
  at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:366)
  at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:344)
  at org.springframework.data.jpa.repository.query.QueryUtils.toJpaOrder(QueryUtils.java:753)
  at org.springframework.data.jpa.repository.query.QueryUtils.toOrders(QueryUtils.java:706)
  at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:760)
  at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:741)
  at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:419)
  at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
  at java.base/java.lang.reflect.Method.invoke(Method.java:580)
  at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355)
  at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
  at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
  at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
  at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
  at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
  at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:173)
  at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:148)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:379)
  at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:165)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
  at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
  at jdk.proxy3/jdk.proxy3.$Proxy108.findAll(Unknown Source)
  at net.example.SortingTest.shouldSortThroughRepository(SortingTest.java:62)
  at java.base/java.lang.reflect.Method.invoke(Method.java:580)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
mp911de commented 3 months ago

Related to #3588.

We need to look up property paths to formulate a proper order expression. For findAll we do not have any means to consider subclasses, we only have access to the repository's domain type.

We are exploring a switch from Criteria Queries towards String-based JPQL queries. In such an arrangement you could use JpaSort.unsafe(…) to reference properties that would be resolved during query execution by your JPA provider.

agrancaric commented 3 months ago

Related to #3588.

We need to look up property paths to formulate a proper order expression. For findAll we do not have any means to consider subclasses, we only have access to the repository's domain type.

We are exploring a switch from Criteria Queries towards String-based JPQL queries. In such an arrangement you could use JpaSort.unsafe(…) to reference properties that would be resolved during query execution by your JPA provider.

Thanks for the answer. With Hibernate (I am not sure about other providers) it is possible to get subclasses by using the following code (maybe there is also a a smarter way of doing this):

 if (from.getModel() instanceof EntityDomainType<?> entityDomainType) {
            return entityDomainType.getSubTypes().stream()
                .map(Type::getJavaType)
                .toList();
        }

also there is a possibility of classpath scanning, not sure what do you think about that? We have a workaround implemented but if it is possible we would prefer to have it supported by Spring. If not then we'll just leave the workaround. Thanks in advance for your answer.

mp911de commented 2 months ago

We do not want to change this behavior by default as subtypes open up exploitation vectors using sorting. If you want to use subtype properties for sorting, then use JpaSort.unsafe(…) for these cases in the context of JPQL queries.