REST Query Language con Spring Data JPA e Querydsl

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

• Linguaggio di query REST con specifiche JPA Spring Data

• REST Query Language con Spring Data JPA e Querydsl (articolo corrente) • 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, stiamo cercando di creare un linguaggio di query per un'API REST utilizzando Spring Data JPA e Querydsl .

Nei primi due articoli di questa serie, abbiamo creato la stessa funzionalità di ricerca / filtro utilizzando i criteri JPA e le specifiche JPA di Spring Data.

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. Configurazione Querydsl

Innanzitutto, vediamo come configurare il nostro progetto per utilizzare Querydsl.

Dobbiamo aggiungere le seguenti dipendenze a pom.xml :

 com.querydsl querydsl-apt 4.2.2   com.querydsl querydsl-jpa 4.2.2 

È inoltre necessario configurare il plug-in APT - Strumento di elaborazione delle annotazioni - come segue:

 com.mysema.maven apt-maven-plugin 1.1.3    process   target/generated-sources/java com.mysema.query.apt.jpa.JPAAnnotationProcessor    

Questo genererà i tipi Q per le nostre entità.

3. L' entità MyUser

Successivamente, diamo un'occhiata all'entità " MyUser " che utilizzeremo nella nostra API di ricerca:

@Entity public class MyUser { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }

4. Predicato personalizzato con PathBuilder

Ora, creiamo un predicato personalizzato basato su alcuni vincoli arbitrari.

Stiamo usando PathBuilder qui invece dei tipi Q generati automaticamente perché abbiamo bisogno di creare percorsi dinamicamente per un utilizzo più astratto:

public class MyUserPredicate { private SearchCriteria criteria; public BooleanExpression getPredicate() { PathBuilder entityPath = new PathBuilder(MyUser.class, "user"); if (isNumeric(criteria.getValue().toString())) { NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class); int value = Integer.parseInt(criteria.getValue().toString()); switch (criteria.getOperation()) { case ":": return path.eq(value); case ">": return path.goe(value); case "<": return path.loe(value); } } else { StringPath path = entityPath.getString(criteria.getKey()); if (criteria.getOperation().equalsIgnoreCase(":")) { return path.containsIgnoreCase(criteria.getValue().toString()); } } return null; } }

Si noti come l'implementazione del predicato si occupi genericamente di più tipi di operazioni . Questo perché il linguaggio di query è per definizione un linguaggio aperto in cui è possibile filtrare potenzialmente in base a qualsiasi campo, utilizzando qualsiasi operazione supportata.

Per rappresentare quel tipo di criteri di filtraggio aperti, stiamo usando un'implementazione semplice ma abbastanza flessibile - SearchCriteria :

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

Il criterio di ricerca contiene i dettagli di cui abbiamo bisogno per rappresentare un vincolo:

  • 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

5. MyUserRepository

Ora, diamo un'occhiata al nostro MyUserRepository .

Abbiamo bisogno del nostro MyUserRepository per estendere QuerydslPredicateExecutor in modo da poter utilizzare Predicates in seguito per filtrare i risultati della ricerca:

public interface MyUserRepository extends JpaRepository, QuerydslPredicateExecutor, QuerydslBinderCustomizer { @Override default public void customize( QuerydslBindings bindings, QMyUser root) { bindings.bind(String.class) .first((SingleValueBinding) StringExpression::containsIgnoreCase); bindings.excluding(root.email); } }

Si noti che stiamo usando qui il Q-tipo generato per il MyUser entità, che sarà chiamato QMyUser.

6. Combina predicati

Successivamente, diamo un'occhiata alla combinazione di predicati per utilizzare più vincoli nel filtraggio dei risultati.

Nell'esempio seguente, lavoriamo con un builder, MyUserPredicatesBuilder , per combinare Predicates :

public class MyUserPredicatesBuilder { private List params; public MyUserPredicatesBuilder() { params = new ArrayList(); } public MyUserPredicatesBuilder 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 predicates = params.stream().map(param -> { MyUserPredicate predicate = new MyUserPredicate(param); return predicate.getPredicate(); }).filter(Objects::nonNull).collect(Collectors.toList()); BooleanExpression result = Expressions.asBoolean(true).isTrue(); for (BooleanExpression predicate : predicates) { result = result.and(predicate); } return result; } }

7. Testare le query di ricerca

Avanti: testiamo la nostra API di ricerca.

Inizieremo inizializzando il database con alcuni utenti, per averli pronti e disponibili per il test:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @Rollback public class JPAQuerydslIntegrationTest { @Autowired private MyUserRepository repo; private MyUser userJohn; private MyUser userTom; @Before public void init() { userJohn = new MyUser(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("[email protected]"); userJohn.setAge(22); repo.save(userJohn); userTom = new MyUser(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); repo.save(userTom); } }

Successivamente, vediamo come trovare gli utenti con il cognome :

@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe"); Iterable results = repo.findAll(builder.build()); assertThat(results, containsInAnyOrder(userJohn, userTom)); }

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

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("firstName", ":", "John").with("lastName", ":", "Doe"); Iterable 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() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("lastName", ":", "Doe").with("age", ">", "25"); Iterable results = repo.findAll(builder.build()); assertThat(results, contains(userTom)); assertThat(results, not(contains(userJohn))); }

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

@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder() .with("firstName", ":", "Adam").with("lastName", ":", "Fox"); Iterable results = repo.findAll(builder.build()); assertThat(results, emptyIterable()); }

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

@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo"); Iterable 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 MyUserRepository myUserRepository; @RequestMapping(method = RequestMethod.GET, value = "/myusers") @ResponseBody public Iterable search(@RequestParam(value = "search") String search) { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder(); 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 myUserRepository.findAll(exp); } }

Here is a quick test URL example:

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

And the response:

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

9. Conclusion

Questo terzo articolo ha trattato i primi passaggi della creazione di un linguaggio di query per un'API REST , facendo un buon uso della libreria Querydsl.

L'implementazione è ovviamente all'inizio, ma può essere facilmente evoluta per supportare operazioni aggiuntive.

L' implementazione completa di questo articolo può essere trovata nel progetto GitHub: questo è un progetto basato su Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.

Successivo » Linguaggio di query REST - Operazioni di ricerca avanzate « Precedente Linguaggio di query REST con specifiche JPA Spring Data