QueryDsl 网络查询 Map 字段的键
QueryDsl web query on the key of a Map field
概览
给定
- Spring 数据 JPA,Spring 数据休息,QueryDsl
- 一个
Meetup
实体
- 带有
Map<String,String> properties
字段
- 坚持
MEETUP_PROPERTY
table 作为 @ElementCollection
- 一个
MeetupRepository
- 延伸
QueryDslPredicateExecutor<Meetup>
我希望
网络查询
GET /api/meetup?properties[aKey]=aValue
至 return 仅限 属性 条目具有指定键和值的聚会:aKey=aValue。
但是,这对我不起作用。
我错过了什么?
尝试过
简单字段
简单的字段有效,例如名称和描述:
GET /api/meetup?name=whatever
集合字段有效,喜欢参与者:
GET /api/meetup?participants.name=whatever
但不是这个 Map 字段。
自定义 QueryDsl 绑定
我尝试通过存储库自定义绑定
extend QuerydslBinderCustomizer<QMeetup>
并覆盖
customize(QuerydslBindings bindings, QMeetup meetup)
方法,但是当 customize()
方法被命中时,lambda 中的绑定代码没有被命中。
编辑:得知这是因为 QuerydslBindings
评估查询参数的方法不让它与它内部持有的 pathSpecs
映射相匹配 - 其中包含您的自定义绑定。
一些细节
Meetup.properties 字段
@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<>();
自定义查询dsl绑定
编辑:见上;事实证明,这对我的代码没有任何作用。
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;
});
}
其他发现
QuerydslPredicateBuilder.getPredicate()
要求 QuerydslBindings.getPropertyPath()
尝试 2 种方法来 return 一条路径,这样它就可以创建一个 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
可以使用的谓词。
- 1 是查看自定义绑定。我在那里看不到任何表达地图查询的方法
- 2 默认为 Spring 的 bean 路径。那里有相同的表达问题。你如何表达地图?
所以看起来不可能让
QuerydslPredicateBuilder.getPredicate()
自动创建谓词。
很好 - 我可以手动完成,如果我可以连接到 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
如何覆盖 class 或替换 bean?它在 RepositoryRestMvcConfiguration.repoRequestArgumentResolver()
bean 声明中被实例化并 returned 作为一个 bean。
- 我 可以 通过声明我自己的
repoRequestArgumentResolver
bean 来覆盖那个 bean,但是它没有被使用。
- 它被
RepositoryRestMvcConfiguration
覆盖。我无法通过设置 @Primary
或 @Ordered(HIGHEST_PRECEDENCE)
来 强制 它。
- 我可以通过明确的组件扫描
RepositoryRestMvcConfiguration.class
来强制它,但这也会弄乱Spring引导的自动配置,因为它会导致
RepositoryRestMvcConfiguration's
待处理的 bean 声明
在任何自动配置运行之前。除其他外,这导致杰克逊以不受欢迎的方式序列化响应。
问题
嗯 - 看来我所期望的支持并不存在。
所以问题就变成了:
我如何 正确覆盖 repoRequestArgumentResolver
bean?
顺便说一句 - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
尴尬地非 public。 :/
替换 Bean
实施 ApplicationContextAware
这就是我在应用程序上下文中替换 bean 的方式。
感觉有点hacky。我很想听听更好的方法。
@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);
}
}
从 http 参数创建地图搜索谓词
扩展 RootResourceInformationHandlerMethodArgumentResolver
这些是根据 http 查询参数创建我自己的地图搜索谓词的代码片段。
再次 - 很想知道更好的方法。
postProcess
方法调用:
predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();
只是 在 predicate
引用被传递到 QuerydslRepositoryInvokerAdapter
构造函数并返回之前。
这是 addCustomMapPredicates
方法:
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();
}
和模式:
/**
* disallow a . or ] from preceding a [
*/
private static final Pattern PATTERN = Pattern.compile(".*[^.]\[.*[^\[]");
我花了几天时间研究如何做到这一点。最后我只是 手动 添加到谓词。这个解决方案感觉简单而优雅。
因此您通过
访问地图
GET /api/meetup?properties.aKey=aValue
我在控制器上注入了请求参数和谓词。
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);
}
然后我只是简单地解析了查询参数并添加了 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;
}
概览
给定
- Spring 数据 JPA,Spring 数据休息,QueryDsl
- 一个
Meetup
实体- 带有
Map<String,String> properties
字段- 坚持
MEETUP_PROPERTY
table 作为@ElementCollection
- 坚持
- 带有
- 一个
MeetupRepository
- 延伸
QueryDslPredicateExecutor<Meetup>
- 延伸
我希望
网络查询
GET /api/meetup?properties[aKey]=aValue
至 return 仅限 属性 条目具有指定键和值的聚会:aKey=aValue。
但是,这对我不起作用。 我错过了什么?
尝试过
简单字段
简单的字段有效,例如名称和描述:
GET /api/meetup?name=whatever
集合字段有效,喜欢参与者:
GET /api/meetup?participants.name=whatever
但不是这个 Map 字段。
自定义 QueryDsl 绑定
我尝试通过存储库自定义绑定
extend QuerydslBinderCustomizer<QMeetup>
并覆盖
customize(QuerydslBindings bindings, QMeetup meetup)
方法,但是当 customize()
方法被命中时,lambda 中的绑定代码没有被命中。
编辑:得知这是因为 QuerydslBindings
评估查询参数的方法不让它与它内部持有的 pathSpecs
映射相匹配 - 其中包含您的自定义绑定。
一些细节
Meetup.properties 字段
@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<>();
自定义查询dsl绑定
编辑:见上;事实证明,这对我的代码没有任何作用。
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;
});
}
其他发现
QuerydslPredicateBuilder.getPredicate()
要求QuerydslBindings.getPropertyPath()
尝试 2 种方法来 return 一条路径,这样它就可以创建一个QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
可以使用的谓词。- 1 是查看自定义绑定。我在那里看不到任何表达地图查询的方法
- 2 默认为 Spring 的 bean 路径。那里有相同的表达问题。你如何表达地图?
所以看起来不可能让
QuerydslPredicateBuilder.getPredicate()
自动创建谓词。 很好 - 我可以手动完成,如果我可以连接到QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
如何覆盖 class 或替换 bean?它在 RepositoryRestMvcConfiguration.repoRequestArgumentResolver()
bean 声明中被实例化并 returned 作为一个 bean。
- 我 可以 通过声明我自己的
repoRequestArgumentResolver
bean 来覆盖那个 bean,但是它没有被使用。- 它被
RepositoryRestMvcConfiguration
覆盖。我无法通过设置@Primary
或@Ordered(HIGHEST_PRECEDENCE)
来 强制 它。 - 我可以通过明确的组件扫描
RepositoryRestMvcConfiguration.class
来强制它,但这也会弄乱Spring引导的自动配置,因为它会导致RepositoryRestMvcConfiguration's
待处理的 bean 声明 在任何自动配置运行之前。除其他外,这导致杰克逊以不受欢迎的方式序列化响应。
- 它被
问题
嗯 - 看来我所期望的支持并不存在。
所以问题就变成了:
我如何 正确覆盖 repoRequestArgumentResolver
bean?
顺便说一句 - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
尴尬地非 public。 :/
替换 Bean
实施 ApplicationContextAware
这就是我在应用程序上下文中替换 bean 的方式。
感觉有点hacky。我很想听听更好的方法。
@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);
}
}
从 http 参数创建地图搜索谓词
扩展 RootResourceInformationHandlerMethodArgumentResolver
这些是根据 http 查询参数创建我自己的地图搜索谓词的代码片段。 再次 - 很想知道更好的方法。
postProcess
方法调用:
predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();
只是 在 predicate
引用被传递到 QuerydslRepositoryInvokerAdapter
构造函数并返回之前。
这是 addCustomMapPredicates
方法:
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();
}
和模式:
/**
* disallow a . or ] from preceding a [
*/
private static final Pattern PATTERN = Pattern.compile(".*[^.]\[.*[^\[]");
我花了几天时间研究如何做到这一点。最后我只是 手动 添加到谓词。这个解决方案感觉简单而优雅。
因此您通过
访问地图GET /api/meetup?properties.aKey=aValue
我在控制器上注入了请求参数和谓词。
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);
}
然后我只是简单地解析了查询参数并添加了 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;
}