QueryDsl Web查询Map字段的键 [英] QueryDsl web query on the key of a Map field
问题描述
概述
给定
- Spring Data JPA,Spring Data Rest, QueryDsl
- a
Meetup
entity
-
地图<字符串,字符串>属性
字段
- 作为<$ c持久保存在
MEETUP_PROPERTY
表中$ c> @ElementCollection
- 作为<$ c持久保存在
-
- 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))));
}
返回构建器;
});
}
其他调查结果
-
QuerydslPredicateBuilder.getPredicate()
询问QuerydslBindings.getPropertyPath()
试用2种方法返回一个路径,因此它可以使一个谓词QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
可以使用。
- 1是查看自定义绑定。我没有看到任何表达地图查询的方法
- 2默认为Spring的bean路径。那里的表达问题相同。你怎么表达地图?
因此看起来无法获得QuerydslPredicateBuilder.getPredicate()
来自动创建谓词。
好吧 - 我可以手动完成,如果我可以挂钩QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
如何覆盖该类或替换bean?它被实例化并在 RepositoryRestMvcConfiguration.repoRequestArgumentResolver()
bean声明中作为bean返回。
- 我可以通过声明我自己的
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之前
$ 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
- persisted in a
- with a
- a
MeetupRepository
- that extends
QueryDslPredicateExecutor<Meetup>
- that extends
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
QuerydslPredicateBuilder.getPredicate()
asksQuerydslBindings.getPropertyPath()
to try 2 ways to return a path from so it can make a predicate thatQuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.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 intoQuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.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.
- I can override that bean by declaring my own
repoRequestArgumentResolver
bean, but it doesn't get used.- It gets overridden by
RepositoryRestMvcConfiguration
s. 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 causesRepositoryRestMvcConfiguration'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.
- It gets overridden by
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屋!