spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
914 stars 560 forks source link

@RepositoryRestController not resolving querydsl Predicate [DATAREST-838] #1210

Open spring-projects-issues opened 8 years ago

spring-projects-issues commented 8 years ago

Domingo Gómez García opened DATAREST-838 and commented

Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface

When using @RepositoryRestController. Branched from the examples and applied changes:

https://github.com/domgom/spring-data-examples/blob/DATAREST-838/web/querydsl/src/main/java/example/users/web/UserController.java#L44

Original SO question: http://stackoverflow.com/questions/32486860/exception-using-spring-data-jpa-and-querydsl-via-rest-controller


Affects: 2.5.1 (Hopper SR1)

10 votes, 13 watchers

spring-projects-issues commented 7 years ago

Casey Link commented

Just ran into this bug when implementing a workaround for DATACMNS-941.

@RepositoryRestController
@RequestMapping("person-containers")
@ExposesResourceFor(PersonContainer.class)
@RequiredArgsConstructor(onConstructor = @__(@Autowired) )
public class PersonContainerController
{
    private final PersonContainerService personContainerService;
    private final PagedResourcesAssembler pagedResourcesAssembler;

    @RequestMapping(value = "/search/results", method = RequestMethod.GET)
    PagedResources<?> findResults(
            @QuerydslPredicate(root = PersonContainer.class) Predicate predicate,
            @PageableDefault(size = 20, sort = "name", direction = Sort.Direction.ASC) final Pageable p,
            @RequestParam final MultiValueMap<String, String> parameters,
            final PersistentEntityResourceAssembler resourceAssembler)
    { ... }

Calling the api method results in the error Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface

This is quite unfortunate because RepositoryRestController is required to use the PersistentEntityResourceAssembler. If I change to a normal @RestController, I get:

Failed to instantiate [org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler.<init>()

spring-projects-issues commented 7 years ago

Casey Link commented

Ok I managed a workaround, but boy is it a doozy!

To be clear, the goal is to:

Here is the Controller:

@RestController
@RequestMapping("person-containers")
@ExposesResourceFor(PersonContainer.class)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class PersonContainerController
{
    private final PersonContainerService personContainerService;
    private final PagedResourcesAssembler pagedResourcesAssembler;

    @RequestMapping(value = "/search/results", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public PagedResources<PersistentEntityResource> findResults(
            @QuerydslPredicate(root = PersonContainer.class) Predicate predicate,
            @PageableDefault(size = 20) final Pageable p,
            @RequestParam final MultiValueMap<String, String> parameters,
            final PersistentEntityResourceAssembler resourceAssembler)
    {
        if (predicate != null)
            log.info("PREDICATE: {}", predicate.toString());

        if (!parameters.isEmpty())
            log.info("ALL PARAMS: {}", parameters.toString());

        Page<PersonContainer> page = personContainerService
                .findPersonContainers(predicate, p);

        if (page.hasContent())
            return pagedResourcesAssembler.toResource(page, resourceAssembler);

        return pagedResourcesAssembler.toEmptyResource(page, PersonContainer.class, null);
    }

}

You also must configure WebMvc slightly to avoid the bug DATAREST-657

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@EnableSpringDataWebSupport
public class MvcConfig extends WebMvcConfigurerAdapter
{

    @Autowired
    @Qualifier("repositoryExporterHandlerAdapter")
    RequestMappingHandlerAdapter repositoryExporterHandlerAdapter;

    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        List<HandlerMethodArgumentResolver> customArgumentResolvers = repositoryExporterHandlerAdapter.getCustomArgumentResolvers();
        argumentResolvers.addAll(customArgumentResolvers);
    }
}

Not shown: the QuerydslBinderCustomizer implementation on my PersonContainerRepository, it's nothing special.

Like I said, quite a workaround. Hopefully this and DATAREST-657 can be fixed

spring-projects-issues commented 7 years ago

Tyler Carrington commented

I ran into trouble mixing @RestController with @RepositoryRestController so I created a new workaround that takes the parameters and creates a predicate from it. This allows us to use the goodness from @RepositoryRestController with the predicates.

@Slf4j
@RepositoryRestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class PersonContainerController
{
    private final PersonContainerService personContainerService;
    private final PagedResourcesAssembler pagedResourcesAssembler;

    @RequestMapping(value = "/search/results", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public PagedResources<PersistentEntityResource> findResults(
            @PageableDefault(size = 20) final Pageable p,
            @RequestParam final MultiValueMap<String, String> parameters,
            final PersistentEntityResourceAssembler resourceAssembler)
    {
        if (!parameters.isEmpty())
            log.info("ALL PARAMS: {}", parameters.toString());

        Predicate predicate = predicateService.getPredicateFromParameters(parameters, PersonContainer.class);

        Page<PersonContainer> page = personContainerService
                .findPersonContainers(predicate, p);

        if (page.hasContent())
            return pagedResourcesAssembler.toResource(page, resourceAssembler);

        return pagedResourcesAssembler.toEmptyResource(page, PersonContainer.class, null);
    }

}
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class PredicateService {

    private final QuerydslPredicateBuilder querydslPredicateBuilder;
    private final QuerydslBindingsFactory querydslBindingsFactory;

    public <T> Predicate getPredicateFromParameters(final MultiValueMap<String, String> parameters, Class<T> tClass) {
        TypeInformation<T> typeInformation = ClassTypeInformation.from(tClass);
        return querydslPredicateBuilder.getPredicate(typeInformation, parameters, querydslBindingsFactory.createBindingsFor(null, typeInformation));
    }
}
@Configuration
public class QueryDslConfiguration {
    @Bean
    public QuerydslBindingsFactory querydslBindingsFactory() {
        return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
    }

    @Bean
    public QuerydslPredicateBuilder querydslPredicateBuilder() {
        return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory().getEntityPathResolver());
    }
}
spring-projects-issues commented 5 years ago

SayakMukhopadhyay commented

This issue is still present in Spring 3.1.5.RELEASE. Any information regarding a tentative date of a fix?

spring-projects-issues commented 4 years ago

Oliver Drotbohm commented

Can you please verify this issue is still present if you move away from the class level @RequestMapping? Using that causes the controller to be picked up by the standard Spring MVC handler mapping that doesn't know about the Spring Data specific infrastructure extensions

slangeberg commented 3 years ago

Our project on Spring data rest webmvc 3.3.4.RELEASE still has same error, even after i remove @RequestMapping from @RepositoryRestController

Oliver Drotbohm commented

Can you please verify this issue is still present if you move away from the class level @RequestMapping? Using that causes the controller to be picked up by the standard Spring MVC handler mapping that doesn't know about the Spring Data specific infrastructure extensions

slangeberg commented 3 years ago

Applying @tylercarrington's workaround has worked for us with a couple changes.

On spring boot 2.4 the hateoas interface has changed in controllers. Now you assemble Model not Resource:

    @ResponseBody
    @GetMapping("/query")
    public RepresentationModel<?> findByQueryDsl(
            Pageable pageable,
            @RequestParam MultiValueMap<String, String> parameters,
            PersistentEntityResourceAssembler resourceAssembler) {

        Page<User> page = userLookupService.findUserByQueryDsl(pageable, parameters);

        final var pagedModel = page.hasContent()
            ? pagedResourcesAssembler.toModel(page, resourceAssembler)
            : pagedResourcesAssembler.toEmptyModel(page, User.class);
        return pagedModel;
    }

And in the configuration, i found context already had bean populated for QuerydslBindingsFactory, so i dropped it:

@Configuration
public class QueryDslConfiguration {
//    @Bean
//    public QuerydslBindingsFactory querydslBindingsFactory() {
//        return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
//    }

    @Bean
    public QuerydslPredicateBuilder querydslPredicateBuilder(QuerydslBindingsFactory querydslBindingsFactory) {
        return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory.getEntityPathResolver());
    }
}
yejianfengblue commented 3 years ago

@RepositoryRestController

Example custom controller annotated with @RepositoryRestController whose method has a Predicate argument.

@RepositoryRestController
public class AccountController {

