Linguaggio di query REST con specifiche JPA di Spring Data

Questo articolo fa parte di una serie: • REST Query Language con Spring e JPA Criteria

• Linguaggio di query REST con specifiche JPA Spring Data (articolo corrente) • Linguaggio query REST con JPA Spring Data e Querydsl

• REST Query Language - Operazioni di ricerca avanzate

• REST Query Language - Implementazione dell'operazione OR

• REST Query Language con RSQL

• Linguaggio query REST con supporto Web Querydsl

1. Panoramica

In questo tutorial, creeremo un'API REST di ricerca / filtro utilizzando Spring Data JPA e specifiche.

Abbiamo iniziato a esaminare un linguaggio di query nel primo articolo di questa serie, con una soluzione basata sui criteri JPA.

Allora, perché un linguaggio di query? Perché, per qualsiasi API abbastanza complessa, cercare / filtrare le risorse tramite campi molto semplici semplicemente non è sufficiente. Un linguaggio di query è più flessibile e ti consente di filtrare esattamente le risorse di cui hai bisogno.

2. Entità utente

Innanzitutto, iniziamo con una semplice entità utente per la nostra API di ricerca:

@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. Filtro utilizzando le specifiche

Ora, entriamo direttamente nella parte più interessante del problema: eseguire query con le specifiche JPA Spring Data personalizzate .

Creeremo una UserSpecification che implementa l' interfaccia delle specifiche e passeremo il nostro vincolo per costruire la query effettiva :

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

Come possiamo vedere , creiamo una specifica basata su alcuni semplici vincoli che rappresentiamo nella seguente classe " SearchCriteria ":

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

L' implementazione di SearchCriteria contiene una rappresentazione di base di un vincolo e si basa su questo vincolo che costruiremo la query:

  • chiave : il nome del campo, ad esempio firstName , age , ... ecc.
  • operazione : l'operazione - ad esempio, uguaglianza, minore di, ... ecc.
  • valore : il valore del campo, ad esempio giovanni, 25, ... ecc.

Naturalmente, l'implementazione è semplicistica e può essere migliorata; è comunque una solida base per le operazioni potenti e flessibili di cui abbiamo bisogno.

4. Il repository utente

Avanti: diamo un'occhiata a UserRepository ; stiamo semplicemente estendendo JpaSpecificationExecutor per ottenere le nuove API di specifica:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Testare le query di ricerca

Ora, testiamo la nuova API di ricerca.

Innanzitutto, creiamo alcuni utenti per averli pronti quando vengono eseguiti i test:

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

Successivamente, vediamo come trovare gli utenti con il cognome :

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

Vediamo ora come trovare un utente con nome e cognome dati :

@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: abbiamo utilizzato " dove " e " e " per combinare le specifiche .

Successivamente, vediamo come trovare un utente con il cognome e l'età minima specificati :

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

Vediamo ora come cercare un utente che in realtà non esiste :

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

Infine, vediamo come trovare un utente a cui viene data solo una parte del nome :

@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. Combina specifiche

Successivamente, diamo un'occhiata alla combinazione delle nostre specifiche personalizzate per utilizzare più vincoli e filtrare in base a più criteri.

Stiamo per implementare un builder - UserSpecificationsBuilder - per combinare facilmente e fluentemente le specifiche :

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

Infine, utilizziamo questa nuova funzionalità di ricerca / filtro della persistenza e configuriamo l'API REST , creando un UserController con una semplice operazione di ricerca :

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

Nota che per supportare altri sistemi non inglesi, l' oggetto Pattern potrebbe essere modificato come:

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

Ecco un esempio di URL di prova per testare l'API:

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

E la risposta:

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

Poiché le ricerche sono divise da una "," nel nostro esempio Pattern , i termini di ricerca non possono contenere questo carattere. Il modello inoltre non corrisponde agli spazi bianchi.

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.

Successivo » Linguaggio di query REST con Spring Data JPA e Querydsl « Precedente Linguaggio di query REST con criteri Spring e JPA