Meetup
entity
Map<String,String> properties
field
MEETUP_PROPERTY
table as an @ElementCollection
MeetupRepository
QueryDslPredicateExecutor<Meetup>
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?
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.
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.
@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<>();
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;
});
}
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.
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.
repoRequestArgumentResolver
bean, but it doesn't get used.
RepositoryRestMvcConfiguration
s. I can't force it by setting it @Primary
or @Ordered(HIGHEST_PRECEDENCE)
. 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.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. :/
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);
}
}
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(".*[^.]\\[.*[^\\[]");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With