Skip to content

Instantly share code, notes, and snippets.

@odrotbohm
Last active October 22, 2021 09:08
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save odrotbohm/decf03d4948cd58a51bc to your computer and use it in GitHub Desktop.
Save odrotbohm/decf03d4948cd58a51bc to your computer and use it in GitHub Desktop.
Dynamic, Querydsl-based filter bindings using Spring Data REST

We’re just experimenting with some Spring Data / Spring MVC integration that will allow you to bind property path expressions in the request to a Querydsl Predicate as a Spring MVC controller method argument. This is cool for manually implemented controllers so I wondered what it takes to integrate that with Spring Data REST.

In the example above you see StoreRepository extending QuerydslPredicateExecutor<Store>. In the coming version of Spring Data REST, this will cause the collection resource exposed for the repository to accept property based filtering:

GET /stores?name=Foo

will return all stores with a name of "Foo". A plain equals(…) comparison might be a bit strict, so we expose a QuerydslBinderCustomizer interface that will allow you to customize the way properties are bound within the overall predicate.

As you can see above, the easiest way to define customized bindings is by implementing customize(…) in a default method and use the provided QuerydslBindings and entity path to tweak the binding as you like. You see we define the city to be bound via endsWith(…), String properties in general are bound via a contains(…) to make the resulting predicate less restrictive.

A working example can be found in this feature branch of the Spring Data Examples repository.

public interface StoreRepository extends PagingAndSortingRepository<Store, String>,
QueryDslPredicateExecutor<Store>, QuerydslBinderCustomizer<QStore> {
@RestResource(rel = "by-location")
Page<Store> findByAddressLocationNear(Point location, Distance distance, Pageable pageable);
default void customize(QuerydslBindings bindings, QStore store) {
bindings.bind(store.address.city).single((path, value) -> path.startsWith(value));
bindings.bind(String.class).single((StringPath path, String value) -> path.contains(value));
}
}
@hrandika
Copy link

Where is the 'QStore' comes from? Is this works only with MongoDB? any clue to get this work with mysql or similar databases?

@wwadge
Copy link

wwadge commented Sep 15, 2015

@hrandika QStore comes from queryDSL and is generated automatically if you set it up correctly

@hrandika
Copy link

@wwadge. Thanks got it working after setting up correctly.

@hrandika
Copy link

hrandika commented Nov 8, 2015

Can we have other examples (if they are implemented or available)? Like date between,Search form list or set within a entity?

@pwachira
Copy link

This looks to be a godsend....

@zandrewitte
Copy link

@hrandika, Have you gotten any feedback or found a way to do a multiple value query, like a date between search?

@jihlee
Copy link

jihlee commented Aug 27, 2016

@hrandika, @zandrewitte, have a look at the following thread.
http://stackoverflow.com/a/35158320/4477227

The answer was written by Oliver and it shows how to accomplish something like date between search.

@fernandodof
Copy link

fernandodof commented Sep 20, 2016

Hello everyone. I have an entity which I want to search with contains and sometimes with containsIngonoreCase. Is there a way to change this dynamically ?

default void customize(QuerydslBindings bindings, Quser user) {
    //change based on request
    bindings.bind(user.name).first((path, value) -> path.contains(value));
    //OR
    bindings.bind(user.name).first((path, value) -> path.containsIgnoreCase(value));
}

///UPDATED (Answering my own question above)

bindings.bind(String.class).first((StringPath path, String value) -> {
       if (value.startsWith("%") && value.endsWith("%")) {
           return path.containsIgnoreCase(value.substring(1, value.length() - 1));
       } else {
           return path.eq(value);
       }
});

@woemler
Copy link

woemler commented Jan 16, 2017

The above comment has code that works nicely for string values, but what about numeric? If I wanted a query string (for Spring Data REST) that translated to a greater-than or less-than operation, I could not include string characters in the value. Can a single path have multiple aliases that resolve to different query operations. For example:

