QueryDsl Web查询Map字段的键 [英] QueryDsl web query on the key of a Map field

查看:248
本文介绍了QueryDsl Web查询Map字段的键的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

概述



给定




  • Spring Data JPA,Spring Data Rest, QueryDsl

  • a Meetup entity


    • 地图<字符串,字符串>属性字段


      • 作为<$ c持久保存在 MEETUP_PROPERTY 表中$ c> @ElementCollection



  • a MeetupRepository


    • 扩展 QueryDslPredicateExecutor< Meetup>




我希望



网页查询

  GET / api / meetup?properties [aKey] = aValue 

仅返回具有指定键和值的属性条目的Meetup:aKey = aValue。



然而,这对我不起作用。
我缺少什么?



尝试



简单字段



简单字段起作用,如名称和描述:

  GET / api / meetup?name = whatever 

收集字段起作用,如参与者:

  GET /api/meetup?participants.name=whatever 

但是不是这个Map字段。



自定义QueryDsl绑定



我试过通过让存储库<自定义绑定/ p>

  extend QuerydslBinderCustomizer< QMeetup> 

并覆盖

 自定义(QuerydslBindings绑定,QMeetup聚会)

方法,但是 customize()方法正在被命中,lambda中的绑定代码不是。



编辑:了解那是因为 QuerydslBindings 评估查询参数的方法不要让它与其内部持有的 pathSpecs 映射相匹配 - 它具有您的自定义绑定在它。



一些细节



Meetup.properties字段



'pre> @ElementCollection(取= FetchType.EAGER)
@CollectionTable(名称= MEETUP_PROPERTY,joinColumns = @JoinColumn(名称= MEETUP_ID))
@MapKeyColumn(name =KEY)
@Column(name =VALUE,length = 2048)
private Map< String,String> properties = new HashMap<>();



自定义querydsl绑定



编辑:见上文;事实证明,这对我的代码没有任何作用。

 公共接口MeetupRepository扩展PagingAndSortingRepository< Meetup,Long>,
QueryDslPredicateExecutor< Meetup>,
QuerydslBinderCustomizer< QMeetup> {

@Override
default void customize(QuerydslBindings bindings,QMeetup meetup){
bindings.bind(meetup.properties).first((path,value) - > {
BooleanBuilder builder = new BooleanBuilder();
for(String key:value.keySet()){
builder.and(path.containsKey(key).and(path.get(key) ).eq(value.get(key))));
}
返回构建器;
});
}



其他调查结果




  1. QuerydslPredicateBuilder.getPredicate()询问 QuerydslBindings.getPropertyPath()试用2种方法返回一个路径,因此它可以使一个谓词 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()可以使用。


    • 1是查看自定义绑定。我没有看到任何表达地图查询的方法

    • 2默认为Spring的bean路径。那里的表达问题相同。你怎么表达地图?
      因此看起来无法获得 QuerydslPredicateBuilder.getPredicate()来自动创建谓词。
      好​​吧 - 我可以手动完成,如果我可以挂钩 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()


如何覆盖该类或替换bean?它被实例化并在 RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean声明中作为bean返回。


  1. 可以通过声明我自己的 repoRequestArgumentResolver bean来覆盖该bean,但它不会被使用。


    • 它被 RepositoryRestMvcConfiguration 覆盖。我不能强制它设置 @Primary @Ordered(HIGHEST_PRECEDENCE)

    • 可以通过显式组件扫描强制它 RepositoryRestMvcConfiguration.class ,但这也搞砸了Spring Boot的自动配置,因为它导致
      RepositoryRestMvcConfiguration的 bean声明在任何自动配置运行之前被处理
      。除其他外,这导致杰克逊以不需要的方式序列化的回复。




< h1>问题

好吧 - 看起来我预期的支持不存在。



所以问题变成:
如何我是否正确覆盖 repoRequestArgumentResolver bean?



BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver 非常不公开。 :/

解决方案

替换Bean



实现ApplicationContextAware



这就是我在应用程序上下文中替换bean的方式。



感觉有点hacky。我很想听到更好的方法。

  @Configuration 
公共类CustomQuerydslHandlerMethodArgumentResolverConfig实现ApplicationContextAware {

/ **
*这个类是原来那个实例QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver并放置到Spring应用上下文
*作为{@link RootResourceInformationHandlerMethodArgumentResolver}通过repoRequestArgumentResolver的名称的类< BR />
*通过注入这个bean,我们可以让{@link #meetupApiRepoRequestArgumentResolver}尽可能地委托该bean中的原始代码。
* /
私有最终RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;

@Autowired
公共CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration){
this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;
}

@覆盖
公共无效setApplicationContext(ApplicationContext中的applicationContext)抛出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));
}

