QueryDsl web query on the key of a Map field QueryDsl web query on the key of a Map field spring spring

QueryDsl web query on the key of a Map field


Replace the Bean

Implement ApplicationContextAware

This is how I replaced the bean in the application context.

It feels a little hacky. I'd love to hear a better way to do this.

@Configurationpublic class CustomQuerydslHandlerMethodArgumentResolverConfig implements ApplicationContextAware {    /**     * This class is originally the class that instantiated QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver and placed it into the Spring Application Context     * as a {@link RootResourceInformationHandlerMethodArgumentResolver} by the name of 'repoRequestArgumentResolver'.<br/>     * By injecting this bean, we can let {@link #meetupApiRepoRequestArgumentResolver} delegate as much as possible to the original code in that bean.     */    private final RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;    @Autowired    public CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {        this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;    }    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((GenericApplicationContext) applicationContext).getBeanFactory();        beanFactory.destroySingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME);        beanFactory.registerSingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME,                                      meetupApiRepoRequestArgumentResolver(applicationContext, repositoryRestMvcConfiguration));    }    /**     * This code is mostly copied from {@link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}, except the if clause checking if the QueryDsl library is     * present has been removed, since we're counting on it anyway.<br/>     * That means that if that code changes in the future, we're going to need to alter this code... :/     */    @Bean    public RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver(ApplicationContext applicationContext,                                                                                                     RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {        QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);        QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),                                                                                 factory.getEntityPathResolver());        return new CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),                                                               repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),                                                               repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),                                                               predicateBuilder, factory);    }}

Create a Map-searching predicate from http params

Extend RootResourceInformationHandlerMethodArgumentResolver

And these are the snippets of code that create my own Map-searching predicate based on the http query parameters.Again - would love to know a better way.

The postProcess method calls:

        predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();

just before the predicate reference is passed into the QuerydslRepositoryInvokerAdapter constructor and returned.

Here is that addCustomMapPredicates method:

    private BooleanBuilder addCustomMapPredicates(MultiValueMap<String, String> parameters, Predicate predicate, Class<?> domainType) {        BooleanBuilder booleanBuilder = new BooleanBuilder();        parameters.keySet()                  .stream()                  .filter(s -> s.contains("[") && matches(s) && s.endsWith("]"))                  .collect(Collectors.toList())                  .forEach(paramKey -> {                      String property = paramKey.substring(0, paramKey.indexOf("["));                      if (ReflectionUtils.findField(domainType, property) == null) {                          LOGGER.warn("Skipping predicate matching on [%s]. It is not a known field on domainType %s", property, domainType.getName());                          return;                      }                      String key = paramKey.substring(paramKey.indexOf("[") + 1, paramKey.indexOf("]"));                      parameters.get(paramKey).forEach(value -> {                          if (!StringUtils.hasLength(value)) {                              booleanBuilder.or(matchesProperty(key, null));                          } else {                              booleanBuilder.or(matchesProperty(key, value));                          }                      });                  });        return booleanBuilder.and(predicate);    }    static boolean matches(String key) {        return PATTERN.matcher(key).matches();    }

And the pattern:

    /**     * disallow a . or ] from preceding a [     */    private static final Pattern PATTERN = Pattern.compile(".*[^.]\\[.*[^\\[]");


I spent a few days looking into how to do this. In the end I just went with manually adding to the predicate. This solution feels simple and elegant.

So you access the map via

GET /api/meetup?properties.aKey=aValue

On the controller I injected the request parameters and the predicate.

public List<Meetup> getMeetupList(@QuerydslPredicate(root = Meetup.class) Predicate predicate,                                                @RequestParam Map<String, String> allRequestParams,                                                Pageable page) {    Predicate builder = createPredicateQuery(predicate, allRequestParams);    return meetupRepo.findAll(builder, page);}

I then just simply parsed the query parameters and added contains

private static final String PREFIX = "properties.";private BooleanBuilder createPredicateQuery(Predicate predicate, Map<String, String> allRequestParams) {    BooleanBuilder builder = new BooleanBuilder();    builder.and(predicate);    allRequestParams.entrySet().stream()            .filter(e -> e.getKey().startsWith(PREFIX))            .forEach(e -> {                var key = e.getKey().substring(PREFIX.length());                builder.and(QMeetup.meetup.properties.contains(key, e.getValue()));            });    return builder;}