Язык запросов REST со спецификациями Spring Data JPA

Язык запросов REST со спецификациями Spring Data JPA

1. обзор

В этом руководстве мы создадимSearch/Filter REST API, используя Spring Data JPA и спецификации.

Мы начали изучать язык запросов вfirst article изthis series - с решения на основе критериев JPA.

Итак -why a query language? Потому что - для любого достаточно сложного API - поиска / фильтрации ваших ресурсов по очень простым полям просто недостаточно. A query language is more flexible и позволяет фильтровать именно те ресурсы, которые вам нужны.

2. User Сущность

Во-первых, давайте начнем с простого объектаUser для нашего 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. Фильтр с использованиемSpecification

А теперь давайте перейдем к самой интересной части проблемы - запросам с пользовательскими данными Spring Data JPASpecifications.

Мы создадимUserSpecification, который реализует интерфейсSpecification, и перейдем кpass in our own constraint to construct the actual query:

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

Как мы видим -we create a Specification based on some simple constrains, который мы представляем в следующем классе «SearchCriteria»:

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

РеализацияSearchCriteria содержит базовое представление ограничения - и на его основе мы будем строить запрос:

  • key: имя поля - например,firstName,age и т. д.

  • operation: операция - например, равенство, меньше,… и т. д.

  • value: значение поля - например, 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);
    }
}

Затем давайте посмотрим, как найти пользователей сgiven last name:

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

Теперь давайте посмотрим, как найти пользователя с заданнымboth first and last name:

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

Примечание: мы использовали «where» и «and» дляcombine Specifications.

Затем давайте посмотрим, как найти пользователя с заданнымboth last name and minimum age:

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

Теперь давайте посмотрим, как искатьUser, чтоdoesn’t actually exist:

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

Наконец, давайте посмотрим, как найтиUsergiven only part of the first name:

@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. ОбъединитьSpecifications

Далее - давайте посмотрим на объединение наших пользовательскихSpecifications для использования нескольких ограничений и фильтрации по нескольким критериям.

Мы собираемся реализовать конструктор -UserSpecificationsBuilder - чтобы легко и плавно комбинироватьSpecifications:

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с

Наконец, давайте воспользуемся этой новой функцией поиска / фильтрации с сохранением состояния иset up the REST API - создавUserController с помощью простой операцииsearch:

@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:

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

И ответ:

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

Since the searches are split by a “,” in our Pattern example, the search terms can’t contain this character. Шаблон также не соответствует пробелам.

Если мы хотим искать значения, содержащие запятые, то мы можем рассмотреть возможность использования другого разделителя, такого как «;».

Другой вариант - изменить шаблон для поиска значений между кавычками, а затем убрать их из поискового запроса:

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

8. Заключение

В этом руководстве описана простая реализация, которая может стать основой мощного языка запросов REST. Мы хорошо использовали спецификации данных Spring, чтобы не допустить, чтобы API был связан с доменом иhave the option to handle many other types of operations.

full implementation в этой статье можно найти вthe GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.