    @GetMapping("/accounts")
    ResponseEntity<PagedModel<EntityModel<Account>>> getAll(
            @QuerydslPredicate(root = Account.class) Predicate predicate,
            Pageable pageable) {
    // ...
    }
}

In a Spring Data REST application, DispatcherServlet.handlerAdapters should have 5 adapters:

RepositoryRestHandlerAdapter.supportsInternal() finds annotation @BasePathAwareController on controller. Because @RepositoryRestController is also a @BasePathAwareController, the result adapter to handle a GET request to /accounts is RepositoryRestHandlerAdapter.

RepositoryRestHandlerAdapter's parent field RequestMappingHandlerAdapter.argumentResolvers contains only one querydsl-related resolver QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.

QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver supports parameter only if its type is RootResourceInformation. See org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver#supportsParameter().

That's why the Predicate argument is not resolved.

@RestController

Example custom controller annotated with @RestController whose method has a Predicate argument.

@RestController
public class AccountController {

    @GetMapping("/accounts")
    ResponseEntity<PagedModel<EntityModel<Account>>> getAll(
            @QuerydslPredicate(root = Account.class) Predicate predicate,
            Pageable pageable) {
    // ...
    }
}

The result adapter to handle a GET request to /accounts is RequestMappingHandlerAdapter, whose argumentResolvers contains QuerydslPredicateArgumentResolver. QuerydslPredicateArgumentResolver supports paramater if its type is Predicate or Optional<Predicate>.

In such case, the Predicate argument is resolved.

Proposal

nursba commented 1 year ago

@yejianfengblue

Proposal

  • add a QuerydslPredicateArgumentResolver to RepositoryRestHandlerAdapter resolvers in the code org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration#defaultMethodArgumentResolvers.
  • add a method configureArgumentResolver() to RepositoryRestConfigurer.

could you please share some code samples

ArnauAregall commented 1 year ago

Applying @TylerCarrington's workaround has worked for us with a couple changes.

On spring boot 2.4 the hateoas interface has changed in controllers. Now you assemble Model not Resource:

    @ResponseBody
    @GetMapping("/query")
    public RepresentationModel<?> findByQueryDsl(
            Pageable pageable,
            @RequestParam MultiValueMap<String, String> parameters,
            PersistentEntityResourceAssembler resourceAssembler) {

        Page<User> page = userLookupService.findUserByQueryDsl(pageable, parameters);

        final var pagedModel = page.hasContent()
            ? pagedResourcesAssembler.toModel(page, resourceAssembler)
            : pagedResourcesAssembler.toEmptyModel(page, User.class);
        return pagedModel;
    }

And in the configuration, i found context already had bean populated for QuerydslBindingsFactory, so i dropped it:

@Configuration
public class QueryDslConfiguration {
//    @Bean
//    public QuerydslBindingsFactory querydslBindingsFactory() {
//        return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
//    }

    @Bean
    public QuerydslPredicateBuilder querydslPredicateBuilder(QuerydslBindingsFactory querydslBindingsFactory) {
        return new QuerydslPredicateBuilder(new DefaultConversionService(), querydslBindingsFactory.getEntityPathResolver());
    }
}

As of today, with Spring Data Rest 3.7.7 this is still the most accurate workaround.