Lenguaje de consulta REST con especificaciones de Spring Data JPA

Este artículo es parte de una serie: • Lenguaje de consulta REST con criterios Spring y JPA

• Lenguaje de consulta REST con especificaciones de Spring Data JPA (artículo actual) • Lenguaje de consulta REST con Spring Data JPA y Querydsl

• Lenguaje de consulta REST: operaciones de búsqueda avanzadas

• Lenguaje de consulta REST: implementación de operaciones OR

• Lenguaje de consulta REST con RSQL

• Lenguaje de consulta REST con soporte web Querydsl

1. Información general

En este tutorial, crearemos una API REST de búsqueda / filtro utilizando Spring Data JPA y especificaciones.

Comenzamos a buscar un lenguaje de consulta en el primer artículo de esta serie, con una solución basada en criterios JPA.

Entonces, ¿por qué un lenguaje de consulta? Porque, para cualquier API lo suficientemente compleja, buscar / filtrar sus recursos por campos muy simples simplemente no es suficiente. Un lenguaje de consulta es más flexible y le permite filtrar exactamente los recursos que necesita.

2. Entidad de usuario

Primero, comencemos con una entidad de usuario simple para nuestra API de búsqueda:

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; // standard getters and setters }

3. Filtrar utilizando la especificación

Ahora, vayamos directamente a la parte más interesante del problema: consultar con las especificaciones personalizadas de Spring Data JPA .

Crearemos una UserSpecification que implementa la interfaz de Specification y pasaremos nuestra propia restricción para construir la consulta real :

public class UserSpecification implements Specification { private SearchCriteria criteria; @Override public Predicate toPredicate (Root root, CriteriaQuery query, CriteriaBuilder builder) { if (criteria.getOperation().equalsIgnoreCase(">")) { return builder.greaterThanOrEqualTo( root. get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase("<")) { return builder.lessThanOrEqualTo( root. get(criteria.getKey()), criteria.getValue().toString()); } else if (criteria.getOperation().equalsIgnoreCase(":")) { if (root.get(criteria.getKey()).getJavaType() == String.class) { return builder.like( root.get(criteria.getKey()), "%" + criteria.getValue() + "%"); } else { return builder.equal(root.get(criteria.getKey()), criteria.getValue()); } } return null; } }

Como podemos ver, creamos una Especificación basada en algunas restricciones simples que representamos en la siguiente clase " SearchCriteria ":

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

La implementación de SearchCriteria contiene una representación básica de una restricción, y está basada en esta restricción que vamos a construir la consulta:

  • clave : el nombre del campo - por ejemplo, primerNombre , edad , etc ...
  • operación : la operación - por ejemplo, igualdad, menor que,… etc.
  • valor : el valor del campo, por ejemplo, juan, 25, ... etc.

Por supuesto, la implementación es simplista y se puede mejorar; Sin embargo, es una base sólida para las operaciones poderosas y flexibles que necesitamos.

4. El repositorio de usuarios

A continuación, echemos un vistazo al UserRepository ; simplemente estamos ampliando el JpaSpecificationExecutor para obtener las nuevas API de especificación:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Pruebe las consultas de búsqueda

Ahora, probemos la nueva API de búsqueda.

Primero, creemos algunos usuarios para tenerlos listos cuando se ejecuten las pruebas:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceJPAConfig.class }) @Transactional @TransactionConfiguration public class JPASpecificationsTest { @Autowired private UserRepository repository; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("[email protected]"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); repository.save(userTom); } }

A continuación, veamos cómo encontrar usuarios con apellido dado :

@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }

Ahora, veamos cómo encontrar un usuario con nombre y apellido :

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "john")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

Nota: Usamos " dónde " y " y " para combinar Especificaciones .

A continuación, veamos cómo encontrar un usuario con tanto apellido como edad mínima :

@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("age", ">", "25")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "doe")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

Ahora, veamos cómo buscar un usuario que en realidad no existe :

@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { UserSpecification spec1 = new UserSpecification(new SearchCriteria("firstName", ":", "Adam")); UserSpecification spec2 = new UserSpecification(new SearchCriteria("lastName", ":", "Fox")); List results = repository.findAll(Specification.where(spec1).and(spec2)); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }

Por último, veamos cómo encontrar un usuario con solo una parte del nombre :

@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { UserSpecification spec = new UserSpecification(new SearchCriteria("firstName", ":", "jo")); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. Combinar especificaciones

A continuación, echemos un vistazo a la combinación de nuestras especificaciones personalizadas para utilizar múltiples restricciones y filtrar según múltiples criterios.

Vamos a implementar un constructor, UserSpecificationsBuilder , para combinar especificaciones de manera fácil y fluida :

public class UserSpecificationsBuilder { private final List params; public UserSpecificationsBuilder() { params = new ArrayList(); } public UserSpecificationsBuilder with(String key, String operation, Object value) { params.add(new SearchCriteria(key, operation, value)); return this; } public Specification build() { if (params.size() == 0) { return null; } List specs = params.stream() .map(UserSpecification::new) .collect(Collectors.toList()); Specification result = specs.get(0); for (int i = 1; i < params.size(); i++) { result = params.get(i) .isOrPredicate() ? Specification.where(result) .or(specs.get(i)) : Specification.where(result) .and(specs.get(i)); } return result; } }

7. UserController

Finalmente, usemos esta nueva funcionalidad de búsqueda / filtro de persistencia y configuremos la API REST , creando un UserController con una simple operación de búsqueda :

@Controller public class UserController { @Autowired private UserRepository repo; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List search(@RequestParam(value = "search") String search) { UserSpecificationsBuilder builder = new UserSpecificationsBuilder(); 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)); } Specification spec = builder.build(); return repo.findAll(spec); } }

Tenga en cuenta que para admitir otros sistemas que no estén en inglés, el objeto Pattern podría cambiarse como:

Pattern pattern = Pattern.compile("(\\w+?)(:|)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);

Aquí hay un ejemplo de URL de prueba para probar la API:

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

Y la respuesta:

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

Dado que las búsquedas están divididas por un "," en nuestro ejemplo de patrón , los términos de búsqueda no pueden contener este carácter. El patrón tampoco coincide con los espacios en blanco.

If we want to search for values containing commas, then we can consider using a different separator such as “;”.

Another option would be to change the pattern to search for values between quotes, then strip these from the search term:

Pattern pattern = Pattern.compile("(\\w+?)(:|)(\"([^\"]+)\")");

8. Conclusion

This tutorial covered a simple implementation that can be the base of a powerful REST query language. We've made good use of Spring Data Specifications to make sure we keep the API away from the domain and have the option to handle many other types of operations.

The full implementation of this article can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Siguiente » Lenguaje de consulta REST con Spring Data JPA y Querydsl « Lenguaje de consulta REST anterior con criterios Spring y JPA