Язык запросов REST с использованием Spring Data JPA и Querydsl

Язык запросов REST с использованием Spring Data JPA и Querydsl

1. обзор

В этом руководстве мы рассмотрим создание языка запросов дляREST API using Spring Data JPA and Querydsl.

В первых двух статьяхthis series мы создали те же функции поиска / фильтрации, используя критерии JPA и спецификации Spring Data JPA.

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

2. Конфигурация Querydsl

Во-первых, давайте посмотрим, как настроить наш проект для использования Querydsl.

Нам нужно добавить вpom.xml следующие зависимости:


    com.querydsl
    querydsl-apt
    4.1.4
    

    com.querydsl
    querydsl-jpa
    4.1.4

Нам также необходимо настроить плагин APT - инструмент обработки аннотаций следующим образом:


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

3. СущностьMyUser

Далее - давайте взглянем на объект «MyUser», который мы собираемся использовать в нашем Search API:

@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. ПользовательскийPredicate сPathBuilder

Теперь - давайте создадим собственныйPredicate на основе некоторых произвольных ограничений.

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

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

Обратите внимание на реализацию предикатаgenerically dealing with multiple types of operations. Это связано с тем, что язык запросов по определению является открытым языком, где вы можете фильтровать по любому полю, используя любую поддерживаемую операцию.

Чтобы представить такие критерии открытой фильтрации, мы используем простую, но довольно гибкую реализацию -SearchCriteria:

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

SearchCriteria содержит детали, необходимые для представления ограничения:

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

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

  • value: значение поля - например: john, 25,… и т. д.

5. MyUserRepositoryс

А теперь давайте посмотрим на нашMyUserRepository.

Нам нужно, чтобы нашMyUserRepository расширилQuerydslPredicateExecutor, чтобы мы могли использоватьPredicates позже для фильтрации результатов поиска:

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

6. ОбъединитьPredicates

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

В следующем примере - мы работаем с построителем -MyUserPredicatesBuilder - для объединения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. Протестируйте поисковые запросы

Далее - давайте проверим наш API поиска.

Мы начнем с инициализации базы данных с несколькими пользователями, чтобы они были готовы и доступны для тестирования:

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

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

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

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

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

Затем давайте посмотрим, как найти пользователя с заданным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)));
}

Теперь давайте посмотрим, как искатьMyUser, что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());
}

Наконец, давайте посмотрим, как найтиMyUsergiven only part of the first name - как в следующем примере:

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

Наконец, давайте соберем все вместе и создадим REST API.

Мы определяемUserController, который определяет простой методfindAll() с параметром «search» для передачи в строке запроса:

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

Вот пример быстрого теста URL:

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

И ответ:

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

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

Эта третья статья посвященаthe first steps of building a query language for a REST API, в которой эффективно используется библиотека Querydsl.

Реализация, конечно, на раннем этапе, но ее можно легко развить для поддержки дополнительных операций.

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