Querydsl Value operator SDK’s example application provides a simplistic Employee Registry API system and illustrates the use of Spring Data Querydsl Value operators usage in a way that empowers this example API to offer rich Employee Search interface.

Full SDK documentation can be located here

Schematics

Application is developed using Spring Boot 2.x with embedded Tomcat server. It utilizes an embedded MongoDB server for repository.

Resource Model

Resource model of this API can be found in org.bitbucket.gt_tech.spring.data.querydsl.value.operators.example.model package. Note the use of Document annotation on top level resource, Employee. This annotation though is optional for Spring Data MongoDB in case of a single-tenant/datastore API but is used by the Maven Querdsl API code generation to identify candidates for which Q-Classes (Q-Types) needs to be generated.

Resource Model Q-Type classes generation

Project’s POM file has following plugin defined which generates the Q-Type classes before compilation phase.

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>${plugin.mongodb.mysema.apt.version}</version>
    <dependencies>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>4.1.4</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
                <outputDirectory>${project.build.directory}/generated-sources/querydsl/java</outputDirectory>
                <!-- Works with Entity Annotation -->
                <!--<processor>com.querydsl.apt.morphia.MorphiaAnnotationProcessor</processor> -->
                <!-- Works with QueryEntity annotation -->
                <!--<processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor> -->
                <processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
                <options>
                    <querydsl.logInfo>true</querydsl.logInfo>
                    <querydsl.listAccessors>false</querydsl.listAccessors>
                    <querydsl.useGetters>true</querydsl.useGetters>
                    <querydsl.unknownAsEmbeddable>true</querydsl.unknownAsEmbeddable>
                </options>
            </configuration>
        </execution>
    </executions>
</plugin>

POM also pulls in spring-boot-starter-data-mongodb artifact to provide run-time bootstrapping for satisfying the provided scoped dependencies of core SDK library, this was anyway required to make Spring data work with MongoDB.

Test employee data

Application loads test data for handful of employees in embedded MongoDB at the time of startup. Test data can be found here. Data load is handled by EmployeeDataBootstrap

Repository

EmployeeRepository is the main repository interface which as per guidance in core SDK library’s documentation extends QuerydslPredicateExecutor and QuerydslBinderCustomizer.

To keep example API code small, this repository is also annotated with Spring MVC specific annotations so it also acts as a RestController by utilizing Spring Data Rest framework.

Following two methods in repository provide the search capabilities: * Generic search with value operator support

@RequestMapping(path = { "/search" }, produces = { MediaType.APPLICATION_JSON_VALUE }, method = { RequestMethod.GET,
			RequestMethod.POST })
	default ResponseEntity<Iterable<Employee>> search(
			@ApiIgnore @QuerydslPredicate(root = Employee.class) Predicate predicate, @PageableDefault Pageable pageable) {
		if (predicate == null || (BooleanBuilder.class.isAssignableFrom(predicate.getClass())
				&& !((BooleanBuilder) predicate).hasValue())) {
			return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
		} else {
			return ResponseEntity.ok(this.findAll(predicate, pageable));
		}
	}
  • An example of frequently used searches which may change on API specific reasons but it’s internal implementation delegation to generic search
@RequestMapping(path = { "/emails/{emailAddress}" }, produces = { MediaType.APPLICATION_JSON_VALUE }, method = {
			RequestMethod.GET, RequestMethod.POST })
	default ResponseEntity<Iterable<Employee>> search(@PathVariable String emailAddress) {
		if (StringUtils.isBlank(emailAddress)) {
			return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
		} else {
			return search(QEmployee.employee.emails.any().address.eq(emailAddress), PageRequest.of(0, 10));
		}
	}

customize(..) method’s implementation illustrates the usage of ExpressionProviderFactory to enable rich value operators on search fields.

Integration tests

Example application is equipped with e2e integration tests demonstrating various queries and how the result/response varies depending on different usage of value operators. Example queries mentioned below in this document can also be seen within EmployeeSearchIT test class.

