specification-arg-resolver icon indicating copy to clipboard operation
specification-arg-resolver copied to clipboard

IllegalStateException during deployment: Operator IN on type requires a Collection argument

Open rcbandit111 opened this issue 4 years ago • 7 comments

I want to create this Spring Data Repository:

API Endpoint using specification-arg-resolver:

@GetMapping("find")
    public Page<CustomersFullDTO> getAllBySpecification(
            @And({
                    @Spec(path = "status", spec = In.class),
            }) Specification<Users> specification,
            Pageable pageable
    ) {
        return customersService.getAllBySpecification(specification, pageable)
                .map(g -> CustomersFullDTO.builder()
                        .id(g.getId())
                        ......
                        .build()
                );
    }

Interface:

public interface CustomersService {
    Page<Users> findAll(int page, int size);

    Page<Users> getAllBySpecification(final Specification<Users> specification, final Pageable pageable);
}

Service:

@Override
public Page<Users> findAll(int page, int size) {
    return dao.findAllByTypeIn(PageRequest.of(page, size), "CustomerUser");
}

@Override
public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
    return this.dao.findAllByTypeIn(specification, pageable, List.of("CustomerUser"));
}

Repository:

@Repository
public interface CustomersRepository extends JpaRepository<Users, Integer>, JpaSpecificationExecutor<Users> {

    Page<Users> findAllByTypeIn(Pageable page, String... types);

    Page<Users> findAllByTypeIn(Specification<Users> specification, Pageable pageable, List<String> types);
}

When I deploy the package I get this error:

Caused by: java.lang.IllegalStateException: Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).

Do you know how this issue can be fixed? I want at lower level to use a "marker" to get rows which contain only value "CustomerUser". Search values should not have any impact into this.

One way to solve this currently is to add specification implementation: https://stackoverflow.com/questions/56959816/add-where-in-clause-to-jpa-specification but this adds a lot of overhead. Can you advise?

rcbandit111 avatar Oct 20 '20 14:10 rcbandit111

Hello @tkaczmarzyk

Here it is:

11:31:42.823 [main] ERROR SpringApplication[reportFailure:837] - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customerController' defined in URL [jar:file:/opt/datalis/skyshop-admin-be-1.0.jar!/BOOT-INF/classes!/org/engine/admin/be/rest/customer/CustomerController.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customersServiceImpl' defined in URL [jar:file:/opt/datalis/skyshop-admin-be-1.0.jar!/BOOT-INF/lib/skyshop-plugin-1.0.jar!/org/engine/plugin/production/service/CustomersServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customersRepository' defined in org.engine.plugin.production.service.CustomersRepository defined in @EnableJpaRepositories declared on ContextProductionDatasource: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List)! Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:227)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1203)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
        at org.engine.admin.be.SkyshopAdminBEApplication.main(SkyshopAdminBEApplication.java:28)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:567)
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customersServiceImpl' defined in URL [jar:file:/opt/datalis/skyshop-admin-be-1.0.jar!/BOOT-INF/lib/skyshop-plugin-1.0.jar!/org/engine/plugin/production/service/CustomersServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customersRepository' defined in org.engine.plugin.production.service.CustomersRepository defined in @EnableJpaRepositories declared on ContextProductionDatasource: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List)! Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:227)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1203)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
        at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:884)
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
        ... 28 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customersRepository' defined in org.engine.plugin.production.service.CustomersRepository defined in @EnableJpaRepositories declared on ContextProductionDatasource: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List)! Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1794)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
        at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:884)
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
        ... 42 common frames omitted
Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List)! Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).
        at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:96)
        at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:107)
        at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateIfNotFoundQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:218)
        at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$AbstractQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:81)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:99)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lambda$mapMethodsToQuery$1(QueryExecutorMethodInterceptor.java:92)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
        at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
        at java.base/java.util.Collections$UnmodifiableCollection$1.forEachRemaining(Collections.java:1054)
        at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
        at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.mapMethodsToQuery(QueryExecutorMethodInterceptor.java:94)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lambda$new$0(QueryExecutorMethodInterceptor.java:84)
        at java.base/java.util.Optional.map(Optional.java:265)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.<init>(QueryExecutorMethodInterceptor.java:84)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:331)
        at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:297)
        at org.springframework.data.util.Lazy.getNullable(Lazy.java:212)
        at org.springframework.data.util.Lazy.get(Lazy.java:94)
        at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:300)
        at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:144)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1790)
        ... 53 common frames omitted
