REST-Abfragesprache mit Spring Data JPA-Spezifikationen

Dieser Artikel ist Teil einer Reihe: • REST-Abfragesprache mit Spring- und JPA-Kriterien

• REST-Abfragesprache mit Spring Data JPA-Spezifikationen (aktueller Artikel) • REST-Abfragesprache mit Spring Data JPA und Querydsl

• REST-Abfragesprache - Erweiterte Suchvorgänge

• REST-Abfragesprache - Implementierung einer ODER-Operation

• REST-Abfragesprache mit RSQL

• REST-Abfragesprache mit Querydsl-Webunterstützung

1. Übersicht

In diesem Tutorial erstellen wir eine Such- / Filter-REST-API unter Verwendung von Spring Data JPA und Spezifikationen.

Im ersten Artikel dieser Reihe haben wir uns mit einer Abfragesprache befasst - mit einer auf JPA-Kriterien basierenden Lösung.

Also - warum eine Abfragesprache? Weil - für jede ausreichend komplexe API - das Suchen / Filtern Ihrer Ressourcen nach sehr einfachen Feldern einfach nicht ausreicht. Eine Abfragesprache ist flexibler und ermöglicht es Ihnen, genau auf die Ressourcen zu filtern, die Sie benötigen.

2. Benutzer Entity

Beginnen wir zunächst mit einer einfachen Benutzerentität für unsere Such-API:

@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. Filtern nach Spezifikation

Kommen wir nun zum interessantesten Teil des Problems - der Abfrage mit benutzerdefinierten Spring Data JPA- Spezifikationen .

Wir erstellen eine Benutzerspezifikation, die die Spezifikationsschnittstelle implementiert, und übergeben unsere eigene Einschränkung, um die eigentliche Abfrage zu erstellen :

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

Wie wir sehen können , erstellen wir eine Spezifikation basierend auf einigen einfachen Einschränkungen, die wir in der folgenden Klasse " SearchCriteria " darstellen:

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

Die SearchCriteria- Implementierung enthält eine grundlegende Darstellung einer Einschränkung - und basierend auf dieser Einschränkung werden wir die Abfrage erstellen:

  • Schlüssel : der Feldname - zum Beispiel Vorname , Alter usw.
  • Operation : die Operation - zum Beispiel Gleichheit, weniger als, ... usw.
  • value : der Feldwert - zum Beispiel John, 25,… usw.

Natürlich ist die Implementierung einfach und kann verbessert werden. Es ist jedoch eine solide Basis für die leistungsstarken und flexiblen Operationen, die wir benötigen.

4. Das UserRepository

Weiter - werfen wir einen Blick auf das UserRepository . Wir erweitern einfach den JpaSpecificationExecutor , um die neuen Spezifikations-APIs zu erhalten:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Testen Sie die Suchanfragen

Testen Sie jetzt die neue Such-API.

Lassen Sie uns zunächst einige Benutzer erstellen, um sie beim Ausführen der Tests bereit zu halten:

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

Als nächstes sehen wir uns an, wie Sie Benutzer mit dem angegebenen Nachnamen finden :

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

Lassen Sie uns nun sehen, wie Sie einen Benutzer mit Vor- und Nachnamen finden :

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

Hinweis: Wir haben " wo " und " und " verwendet, um Spezifikationen zu kombinieren .

Als nächstes sehen wir uns an, wie Sie einen Benutzer mit dem angegebenen Nachnamen und dem Mindestalter finden :

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

Lassen Sie uns nun sehen, wie Sie nach Benutzern suchen , die tatsächlich nicht vorhanden sind :

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

Zum Schluss sehen wir uns an, wie Sie einen Benutzer finden, dem nur ein Teil des Vornamens zugewiesen wurde :

@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. Kombinieren Sie die Spezifikationen

Weiter - Schauen wir uns an, wie Sie unsere benutzerdefinierten Spezifikationen kombinieren , um mehrere Einschränkungen zu verwenden und nach mehreren Kriterien zu filtern.

Wir werden einen Builder implementieren - UserSpecificationsBuilder - um Spezifikationen einfach und fließend zu kombinieren :

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

Lassen Sie uns abschließend diese neue Persistenzsuch- / Filterfunktion verwenden und die REST-API einrichten, indem Sie einen UserController mit einer einfachen Suchoperation erstellen :

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

Beachten Sie, dass das Pattern- Objekt zur Unterstützung anderer nicht englischer Systeme wie folgt geändert werden kann:

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

Hier ist ein Beispiel für eine Test-URL zum Testen der API:

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

Und die Antwort:

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

Da die Suchvorgänge in unserem Musterbeispiel durch ein "," getrennt sind , können die Suchbegriffe dieses Zeichen nicht enthalten. Das Muster stimmt auch nicht mit Leerzeichen überein.

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.

Weiter » REST-Abfragesprache mit Spring Data JPA und Querydsl « Vorherige REST-Abfragesprache mit Spring- und JPA-Kriterien