A lightweight abstraction for constructing queries oriented around domain models / entities that minimizes magic strings, provides type safety, produces queries that read well, and provides a decoupled way to define new query target formats - allowing you to change your mind about where you store your data without having to change all of your query code.
A lot of existing query builders are lacking. It's difficult to write a query builder that is both convenient and safe, which has resulted in most query builders giving up on type safety or allowing you to call methods that don't make sense to be able to call at that time.
It uses the type system to enforce that you won't call an unapplicable method at any point as you build your queries. Also, there's no need to worry about someone passing an integer to a string field, etc. It supports both chaining and composition to build a query and when used with static imports it becomes a succinct (as far as java goes) query DSL.
- Java Predicates
- A string in RSQL format
- Elasticsearch's QueryBuilder
- Spring Data's MongoDB Criteria
//--------------------------------------------------------------------------
// define a single query model for each of the models you want to be able to
// query against. Return the appropriate property interface for each field
// based on the type of the value on your model
//--------------------------------------------------------------------------
public class PersonQuery extends QBuilder<PersonQuery> {
public StringProperty<PersonQuery> firstName() {
return stringField("firstName");
}
public IntegerProperty<PersonQuery> age() {
return integerField("age");
}
}
Condition<PersonQuery> q = new PersonQuery().firstName().eq("Paul").and().age().eq(23);
String rsql = q.query(new RSQLVisitor());
// firstName==Paul;age==23
Criteria mongoCriteria = q.query(new MongoVisitor());
// {firstName: "Paul", age: 23}
FilterBuilder filter = q.query(new ElasticsearchVisitor());
// { "and" : { "filters" : [ { "term" : { "firstName" : "Paul" } }, { "term" : { "age" : 23 } } ] } }
Predicate<Person> predicate = q.query(new PredicateVisitor<>());
// List<Person> personsNamedPaulAndAge23 = persons.stream().filter(predicate).collect(toList());
public class PersonQuery extends QBuilder<PersonQuery> {
public static StringProperty<PersonQuery> firstName() {
return new PersonQuery().stringField("firstName");
}
public static IntegerProperty<PersonQuery> age() {
return new PersonQuery().integerField("age");
}
}
import static com.company.queries.PersonQuery.*;
Condition<PersonQuery> query = firstName().eq("Paul").or(and(firstName().ne("Richard"), age().gt(22)));
Chaining with the infix forms of and()
and or()
is complicated when you use both and then want to talk about
precedence. The implementation is such that whenever you change from a chain of and()
to
a chain of or()
(or vice versa), then the previous statements are wrapped together and the existing set
becomes one of the elements in the new or()
(or and()
) chain.
In general, to avoid unintended precedence concerns, it's best to limit your chains
to only and()
or or()
operators and use the compositional forms
and(Condition<T> c1, Condition<T> c2, Condition<T>... cn)
and
or(Condition<T> c1, Condition<T> c2, Condition<T>... cn)
for queries that must mix the two.
Many criteria / query builders don't provide much in the way of a good "user experience". You can use q-builders simply because they provide an improvement over these.
By using a layer that provides some decoupling from your storage, if you decide to swap out a datastore for something else but you're not also changing your data's structure then you can just write a new target backend and keep your query building the same.
Chances are you want to provide an ability for people to query against your API, but you also make queries against the database yourself in the implementation of business logic on the server. One option could be to use these builders to define one query model in a shared jar that you use in both your SDK and API and simply target different query formats. Like RSQL for over-the-wire and directly into mongo criteria on the API server.
The RSQL builder introduces some new operators to the standard RSQL set. Since this library only builds queries it doesn't dictate what you use to parse them. However, it's written specifically to be compatible with rsql-parser. So, you should make sure that you add the following operators before parsing:
- "=ex=" The exists clause. It has values of either
true
orfalse
. - "=q=" The subquery clause. It has a string value that itself might be an entire RSQL query.
- "=re=" A regular expression as a string. The string will be passed as-is to the backend visitor you use, so the regex string must be in the same flavor as the visitor you intend to use (mongo regex vs java regex, etc)
<dependencies>
<dependency>
<groupId>com.github.rutledgepaulv</groupId>
<artifactId>q-builders</artifactId>
<version>1.5</version>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>com.github.rutledgepaulv</groupId>
<artifactId>q-builders</artifactId>
<version>1.6-SNAPSHOT</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>ossrh</id>
<name>Repository for snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- only necessary if you're using the spring data mongodb criteria target type -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.9.2.RELEASE</version>
</dependency>
<!-- only necessary if you're using the elasticsearch filter builder target type -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>2.3.4</version>
</dependency>
</dependencies>
This project is licensed under MIT license.