/ **
*此代码主要从{@link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}复制,但if子句检查QueryDsl库是否为
*礼物已被删除,因为无论如何我们都指望它。< br />
*这意味着,如果在未来的代码修改,我们将需要修改这个代码...:/
* /
@Bean
公共RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver (ApplicationContext applicationContext,
RepositoryRestMvcConfiguration repositoryRestMvcConfiguration){
QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),
factory.getEntityPathResolver());

返回新的CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),
repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),
repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),
predicateBuilder,factory );
}
}



从http参数创建地图搜索谓词



扩展RootResourceInformationHandlerMethodArgumentResolver



这些是基于http查询创建我自己的地图搜索谓词的代码片段参数。
再次 - 很想知道更好的方法。



postProcess 方法调用:

  predicate = addCustomMapPredicates(parameterMap,predicate,domainType).getValue();在谓词  

$ c>引用传递到 QuerydslRepositoryInvokerAdapter 构造函数并返回。



这是 addCustomMapPredicates 方法:

 私有BooleanBuilder addCustomMapPredicates(MultiValueMap<字符串,字符串>参数,谓词谓词,类< ;?> 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(在[%s]上跳过谓词匹配。它不是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));
}
});
});
返回booleanBuilder.and(谓词);
}

静态布尔匹配(字符串键){
返回PATTERN.matcher(键).matches();
}

模式:

  / ** 
*不允许a。或者]来自前面的[
* /
私有静态最终模式PATTERN = Pattern.compile(。* [^。] \\ [。* [^ \\ []) ;


Overview

Given

  • Spring Data JPA, Spring Data Rest, QueryDsl
  • a Meetup entity
    • with a Map<String,String> properties field
      • persisted in a MEETUP_PROPERTY table as an @ElementCollection
  • a MeetupRepository
    • that extends QueryDslPredicateExecutor<Meetup>

I'd expect

A web query of

GET /api/meetup?properties[aKey]=aValue

to return only Meetups with a property entry that has the specified key and value: aKey=aValue.

However, that's not working for me. What am I missing?

Tried

Simple Fields

Simple fields work, like name and description:

GET /api/meetup?name=whatever

Collection fields work, like participants:

GET /api/meetup?participants.name=whatever

But not this Map field.

Customize QueryDsl bindings

I've tried customizing the binding by having the repository

extend QuerydslBinderCustomizer<QMeetup>

and overriding the

customize(QuerydslBindings bindings, QMeetup meetup)

method, but while the customize() method is being hit, the binding code inside the lambda is not.

EDIT: Learned that's because QuerydslBindings means of evaluating the query parameter do not let it match up against the pathSpecs map it's internally holding - which has your custom bindings in it.

Some Specifics

Meetup.properties field

@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "MEETUP_PROPERTY", joinColumns = @JoinColumn(name = "MEETUP_ID"))
@MapKeyColumn(name = "KEY")
@Column(name = "VALUE", length = 2048)
private Map<String, String> properties = new HashMap<>();

customized querydsl binding

EDIT: See above; turns out, this was doing nothing for my code.

public interface MeetupRepository extends PagingAndSortingRepository<Meetup, Long>,
                                          QueryDslPredicateExecutor<Meetup>,
                                          QuerydslBinderCustomizer<QMeetup> {

    @Override
    default void customize(QuerydslBindings bindings, QMeetup meetup) {
        bindings.bind(meetup.properties).first((path, value) -> {
            BooleanBuilder builder = new BooleanBuilder();
            for (String key : value.keySet()) {
                builder.and(path.containsKey(key).and(path.get(key).eq(value.get(key))));
            }
            return builder;
        });
}

Additional Findings

  1. QuerydslPredicateBuilder.getPredicate() asks QuerydslBindings.getPropertyPath() to try 2 ways to return a path from so it can make a predicate that QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess() can use.
    • 1 is to look in the customized bindings. I don't see any way to express a map query there
    • 2 is to default to Spring's bean paths. Same expression problem there. How do you express a map? So it looks impossible to get QuerydslPredicateBuilder.getPredicate() to automatically create a predicate. Fine - I can do it manually, if I can hook into QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()

HOW can I override that class, or replace the bean? It's instantiated and returned as a bean in the RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean declaration.

  1. I can override that bean by declaring my own repoRequestArgumentResolver bean, but it doesn't get used.
    • It gets overridden by RepositoryRestMvcConfigurations. I can't force it by setting it @Primary or @Ordered(HIGHEST_PRECEDENCE).
    • I can force it by explicitly component-scanning RepositoryRestMvcConfiguration.class, but that also messes up Spring Boot's autoconfiguration because it causes RepositoryRestMvcConfiguration's bean declarations to be processed before any auto-configuration runs. Among other things, that results in responses that are serialized by Jackson in unwanted ways.

The Question

Well - looks like the support I expected just isn't there.

So the question becomes: HOW do I correctly override the repoRequestArgumentResolver bean?

BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver is awkwardly non-public. :/

解决方案

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.

@Configuration
public 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(".*[^.]\\[.*[^\\[]");

这篇关于QueryDsl Web查询Map字段的键的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