Search code examples
springspring-dataspring-data-restquerydsl

Create a collection resource with Sort and querydsl Predicate but no Pageable


I'm using Spring data Rest and looking for a way to create a Collection (or search) resource having Sort and Predicate (using querydsl) capabilities but no Pager without luck.

Until now, i only needed to disable pager but have Sort capabilities. It worked well using the following and calling GET /userAccounts/search/noPager :

public interface ReportRepository 
    extends JpaRepository<Report, Integer>{

    /**
     * Non paged data with Sort capabilities
     */
    @RestResource(path="noPager")
    List<Report> findAllBy(Sort sort);

Now i need to add Predicate capabilites. If i do the following:

public interface UserAccountRepository 
    extends JpaRepository<UserAccount, Integer>,
            QueryDslPredicateExecutor<UserAccount> {

    /**
     * Non paged data with Sort capabilities
     */
    @RestResource(path="noPager")
    List<UserAccount> findAllBy(Predicate predicate, Sort sort);

I get the following error when calling GET /userAccounts/search/noPager:

java.lang.IllegalArgumentException: Unable to detect parameter names for query method fr.texsys.datemplus.dm.domain.data.UserAccountRepository.findAllBy! Use @Param or compile with -parameters on JDK 8. at org.springframework.data.repository.support.ReflectionRepositoryInvoker.prepareParameters(ReflectionRepositoryInvoker.java:235) ~[spring-data-commons-1.12.3.RELEASE.jar!/:na] at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invokeQueryMethod(ReflectionRepositoryInvoker.java:206) ~[spring-data-commons-1.12.3.RELEASE.jar!/:na]

More strange, i tried to use QueryDSL with a CrudRepository, which has no pager capabilities. If i extend a CrudRepository with QueryDslPredicateExecutor<UserAccount>, and call the collection resource GET /userAccounts the Pager is activated.

Without QueryDslPredicateExecutor:

public interface UserAccountRepository 
    extends CrudRepository<UserAccount, Integer> {

There is no pager:

{
  "_embedded" : {
    "userAccounts" : [...]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:15571/userAccounts"
    },
    "profile" : {
      "href" : "http://localhost:15571/profile/userAccounts"
    }
  }
}

But with QueryDslPredicateExecutor:

public interface UserAccountRepository 
    extends CrudRepository<UserAccount, Integer>,
            QueryDslPredicateExecutor<UserAccount> {

The pager is activated

{
  "_embedded" : {
    "userAccounts" : [ ... ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:15571/userAccounts"
    },
    "profile" : {
      "href" : "http://localhost:15571/profile/userAccounts"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 3,
    "totalPages" : 1,
    "number" : 0
  }
}

Solution

  • So i managed do to what i want but it is really cumbersome and hope there is a lighter and more convenient way i've not discovered:

    1: Create a custom QuerydslRepositoryInvokerAdapter and route the invokeFindAll(Pageable pageable) method to the findAll(Sort sort) of the executor:

    public class CustomQuerydslRepositoryInvokerAdapter 
            extends QuerydslRepositoryInvokerAdapter {
    
        private final QueryDslPredicateExecutor<Object> executor;
        private final Predicate predicate;
    
        public CustomQuerydslRepositoryInvokerAdapter(
                RepositoryInvoker delegate, 
                QueryDslPredicateExecutor<Object> executor,
                Predicate predicate) {
            super(delegate, executor, predicate);
    
            Assert.notNull(delegate, "Delegate RepositoryInvoker must not be null!");
            Assert.notNull(executor, "QuerydslPredicateExecutor must not be null!");
    
            this.executor = executor;
            this.predicate = predicate;
        }
    
        @Override
        public Iterable<Object> invokeFindAll(Pageable pageable) {
            return executor.findAll(predicate, pageable.getSort());
        }
    
    }
    

    2: Create a custom RootResourceInformationHandlerMethodArgumentResolver:

    public class CustomQuerydslAwareRootResourceInformationHandlerMethodArgumentResolver 
        extends RootResourceInformationHandlerMethodArgumentResolver {
    
        private final Repositories repositories;
        private final QuerydslPredicateBuilder predicateBuilder;
        private final QuerydslBindingsFactory factory;
    
        public CustomQuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(Repositories repositories,
                RepositoryInvokerFactory invokerFactory, ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver,
                QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory) {
    
            super(repositories, invokerFactory, resourceMetadataResolver);
    
            this.repositories = repositories;
            this.predicateBuilder = predicateBuilder;
            this.factory = factory;
        }
    
        /* 
         * (non-Javadoc)
         * @see org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver#postProcess(org.springframework.data.repository.support.RepositoryInvoker, java.lang.Class, java.util.Map)
         */
        @Override
        @SuppressWarnings({ "unchecked" })
        protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker,
                Class<?> domainType, Map<String, String[]> parameters) {
    
            Object repository = repositories.getRepositoryFor(domainType);
    
            if (!CustomQueryDslPredicateExecutor.class.isInstance(repository)
                    || !parameter.hasParameterAnnotation(QuerydslPredicate.class)) {
                return invoker;
            }
    
            ClassTypeInformation<?> type = ClassTypeInformation.from(domainType);
    
            QuerydslBindings bindings = factory.createBindingsFor(null, type);
            Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(parameters), bindings);
    
            return new CustomQuerydslRepositoryInvokerAdapter(invoker, (QueryDslPredicateExecutor<Object>) repository, predicate);
        }
    
        private static MultiValueMap<String, String> toMultiValueMap(Map<String, String[]> source) {
    
            MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>();
    
            for (String key : source.keySet()) {
                result.put(key, Arrays.asList(source.get(key)));
            }
    
            return result;
        }
    
    
    }
    

    3: Overwrite the RootResourceInformationHandlerMethodArgumentResolver Bean definition:

    @Configuration
    public class CustomRepositoryRestMvcConfiguration extends RepositoryRestMvcConfiguration {
    
        @Autowired ApplicationContext applicationContext;
    
        @Override
        @Bean
        public RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver() {
    
            if (QueryDslUtils.QUERY_DSL_PRESENT) {
    
                QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
                QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(defaultConversionService(),
                        factory.getEntityPathResolver());
    
                return new CustomQuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories(),
                        repositoryInvokerFactory(defaultConversionService()), resourceMetadataHandlerMethodArgumentResolver(),
                        predicateBuilder, factory);
            }
    
            return new RootResourceInformationHandlerMethodArgumentResolver(repositories(),
                    repositoryInvokerFactory(defaultConversionService()), resourceMetadataHandlerMethodArgumentResolver());
        }
    
    }
    

    4: Create a custom QueryDslPredicateExecutor:

    public interface CustomQueryDslPredicateExecutor<T> extends QueryDslPredicateExecutor<T> {
    
        Iterable<T> findAll(Predicate predicate, Sort sort);
    
    }
    

    5: Then apply it to the Repository

    public interface UserAccountRepository 
        extends CrudRepository<UserAccount, Integer>,
                CustomQueryDslPredicateExecutor<UserAccount> {
        [...] 
    }
    

    Ouch! but it works...