To run integration tests from Maven:

mvn clean verify

Tests can also be executed from IDE directly after example application is imported

How to start Maven application

Since example application uses Embedded MongoDB, the startup may take time as it downloads the required binaries. Note that MongoDB is started as a forked process so process running the example API must have administrative privelege on user’s machine as otherwise startup may fail.

By default application will start on port 8080 which can be overridden by providing a JVM property server.port to a different value.

From Maven

mvn clean spring-boot:run

From IDE

Search Queries for Employee Search interface

  • Find employee by username
http://localhost:8080/employees/search?userName=dgayle
  • Find employee having one of the two userNames (OR clause)
http://localhost:8080/employees/search?userName=dgayle&userName=ssmith
  • Find employee(s) having a specific username or containing another text in username value
http://localhost:8080/employees/search?userName=dgayle&userName=contains(smith)
  • Find employees having a specific username or containing another text in username value but exclude users who username starts with ‘k’
http://localhost:8080/employees/search?userName=dgayle&userName=contains(smith)&userName=and(not(startsWith(k)))
  • Find employee by email address
http://localhost:8080/employees/search?emails.address=ssmith@company.com
  • Find employee not having the supplied email address
http://localhost:8080/employees/search?emails.address=ne(ssmith@company.com)
  • Find employees having a “company” email address
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)
  • Find employees with an email in “company” email domain but exclude those who also have an email address in “dummy.com” email provider
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&emails.address=and(not(endsWith(@dummy.com)))
  • Find employees with email either in “company” email domain or else “dummy” email domain
http://localhost:8080/employees/search?emails.address=endsWith(@example.com)&emails.address=or(endsWith(@dummy.com))
  • Find employees having emails both in “company” and “dummy” email domain
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&emails.address=and(endsWith(@dummy.com))
  • Find employee in SALES having a company’s email
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&jobData.department=and(SALES)
  • Find employee outside SALES having a company’s email
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&jobData.department=and(ne(SALES))
  • Find all LOCKED employees having a company email
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&status=LOCKED
  • Find all ACTIVE employees having company’s email
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&status=ne(LOCKED)

http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&status=eq(ACTIVE)

http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&status=ACTIVE
  • Find all employees having a company’s email and are at minimum 41 years old.
http://localhost:8080/employees/search?emails.address=endsWith(@company.com)&profile.age=gte(41)
  • Find all employees having a company’s email and are older than 41 years.
http://localhost:8080/employees/search?profile.age=gt(41)
  • Find all employees having a company’s email and are younger than 41 years.
http://localhost:8080/employees/search?profile.age=lt(41)
  • Example for a search field focused REST uri for a frequently used search (e.g. Email based search in this example application)
http://localhost:8080/employees/emails/dgayle@company.com
  • Example(s) for a searching based on date of birth (java.util.Date). These examples utilizes native Spring Data QueryDSL logic to perform a variety of different ways a search can be executed without the use of value operators SDK library. Please note how a ConversionService delegate was provided to QuerydslPredicateArgumentResolverBeanPostProcessor in QueryDslValueOperatorsConfig.java to ensure that even with advanced experimental features, Date type conversion happens appropriately for query execution. It is advised to check the bindings on employee.profile.dob field in EmployeeRepository class.
- Search a user with specific date of birth: 
http://localhost:8080/employees/search?profile.dob=1984-01-10

- Search all users with date of birth in specific date range: 
http://localhost:8080/employees/search?profile.dob=1994-01-10&profile.dob=1962-01-10

- Search all users with date of birth matching with any of the input: 
http://localhost:8080/employees/search?profile.dob=1984-01-10&profile.dob=1986-07-11&profile.dob=1978-10-08&profile.dob=1962-01-10

Exercise

What changes would need to be made to allow searches to find employees between certain age group? (e.g. Find employees in 40 and 50 years old age group)

Obviously that search will not work in out of the box example application and is intentionally done so to facilitate this exercise.

Solution for this exercise can be referred here