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.
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:just before the
predicate
reference is passed into theQuerydslRepositoryInvokerAdapter
constructor and returned.Here is that
addCustomMapPredicates
method:And the pattern: