springrestspring-bootspring-dataquerydsl

How to easy implement 'REST API query language' with Querydsl and Spring Data to filter the entities?


How to easy implement a kind of 'REST API query language' with Spring Data to filter the entities?

For example, for the following Person entity:

@Data
@Entity
public class Person {

  @Id
  @GeneratedValue
  private Long id;

  private LocalDate dob; // date of birth

  private String name;

  @Formula("timestampdiff('year', dob, now())")
  private Integer age;

  public Person(String name, LocalDate dob) {
    this.name = name;
    this.dob = dob;
  }
}

I would like to get its data with such a request:

GET /people?name=jo&age=18&page=1&sort=name,desc

I.e.: 'get the 1st page of all people whose name contains "jo" (case insensitive) and whose age is equal to 18, sorting by name in descending order'.


Solution

  • With help of Querydsl Web Support, the part of Web support Spring Data extension, we can easy implement a kind of 'REST API query language' to filter our entities.

    All we need is do the following:

    1) extend our repository from QuerydslPredicateExecutor,

    2) add Predicate with annotation @QuerydslPredicate, as argument, to our REST controller method

    3) use this predicate in findAll method of the repository:

    public interface PersonRepo extends JpaRepository<Person, Long>, QuerydslPredicateExecutor<Person> {
    } 
    
    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/people")
    public class PersonController {
    
        private final PersonRepo personRepo;
    
        @GetMapping
        public ResponseEntity getFiltered(@QuerydslPredicate(root = Person.class) Predicate predicate, Pageable pageable) {
            return ResponseEntity.ok(personRepo.findAll(predicate, pageable)));
        }
    }
    

    Then we will be able to request our data:

    GET /people?name=John&age=18&page=1&sort=name,desc
    

    Next we have to make case insensitive 'like' filter. To do this we extend our repo from QuerydslBinderCustomizer and override its customize method (right in the repo):

    public interface PersonRepo extends
            JpaRepository<Person, Long>,
            QuerydslPredicateExecutor<Person>,
            QuerydslBinderCustomizer<QPerson> {
    
        @Override
        default void customize(QuerydslBindings bindings, QPerson person) {
    
            // Make case-insensitive 'like' filter for all string properties 
            bindings.bind(String.class).first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
        }
    }
    

    To make it works we have to add parameter bindings to @QuerydslPredicate of our controller method:

    @GetMapping
    public ResponseEntity getFiltered(
        @QuerydslPredicate(root = Person.class, bindings = PersonRepo.class) Predicate predicate, 
        Pageable pageable
    ) {
        return ResponseEntity.ok(personRepo.findAll(predicate, pageable)));
    }
    

    Now we can request our data as asked in the question:

    GET /people?name=jo&age=18&page=1&sort=name,desc
    

    With QuerydslBinderCustomizer we can implement more complex filters, for example between and greater or equal filters (add this code to customize method):

    bindings.bind(person.age).all((path, value) -> {
        Iterator<? extends Integer> it = value.iterator();
        Integer from = it.next();
        if (value.size() >= 2) {
            Integer to = it.next();
            return Optional.of(path.between(from, to)); // between
        } else {
            return Optional.of(path.goe(from)); // greater or equal
        }
    });
    

    If we specify two age parameters in the request then we get all records with the age between these parameters. If we specify only one age parameter - we get records with the age is greater or equal that value.

    GET /people?age=18&age=30
    

    ... get all people with the age between 18 and 30

    GET /people?age=18
    

    ... get all people with the age is greater or equal than 18

    In the end we can exclude some unnecessary properties from the filter, for example the entity id (add this code to customize method):

    bindings.excluding(person.id);
    

    To use Querydsl Web Support we have to add these dependencies and plugin to our Spring Boot project:

    <dependencies>
        <!-- ... -->
    
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- ... -->
    
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/annotations</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    

    Then, it's important, compile the project to build 'Q-classes' of our entities.

    Full example demo you can find in my repo: sb-querydsl-sd-demo, and Postman API-docs of this demo - here: REST query language with Querydsl and Spring Data.