REST език на заявката със спецификации Spring Data JPA

Тази статия е част от поредица: • Език на заявките REST с критерии Spring и JPA

• REST език за заявки със спецификации Spring Data JPA (текуща статия) • REST език за заявки с Spring Data JPA и Querydsl

• REST Query Language - Операции за разширено търсене

• REST език за заявки - Прилагане ИЛИ Операция

• REST език за заявки с RSQL

• REST език за заявки с поддръжка на Querydsl Web

1. Общ преглед

В този урок - ще изградим API за търсене / филтриране на REST, използвайки Spring Data JPA и спецификации.

Започнахме да разглеждаме езика на заявките в първата статия от тази поредица - с решение, основано на критерии JPA.

И така - защо език за заявки? Защото - за всеки достатъчно сложен API - търсенето / филтрирането на вашите ресурси по много прости полета просто не е достатъчно. Езикът за заявки е по-гъвкав и ви позволява да филтрирате до точно необходимите ресурси.

2. Потребителски обект

Първо - нека започнем с прост потребителски обект за нашия 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. Филтрирайте, като използвате спецификация

Сега - нека да влезем направо в най-интересната част от проблема - заявка с персонализирани спецификации Spring Data JPA .

Ще създадем UserSpecification, която реализира интерфейса за спецификация и ще преминем в нашето собствено ограничение, за да изградим действителната заявка :

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

Както виждаме - ние създаваме Спецификация въз основа на някои прости ограничения, които представяме в следния клас „ Критерии за търсене “:

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

В SearchCriteria изпълнението поддържа основно представяне на ограничение - и това е на базата на това ограничение, че ние ще се изграждането на запитването:

  • ключ : името на полето - например firstName , възраст , ... и т.н.
  • операция : операцията - например равенство, по-малко от, ... и т.н.
  • стойност : стойността на полето - например john, 25, ... и т.н.

Разбира се, изпълнението е опростено и може да бъде подобрено; това обаче е солидна основа за мощните и гъвкави операции, от които се нуждаем.

4. UserRepository

След това - нека да разгледаме UserRepository ; ние просто разширяваме JpaSpecificationExecutor, за да получим новите API за спецификация:

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

5. Тествайте заявките за търсене

Сега - нека тестваме новия API за търсене.

Първо, нека създадем няколко потребители, които да ги подготвят, когато тестовете стартират:

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

След това нека видим как да намерим потребители с дадено фамилно име :

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

Сега да видим как да намерим потребител с дадено име и фамилия :

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

Забележка: Използвахме „ къде “ и „ и “, за да комбинираме Спецификации .

След това нека видим как да намерим потребител с дадена фамилия и минимална възраст :

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

Сега да видим как да търсим Потребител , който всъщност не съществува :

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

И накрая - нека видим как да намерим Потребител, на когото е дадена само част от първото име :

@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. Комбинирайте спецификации

След това - нека да разгледаме комбинирането на нашите персонализирани спецификации, за да използваме множество ограничения и да филтрираме според множество критерии.

Ще внедрим конструктор - UserSpecificationsBuilder - за лесно и свободно комбиниране на спецификации :

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

И накрая - нека използваме тази нова функционалност за търсене / филтриране на постоянство и да настроим REST API - чрез създаване на UserController с проста операция за търсене :

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

Имайте предвид, че за да поддържа други системи, които не са на английски, обектът Pattern може да бъде променен по следния начин:

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

Ето пример за тестов URL за тестване на API:

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

И отговорът:

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

Тъй като търсенията са разделени на „,“ в нашия пример за модел , думите за търсене не могат да съдържат този знак. Моделът също не съвпада с празно пространство.

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.

Напред » REST език за заявки с Spring Data JPA и Querydsl « Предишен REST език за заявки с Spring и JPA критерии