Suporte da Web para dados da Spring

Suporte da Web para dados da Spring

1. Visão geral

Spring MVCeSpring Data cada um faz um ótimo trabalho simplificando o desenvolvimento de aplicativos por conta própria. Mas, e se os juntarmos?

Neste tutorial, daremos uma olhada emSpring Data’s web support ehow its resolvers can reduce boilerplate and make our controllers more expressive.

Ao longo do caminho, daremos uma olhada emQuerydsl e como é sua integração com Spring Data.

2. Um pouco de fundo

Suporte da Web da Spring Datais a set of web-related features implemented on top of the standard Spring MVC platform, aimed at adding extra functionality to the controller layer.

A funcionalidade de suporte da Web do Spring Data é construída em torno de várias classesresolver. Os resolvedores simplificam a implementação de métodos de controlador que interoperam com os repositóriosSpring Data e também os enriquecem com recursos adicionais.

Esses recursos incluemfetching domain objects da camada do repositório,without having to explicitly call as implementações do repositório econstructing controller responses que podem ser enviados aos clientes como segmentos de dados que suportam paginação e classificação.

Além disso, as solicitações para métodos do controlador que usam um ou mais parâmetros de solicitação podem ser resolvidas internamente paraQuerydsl consultas.

3. Um projeto de demonstração de inicialização do Spring

Para entender como podemos usar o suporte da Web Spring Data para melhorar a funcionalidade de nossos controladores, vamos criar um projeto Spring Boot básico.

As dependências Maven do nosso projeto de demonstração são razoavelmente padrão, com algumas exceções que discutiremos mais tarde:


    org.springframework.boot
    spring-boot-starter-data-jpa


    org.springframework.boot
    spring-boot-starter-web


    com.h2database
    h2
    runtime


    org.springframework.boot
    spring-boot-starter-test
    test

Neste caso, incluímosspring-boot-starter-web, pois vamos usá-lo para criar um controlador RESTful,spring-boot-starter-jpa para implementar a camada de persistência espring-boot-starter-test para testar a API do controlador.

Como usaremosH2 como banco de dados subjacente, incluímoscom.h2database também.

Vamos ter em mente quespring-boot-starter-web enables Spring Data web support by default. Portanto, não precisamos criar nenhuma classe@Configuration adicional para fazê-lo funcionar em nosso aplicativo.

Por outro lado, para projetos não Spring Boot, precisaríamos definir uma classe@Configuration e anotá-la com as anotações@EnableWebMvce@EnableSpringDataWebSupport.

3.1. A classe de domínio

Agora, vamos adicionar uma classe de entidade JPAUser simples ao projeto, para que possamos ter um modelo de domínio funcional para brincar:

@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. A camada de repositório

Para manter o código simples, a funcionalidade de nosso aplicativo de demonstração Spring Boot será reduzida para apenas buscar algumas entidadesUser de um banco de dados H2 em memória.

O Spring Boot facilita a criação de implementações de repositório que fornecem funcionalidade CRUD mínima imediata. Portanto, vamos definir uma interface de repositório simples que funcione com as entidades JPAUser:

@Repository
public interface UserRepository extends PagingAndSortingRepository {}

Não há nada inerentemente complexo na definição da interfaceUserRepository, exceto que ela estendePagingAndSortingRepository.

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

3.3. A camada do controlador

Agora, precisamos implementar pelo menos um controlador RESTful básico que atue como a camada intermediária entre o cliente e a camada do repositório.

Portanto, vamos criar uma classe de controlador, que pega uma instânciaUserRepository em seu construtor e adiciona um único método para encontrar entidadesUser porid:

@RestController
public class UserController {

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

3.4.  Executando o aplicativo

Finalmente, vamos definir a classe principal do aplicativo e preencher o banco de dados H2 com algumas entidadesUser:

@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);
        };
    }
}

Agora, vamos executar o aplicativo. Como esperado, vemos a lista de entidadesUser persistentes impressa no console na inicialização:

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. A classeDomainClassConverter

Por enquanto, a classeUserController implementa apenas o métodofindUserById().

À primeira vista, a implementação do método parece bastante simples. Mas ele realmente encapsula muitas funcionalidades de suporte da Web do Spring Data nos bastidores.

Como o método leva uma instânciaUser como argumento, podemos acabar pensando que precisamos passar explicitamente o objeto de domínio na solicitação. Mas, nós não.

Spring MVC usa a classeDomainClassConverterto 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. Nenhuma pesquisa adicional é necessária.

Por exemplo, uma solicitação GET HTTP para o endpointhttp://localhost:8080/users/1 retornará o seguinte resultado:

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

Portanto, podemos criar um teste de integração e verificar o comportamento do métodofindUserById():

@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"));
    }
}

Como alternativa, podemos usar uma ferramenta de teste REST API, comoPostman, para testar o método.

O bom sobreDomainClassConverter é que não precisamos chamar explicitamente a implementação do repositório no método do controlador.

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

5. A classePageableHandlerMethodArgumentResolver

Spring MVC suporta o uso de tiposPageable em controladores e repositórios.

Simplificando, uma instânciaPageable é um objeto que contém informações de paginação. Portanto, quando passamos um argumentoPageable para um método do controlador, Spring MVC usa a classePageableHandlerMethodArgumentResolverto resolve the Pageable instance into a PageRequest object,, que é uma implementaçãoPageable simples.

5.1. UsandoPageable como um parâmetro de método do controlador

Para entender como a classePageableHandlerMethodArgumentResolver funciona, vamos adicionar um novo método à classeUserController:

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

Em contraste com o métodofindUserById(), aqui precisamos chamar a implementação do repositório para buscar todas as entidades JPAUser persistentes no banco de dados.

Como o método leva uma instânciaPageable, ele retorna um subconjunto de todo o conjunto de entidades, armazenado em um objetoPage<User>.

Um objetoPageis a sublist of a list of objects that exposes several methods we can use for retrieving information about the paged results, incluindo o número total de páginas de resultado e o número da página que estamos recuperando.

Por padrão, Spring MVC usa a classePageableHandlerMethodArgumentResolver para construir um objetoPageRequest, com os seguintes parâmetros de solicitação:

  • page: o índice da página que queremos recuperar - o parâmetro é indexado por zero e seu valor padrão é0

  • size: o número de páginas que queremos recuperar - o valor padrão é20

  • sort: uma ou mais propriedades que podemos usar para classificar os resultados, usando o seguinte formato:property1,property2(,asc|desc) – por exemplo,?sort=name&sort=email,asc

Por exemplo, uma solicitação GET para o endpointhttp://localhost:8080/userhttp://localhost:8080/users[s] retornará a seguinte saída:

{
  "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
}

Como podemos ver, a resposta inclui os elementos JSONfirst,pageSize,totalElements etotalPages. Isso é realmente útil, pois um front-end pode usar esses elementos para criar facilmente um mecanismo de paginação.

Além disso, podemos usar um teste de integração para verificar o métodofindAllUsers():

@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. Personalização dos parâmetros de paginação

Em muitos casos, queremos personalizar os parâmetros de paginação. A maneira mais simples de fazer isso é usando a anotação@PageableDefault:

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

Como alternativa, podemos usar o método de fábrica estáticoPageRequest'sof() para criar um objetoPageRequest personalizado e passá-lo para o método de repositório:

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

O primeiro parâmetro é o índice de página com base em zero, enquanto o segundo é o tamanho da página que queremos recuperar.

No exemplo acima, criamos um objetoPageRequest de entidadesUser, começando com a primeira página (0), com a página tendo5 entradas.

Além disso, podemos construir um objetoPageRequest usando os parâmetros de solicitaçãopageesize:

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

Usando esta implementação, uma solicitação GET para o endpointhttp://localhost:8080/users?page=0&size=2 retornará a primeira página de objetosUser, e o tamanho da página de resultado será 2:

{
  "content": [
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 2,
      "name": "Robert"
    }
  ],

  // continues with pageable metadata

}

6. A classeSortHandlerMethodArgumentResolver

A paginação é a abordagem de fato para gerenciar com eficiência um grande número de registros de banco de dados. Mas, por si só, é bastante inútil se não pudermos classificar os registros de uma maneira específica.

Para esse fim, Spring MVC fornece a classeSortHandlerMethodArgumentResolver. O resolvedorautomatically creates Sort instances from request parameters or from @SortDefault annotations.

6.1. Usando o parâmetro de método do controladorsort

Para ter uma ideia clara de como a classeSortHandlerMethodArgumentResolver funciona, vamos adicionar o métodofindAllUsersSortedByName() à classe do controlador:

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

Nesse caso, a classeSortHandlerMethodArgumentResolver criará um objetoSort usando o parâmetro de solicitaçãosort.

Como resultado, uma solicitação GET para o endpointhttp://localhost:8080/sortedusers?sort=name retornará uma matriz JSON, com a lista de objetosUser classificados pela propriedadename:

{
  "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. Usando o método de fábrica estáticaSort.by()

Como alternativa, podemos criar um objetoSort usando o método de fábrica estáticoSort.by(), que leva umarray não nulo e não vazio de propriedadesString para serem classificados.

Nesse caso, classificaremos os registros apenas pela propriedadename:

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

Claro, podemos usar várias propriedades, desde que sejam declaradas na classe de domínio.

6.3. Usando a anotação@SortDefault

Da mesma forma, podemos usar a anotação@SortDefault e obter os mesmos resultados:

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

Finalmente, vamos criar um teste de integração para verificar o comportamento do método:

@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. Suporte da Web Querydsl

Como mencionamos na introdução, o suporte da Web do Spring Data nos permite usar parâmetros de solicitação em métodos de controlador para construir tipos deQuerydslPredicate e construir consultas Querydsl

Para manter as coisas simples, veremos apenas como o Spring MVC converte um parâmetro de solicitação em um QuerydslBooleanExpression, que por sua vez é passado para umQuerydslPredicateExecutor.

Para fazer isso, primeiro precisamos adicionar as dependências Mavenquerydsl-apt equerydsl-jpa ao arquivopom.xml:


    com.querydsl
    querydsl-apt


    com.querydsl
    querydsl-jpa

Em seguida, precisamos refatorar nossa interfaceUserRepository, que também deve estender a interfaceQuerydslPredicateExecutor:

@Repository
public interface UserRepository extends PagingAndSortingRepository,
  QuerydslPredicateExecutor {
}

Finalmente, vamos adicionar o seguinte método à classeUserController:

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

Embora a implementação do método pareça bastante simples, ela realmente expõe muitas funcionalidades abaixo da superfície.

Digamos que queremos buscar no banco de dados todas as entidadesUser que correspondem a um determinado nome. Podemos alcançar esteby just calling the method and specifying a name request parameter in the URL:

Como esperado, a solicitação retornará o seguinte resultado:

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

Como fizemos antes, podemos usar um teste de integração para verificar o métodogetUsersByQuerydslPredicate():

@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"));
}

Este é apenas um exemplo básico de como o suporte à web Querydsl funciona. Mas na verdade não revela todo o seu poder. **

Agora, digamos que queremos buscar uma entidadeUser que corresponda a um determinadoid.. Nesse caso,we just need to pass an id request parameter in the URL:

Nesse caso, obteremos este resultado:

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

É claro que o suporte da web Querydsl é um recurso muito poderoso que podemos usar para buscar registros de banco de dados que correspondem a uma determinada condição.

Em todos os casos, todo o processo se reduz ajust calling a single controller method with different request parameters.

8. Conclusão

Neste 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.

Como de costume, todos os exemplos mostrados neste tutorial estão disponíveis emGitHub.