Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 3526

REST Query Language with Spring Data JPA and QueryDSL

$
0
0

I usually post about REST APIs and HTTP on Google+ - you can follow me there:

1. Overview

In this tutorial, we’re looking at building a query language for a REST API using Spring Data JPA and Querydsl.

In the first two articles of this series, we built the same search/filtering functionality using JPA Criteria and Spring Data JPA Specifications.

So – why a query language? Because – for any complex enough API – searching/filtering your resources by very simple fields is simply not enough. A query language is more flexible, and allows you to filter down to exactly the resources you need.

2. Querydsl Configuration

First – let’s see how to configure our project to use Querydsl.

We need to add the following dependencies to pom.xml:

<dependency> 
    <groupId>com.mysema.querydsl</groupId> 
    <artifactId>querydsl-core</artifactId> 
    <version>3.6.0</version>
</dependency> 
<dependency> 
    <groupId>com.mysema.querydsl</groupId> 
    <artifactId>querydsl-apt</artifactId> 
    <version>3.6.0</version>
    </dependency>
<dependency> 
    <groupId>com.mysema.querydsl</groupId> 
    <artifactId>querydsl-jpa</artifactId> 
    <version>3.6.0</version> 
</dependency>

We also need to configure the APT – Annotation processing tool – plugin as follows:

<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/java</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

3. The User Entity

Next – let’s take a look at the “User” entity which we are going to use in our Search API:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
}

4. Custom Predicate with PathBuilder

Now – let’s create a custom Predicate based on some arbitrary constraints.

We’re using PathBuilder here instead of the automatically generated Q-types because we need to create paths dynamically for more abstract usage:

public class UserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder<User> entityPath = new PathBuilder<User>(User.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath<Integer> path = entityPath.getNumber(criteria.getKey(), Integer.class);
            int value = Integer.parseInt(criteria.getValue().toString());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.eq(value);
            } 
            else if (criteria.getOperation().equalsIgnoreCase(">")) {
                return path.goe(value);
            } 
            else if (criteria.getOperation().equalsIgnoreCase("<")) {
                return path.loe(value);
            }
        } 
        else {
            StringPath path = entityPath.getString(criteria.getKey());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.containsIgnoreCase(criteria.getValue().toString());
            }
        }
        return null;
    }
}

Note how the implementation of the predicate is generically dealing with multiple types of operations. This is because the query language is by definition an open language where you can potentially filter by any field, using any supported operation.

To represent that kind of open filtering criteria, we’re using a simple but quite flexible implementation – SearchCriteria:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

The SearchCriteria holds the details we need to represent a constraint:

  • key: the field name – for example: firstName, age, … etc
  • operation: the operation – for example: Equality, less than, … etc
  • value: the field value – for example: john, 25, … etc

5. UserRepository

Now – let’s take a look at our UserRepository.

We need our UserRepository to extend QueryDslPredicateExecutor so that we can use Predicates later to filter search results:

public interface UserRepository extends JpaRepository<User, Long>, 
                                        QueryDslPredicateExecutor<User> {}

6. Combine Predicates

Next– let’s take a look at combining Predicates to use multiple constraints in results filtering.

In the following example – we work with a builder – UserPredicatesBuilder  – to combine Predicates:

public class UserPredicatesBuilder {
    private List<SearchCriteria> params;

    public UserPredicatesBuilder() {
        params = new ArrayList<SearchCriteria>();
    }

    public UserPredicatesBuilder with(String key, String operation, Object value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    public BooleanExpression build() {
        if (params.size() == 0) {
            return null;
        }

        List<BooleanExpression> predicates = new ArrayList<BooleanExpression>();
        UserPredicate predicate;
        for (SearchCriteria param : params) {
            predicate = new UserPredicate(param);
            BooleanExpression exp = predicate.getPredicate();
            if (exp != null) {
                predicates.add(exp);
            }
        }

        BooleanExpression result = predicates.get(0);
        for (int i = 1; i < predicates.size(); i++) {
            result = result.and(predicates.get(i));
        }
        return result;
    }
}

7. Test the Search Queries

Next – let’s test our Search API.

We’ll start by initializing the database with a few users – to have these ready and available for testing:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class JPAQuerydslTest {

    @Autowired
    private UserRepository repo;

    private User userJohn;
    private User userTom;

    @Before
    public void init() {
        userJohn = new User();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("john@doe.com");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new User();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

Next, let’s see how to find users with given last name:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    UserPredicatesBuilder builder = new UserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable<User> results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

Now, let’s see how to find user with given both first and last name:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserPredicatesBuilder builder = new UserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");

    Iterable<User> results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

Next, let’s see how to find user with given both last name and minimum age

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    UserPredicatesBuilder builder = new UserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable<User> results = repo.findAll(builder.build());

    assertThat(results, contains(userTom));
    assertThat(results, not(contains(userJohn)));
}

Now, let’s see how to search for User that doesn’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    UserPredicatesBuilder builder = new UserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable<User> results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

Finally – let’s see how to find a User given only part of the first name – as in the following example:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    UserPredicatesBuilder builder = new UserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable<User> results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

8. UserController

Finally, let’s put everything together and build the REST API.

We’re defining a UserController that defines a simple method findAll() with a “search“ parameter to pass in the query string:

@Controller
public class UserController {

    @Autowired
    private UserRepository repo;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public Iterable<User> search(@RequestParam(value = "search") String search) {
        UserPredicatesBuilder builder = new UserPredicatesBuilder();

        if (search != null) {
            Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
            }
        }
        BooleanExpression exp = builder.build();
        return repo.findAll(exp);
    }
}

Here is a quick test URL example:

http://localhost:8080/users?search=lastName:doe,age>25

And the response:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"tom@doe.com",
    "age":26
}]

9. Conclusion

This third article covered the first steps of building a query language for a REST API, making good use of the Querydsl library.

The implementation is of course early on, but it can easily be evolved to support additional operations.

The full implementation of this article can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

I usually post about REST APIs and HTTP on Google+ - you can follow me there:


Viewing all articles
Browse latest Browse all 3526

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>