Caused by: java.lang.IllegalStateException: Operator IN on type requires a Collection argument, found interface org.springframework.data.jpa.domain.Specification in method public abstract org.springframework.data.domain.Page org.engine.plugin.production.service.CustomersRepository.findAllByTypeIn(org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable,java.util.List).
        at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.throwExceptionOnArgumentMismatch(PartTreeJpaQuery.java:169)
        at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.validate(PartTreeJpaQuery.java:147)
        at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:90)
        ... 79 common frames omitted

rcbandit111 avatar Oct 21 '20 11:10 rcbandit111

After a second look - I think what you are trying to achieve is impossible due to Spring Data limitation (not specification-arg-resolver related issue) - AFAIK you can not mix autogenerated query (by method name convention, findAllByTypeIn in this case) with specification argument (but it's worth to double-check with Spring Data documentation).

BUT I think you can use specification-arg-resolver's constVal to embedd the "typeIn" condition. It would look like this:

@GetMapping("find")
    public Page<CustomersFullDTO> getAllBySpecification(
            @And({
                    @Spec(path = "status", spec = In.class),
                    @Spec(path = "type", spec = In.class, constVal = "CustomerUser")
            }) Specification<Users> specification,
            Pageable pageable
    ) {
        return customersService.getAllBySpecification(specification, pageable)
                .map(g -> CustomersFullDTO.builder()
                        .id(g.getId())
                        ......
                        .build()
                );
    }

(then you use just findAll(specification, pageable) from the repo - the "type" condition is already included in the specification)

This should technically work as expected, but the disadvantage is that part of the logic is now in the annotation, not in the service.

(alternatively, you can research if and how Spring data allows mixing specification arguments with autogenerated queries, perhaps changing the order of parameters like this findAllByTypeIn(List<String> types, Specification<Users> specification, Pageable pageable); would help? But I'm afraid this might just be not supported)

tkaczmarzyk avatar Oct 21 '20 15:10 tkaczmarzyk

Hello @tkaczmarzyk. Thank you for the response.

Adding @Spec(path = "type", spec = In.class, constVal = "CustomerUser") solves the issue. But it's a huge security door. Someone can send type param into the http payload and select private data.

Can you for example add some special annotation/constant which can't be modified by the http request?

Previously I have solved the issue by using JPA Projections but it's not a very elegant solution.

So if you can add some kind of a constant into your framework it will be a suitable solution.

rcbandit111 avatar Oct 21 '20 16:10 rcbandit111

Hi, when you use constVal, then "type" HTTP param should be ignored by the lib (as opposed to "defaultVal", which can be overriden by HTTP param). So it should work exactly as you described.

But in my opinion, it's not a very elegant solution too.

Another approach would be to manually enchance the specification with "and type in" condition in the business layer:

Specification.and(specFromTheController, new In("type", asList("CustomerUser"))

before passing it to the repo.

tkaczmarzyk avatar Oct 22 '20 08:10 tkaczmarzyk

I tried this:

    @Override
    public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {

        specification.and(specification, new CriteriaBuilder.In("type", List.of("CustomerUser"));

        return this.dao.findAll(specification, pageable);
    }

For this code new CriteriaBuilder.In("type", List.of("CustomerUser")); I get 'In' is abstract; cannot be instantiated

Any other solution?

rcbandit111 avatar Oct 22 '20 13:10 rcbandit111

@tkaczmarzyk I have a question: Can you extend constVal to accept a list? I would like to send several values as a constVal?

rcbandit111 avatar Oct 26 '20 15:10 rcbandit111

Hi, I will check that. But I believe you can use comma-separated list (as a single string) in constVal and combine it with paramSeparator=','. It should work (but I haven't checked it yet, though)

tkaczmarzyk avatar Nov 12 '20 07:11 tkaczmarzyk

Hello, a correction for comment above: constVal parameter accepts string array therefore you only need to put values into brackets

Snippet working in example environment:

    @GetMapping
    public Iterable<Customer> filterByName(
            @Spec(path = "firstName", constVal = { "Homer", "Marge" }, spec=In.class) Specification<Customer> spec) {

        return customerRepo.findAll(spec);
    }

rdworak37 avatar Dec 29 '22 08:12 rdworak37