/api/people?age=30 
/api/people?ageGreaterThan=30  
/api/people?ageBetween=30,50  

@fernandodof
Copy link

Good question @woemler

@Ramblurr
Copy link

Ramblurr commented Feb 27, 2017

Indeed, I'd love to know the answer to that question @woemler.

Similarly, I'm using a Projection to create a "virtual" field aggregate on a class, and I want to be able to filter on that virtual field. But of course that virtual field doesn't appear in the querydsl Q class, so I can't bind it using this API, even though I can trivially express the predicate as a querydsl boolean expression.

Perhaps @olivergierke will see this gist again.

@woemler
Copy link

woemler commented Mar 9, 2017

That is an interesting problem, @Ramblurr. Is it possible to create entity classes that represent database views, rather than tables? That might be a workaround to your problem.

@KhaledLela
Copy link

KhaledLela commented May 12, 2017

If Store has a collection of products
/api/store?products.productName=Sony%20Vaio // Returns Stores that has Sony Viao on it's products Good, But all products returned,
I need Only product with name Sony Viao.
default void customize(QuerydslBindings bindings, QStore store) { bindings.bind(store.products.any().first((path, value) -> path.equals(value)); }
Stackoverflow
Any help ?

@ZhongjunTian
Copy link

ZhongjunTian commented Jun 9, 2017

Hi
I have a good implementation of JPA Specification which could be much more powerful than you guys need.
You can also filter data by ANY complex logic (and, or) and ANY operations (equal, startswith, endswith, greaterthan...)
But you just need write ZERO line of code.
https://github.com/ZhongjunTian/spring-repository-plus/

@ArslanAnjum
Copy link

why cant we define a binding for 3rd and so on child in bindings.bind.? for example I have defined following binding :

bindings.bind(survey.district).first(
				(path,value)->	survey
								.location
								.mauza
								.patwarCircle
								.qanungoiHalqa
								.tehsil
								.district
								.districtId
								.eq(value)
								);

but this doesnt seem to work and a get an error that no property district.districtId found.

@aycanadalwork
Copy link

How to do "or" search anyone?

@Ahulkv
Copy link

Ahulkv commented Nov 25, 2017

Is it possible to searialize/deserialize the predicate containing complex objects to send it over the request ?

@frosiere
Copy link

Wouldn't it make more sense to follow the same principle as the other search methods by having something like

GET /stores/search/findAll?name=Foo
GET /stores/search/findAll?name=Foo&status=ACTIVE
or
GET /stores/search/findWithFilter?name=Foo

@qyfbq123
Copy link

qyfbq123 commented Jul 2, 2020

The above comment has code that works nicely for string values, but what about numeric? If I wanted a query string (for Spring Data REST) that translated to a greater-than or less-than operation, I could not include string characters in the value. Can a single path have multiple aliases that resolve to different query operations. For example:

/api/people?age=30 
/api/people?ageGreaterThan=30  
/api/people?ageBetween=30,50  

I accomplish numberic range search like date between search, just as @jihlee replied.

bindings.bind(people.age).all((path, values) -> {
            Iterator<? extends Integer> it = values.iterator();
            if(values.size() == 1) {
                Integer v = it.next();
                return Optional.of(path.eq(v));
            } else {
                Integer v = it.next();
                Integer sign = it.next();
                switch (sign) {
                    case 0:
                        return Optional.of(path.eq(v));
                    case -1:
                        return Optional.of(path.lt(v));
                    case -2:
                        return Optional.of(path.loe(v));
                    case 1:
                        return Optional.of(path.gt(v));
                    case 2:
                        return Optional.of(path.goe(v));
                }
            }
            return Optional.empty();
        });

and search people like these:

GET /api/people?age=18&age=0  //people who's age is 18
GET /api/people?age=18&age=-1 //people who's age less than 18
GET /api/people?age=18&age=1 //people who's age greater than 18

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment