1. Overview

Spring MVC and Spring Data each do a great job simplifying application development in their own right. But, what if we put them together?

In this tutorial, we’ll take a look at Spring Data’s web support and how its resolvers can reduce boilerplate and make our controllers more expressive.

Along the way, we’ll peek at Querydsl and what its integration with Spring Data looks like.

2. A Bit of Background

Spring Data’s web support is a set of web-related features implemented on top of the standard Spring MVC platform, aimed at adding extra functionality to the controller layer.

Spring Data web support’s functionality is built around several resolver classes. Resolvers streamline the implementation of controller methods that interoperate with Spring Data repositories and also enrich them with additional features.

These features include fetching domain objects from the repository layer, without having to explicitly call the repository implementations, and constructing controller responses that can be sent to clients as segments of data that support pagination and sorting.

Also, requests to controller methods that take one or more request parameters can be internally resolved to Querydsl queries.

3. A Demo Spring Boot Project

To understand how we can use Spring Data web support to improve our controllers’ functionality, let’s create a basic Spring Boot project.

Our demo project’s Maven dependencies are fairly standard, with a few exceptions that we’ll discuss later on:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

In this case, we included spring-boot-starter-web, as we’ll use it for creating a RESTful controller, spring-boot-starter-jpa for implementing the persistence layer, and spring-boot-starter-test for testing the controller API.

Since we’ll use H2 as the underlying database, we included com.h2database as well.

Let’s keep in mind that spring-boot-starter-web enables Spring Data web support by default. Hence, we don’t need to create any additional @Configuration classes to get it working within our application.

Conversely, for non-Spring Boot projects, we’d need to define a @Configuration class and annotate it with the @EnableWebMvc and @EnableSpringDataWebSupport annotations.

3.1. The Domain Class

Now, let’s add a simple User JPA entity class to the project, so we can have a working domain model to play with:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private final String name;
   
    // standard constructor / getters / toString

}

3.2. The Repository Layer

To keep the code simple, the functionality of our demo Spring Boot application will be narrowed to just fetching some User entities from an H2 in-memory database.

Spring Boot makes it easy to create repository implementations that provide minimal CRUD functionality out-of-the-box. Therefore, let’s define a simple repository interface that works with the User JPA entities:

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}

There’s nothing inherently complex in the definition of the UserRepository interface, except that it extends PagingAndSortingRepository.

This signals Spring MVC to enable automatic paging and sorting capabilities on database records.

3.3. The Controller Layer

Now, we need to implement at least a basic RESTful controller that acts as the middle tier between the client and the repository layer.

Therefore, let’s create a controller class, which takes a UserRepository instance in its constructor and adds a single method for finding User entities by id:

@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public User findUserById(@PathVariable("id") User user) {
        return user;
    }
}

3.4.  Running the Application

Finally, let’s define the application’s main class and populate the H2 database with a few User entities:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    CommandLineRunner initialize(UserRepository userRepository) {
        return args -> {
            Stream.of("John", "Robert", "Nataly", "Helen", "Mary").forEach(name -> {
                User user = new User(name);
                userRepository.save(user);
            });
            userRepository.findAll().forEach(System.out::println);
        };
    }
}

Now, let’s run the application. As expected, we see the list of persisted User entities printed out to the console on startup:

User{id=1, name=John}
User{id=2, name=Robert}
User{id=3, name=Nataly}
User{id=4, name=Helen}
User{id=5, name=Mary}

4. The DomainClassConverter Class

For now, the UserController class only implements the findUserById() method.

At first sight, the method implementation looks fairly simple. But it actually encapsulates a lot of Spring Data web support functionality behind the scenes.

Since the method takes a User instance as an argument, we might end up thinking that we need to explicitly pass the domain object in the request. But, we don’t.

Spring MVC uses the DomainClassConverter class to convert the id path variable into the domain class’s id type and uses it for fetching the matching domain object from the repository layer. No further lookup is necessary.

For instance, a GET HTTP request to the http://localhost:8080/users/1 endpoint will return the following result:

{
  "id":1,
  "name":"John"
}

Hence, we can create an integration test and check the behavior of the findUserById() method:

@Test
public void whenGetRequestToUsersEndPointWithIdPathVariable_thenCorrectResponse() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/users/{id}", "1")
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
    }
}

Alternatively, we can use a REST API test tool, such as Postman, to test the method.

The nice thing about DomainClassConverter is that we don’t need to explicitly call the repository implementation in the controller method.

By simply specifying the id path variable, along with a resolvable domain class instance, we’ve automatically triggered the domain object’s lookup.

5. The PageableHandlerMethodArgumentResolver Class

Spring MVC supports the use of Pageable types in controllers and repositories.

Simply put, a Pageable instance is an object that holds paging information. Therefore, when we pass a Pageable argument to a controller method, Spring MVC uses the PageableHandlerMethodArgumentResolver class to resolve the Pageable instance into a PageRequest object, which is a simple Pageable implementation.

5.1. Using Pageable as a Controller Method Parameter

To understand how the PageableHandlerMethodArgumentResolver class works, let’s add a new method to the UserController class:

@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

In contrast to the findUserById() method, here we need to call the repository implementation to fetch all the User JPA entities persisted in the database.

Since the method takes a Pageable instance, it returns a subset of the entire set of entities, stored in a Page object.

A Page object is a sublist of a list of objects that exposes several methods we can use for retrieving information about the paged results, including the total number of result pages, and the number of the page that we’re retrieving.

By default, Spring MVC uses the PageableHandlerMethodArgumentResolver class to construct a PageRequest object, with the following request parameters:

  • page: the index of page that we want to retrieve – the parameter is zero-indexed and its default value is 0
  • size: the number of pages that we want to retrieve – the default value is 20
  • sort: one or more properties that we can use for sorting the results, using the following format: property1,property2(,asc|desc) – for instance, ?sort=name&sort=email,asc

For example, a GET request to the http://localhost:8080/users endpoint will return the following output:

{
  "content":[
    {
      "id":1,
      "name":"John"
    },
    {
      "id":2,
      "name":"Robert"
    },
    {
      "id":3,
      "name":"Nataly"
    },
    {
      "id":4,
      "name":"Helen"
    },
    {
      "id":5,
      "name":"Mary"
    }],
  "pageable":{
    "sort":{
      "sorted":false,
      "unsorted":true,
      "empty":true
    },
    "pageSize":5,
    "pageNumber":0,
    "offset":0,
    "unpaged":false,
    "paged":true
  },
  "last":true,
  "totalElements":5,
  "totalPages":1,
  "numberOfElements":5,
  "first":true,
  "size":5,
  "number":0,
  "sort":{
    "sorted":false,
    "unsorted":true,
    "empty":true
  },
  "empty":false
}

As we can see, the response includes the first, pageSize, totalElements, and totalPages JSON elements. This is really useful since a front-end can use these elements for easily creating a paging mechanism.

In addition, we can use an integration test to check the findAllUsers() method:

@Test
public void whenGetRequestToUsersEndPoint_thenCorrectResponse() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/users")
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$['pageable']['paged']").value("true"));
}

5.2. Customizing the Paging Parameters

In many cases, we’ll want to customize the paging parameters. The simplest way to accomplish this is by using the @PageableDefault annotation:

@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
    return userRepository.findAll(pageable);
}

Alternatively, we can use PageRequest‘s of() static factory method to create a custom PageRequest object and pass it to the repository method:

@GetMapping("/users")
public Page<User> findAllUsers() {
    Pageable pageable = PageRequest.of(0, 5);
    return userRepository.findAll(pageable);
}

The first parameter is the zero-based page index, while the second one is the size of the page that we want to retrieve.

In the example above, we created a PageRequest object of User entities, starting with the first page (0), with the page having 5 entries.

Additionally, we can build a PageRequest object using the page and size request parameters:

@GetMapping("/users")
public Page<User> findAllUsers(@RequestParam("page") int page, 
  @RequestParam("size") int size, Pageable pageable) {
    return userRepository.findAll(pageable);
}

Using this implementation, a GET request to the http://localhost:8080/users?page=0&size=2 endpoint will return the first page of User objects, and the size of the result page will be 2:

{
  "content": [
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 2,
      "name": "Robert"
    }
  ],
   
  // continues with pageable metadata
  
}

6. The SortHandlerMethodArgumentResolver Class

Paging is the de-facto approach for efficiently managing large numbers of database records. But, on its own, it’s pretty useless if we can’t sort the records in some specific way.

To this end, Spring MVC provides the SortHandlerMethodArgumentResolver class. The resolver automatically creates Sort instances from request parameters or from @SortDefault annotations.

6.1. Using the sort Controller Method Parameter

To get a clear idea of how the SortHandlerMethodArgumentResolver class works, let’s add the findAllUsersSortedByName() method to the controller class:

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
    return userRepository.findAll(pageable);
}

In this case, the SortHandlerMethodArgumentResolver class will create a Sort object by using the sort request parameter.

As a result, a GET request to the http://localhost:8080/sortedusers?sort=name endpoint will return a JSON array, with the list of User objects sorted by the name property:

{
  "content": [
    {
      "id": 4,
      "name": "Helen"
    },
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 5,
      "name": "Mary"
    },
    {
      "id": 3,
      "name": "Nataly"
    },
    {
      "id": 2,
      "name": "Robert"
    }
  ],
  
  // continues with pageable metadata
  
}

6.2. Using the Sort.by() Static Factory Method

Alternatively, we can create a Sort object by using the Sort.by() static factory method, which takes a non-null, non-empty array of String properties to be sorted.

In this case, we’ll sort the records only by the name property:

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
    Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
    return userRepository.findAll(pageable);
}

Of course, we could use multiple properties, as long as they’re declared in the domain class.

6.3. Using the @SortDefault Annotation

Likewise, we can use the @SortDefault annotation and get the same results:

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@SortDefault(sort = "name", 
  direction = Sort.Direction.ASC) Pageable pageable) {
    return userRepository.findAll(pageable);
}

Finally, let’s create an integration test to check the method’s behavior:

@Test
public void whenGetRequestToSorteredUsersEndPoint_thenCorrectResponse() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/sortedusers")
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$['sort']['sorted']").value("true"));
}

7. Querydsl Web Support

As we mentioned in the introduction, Spring Data web support allows us to use request parameters in controller methods to build Querydsl‘s Predicate types and to construct Querydsl queries.

To keep things simple, we’ll just see how Spring MVC converts a request parameter into a Querydsl BooleanExpression, which in turn is passed to a QuerydslPredicateExecutor.

To accomplish this, first we need to add the querydsl-apt and querydsl-jpa Maven dependencies to the pom.xml file:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

Next, we need to refactor our UserRepository interface, which must also extend the QuerydslPredicateExecutor interface:

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
  QuerydslPredicateExecutor<User> {
}

Finally, let’s add the following method to the UserController class:

@GetMapping("/filteredusers")
public Iterable<User> getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class) 
  Predicate predicate) {
    return userRepository.findAll(predicate);
}

Although the method implementation looks fairly simple, it actually exposes a lot of functionality beneath the surface.

Let’s say that we want to fetch from the database all the User entities that match a given name. We can achieve this by just calling the method and specifying a name request parameter in the URL:

http://localhost:8080/filteredusers?name=John

As expected, the request will return the following result:

[
  {
    "id": 1,
    "name": "John"
  }
]

As we did before, we can use an integration test to check the getUsersByQuerydslPredicate() method:

@Test
public void whenGetRequestToFilteredUsersEndPoint_thenCorrectResponse() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/filteredusers")
      .param("name", "John")
      .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("John"));
}

This is just a basic example of how Querydsl web support works. But it actually doesn’t reveal all of its power.

Now, let’s say that we want to fetch a User entity that matches a given id. In such a case, we just need to pass an id request parameter in the URL:

http://localhost:8080/filteredusers?id=2

In this case, we’ll get this result:

[
  {
    "id": 2,
    "name": "Robert"
  }
]

It’s clear to see that Querydsl web support is a very powerful feature that we can use to fetch database records matching a given condition.

In all the cases, the whole process boils down to just calling a single controller method with different request parameters.

8. Conclusion

In this tutorial, we took an in-depth look at Spring web support’s key components and learned how to use it within a demo Spring Boot project.

As usual, all the examples shown in this tutorial are available over on GitHub.