«1. Обзор

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

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

Итак, почему язык запросов? Потому что — для любого достаточно сложного API — поиска/фильтрации ваших ресурсов по очень простым полям просто недостаточно. Язык запросов является более гибким и позволяет вам отфильтровывать именно те ресурсы, которые вам нужны.

2. Настройка Querydsl

Сначала давайте посмотрим, как настроить наш проект для использования Querydsl.

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

<dependency> 
    <groupId>com.querydsl</groupId> 
    <artifactId>querydsl-apt</artifactId> 
    <version>4.2.2</version>
    </dependency>
<dependency> 
    <groupId>com.querydsl</groupId> 
    <artifactId>querydsl-jpa</artifactId> 
    <version>4.2.2</version> 
</dependency>

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

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

Это сгенерирует Q-типы для наших сущностей.

3. Сущность MyUser

Далее – давайте взглянем на сущность MyUser, которую мы собираемся использовать в нашем 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. Пользовательский предикат с PathBuilder ~ ~~ Теперь давайте создадим собственный предикат на основе некоторых произвольных ограничений.

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

Обратите внимание, как реализация предиката в общем случае работает с несколькими типами операций . Это связано с тем, что язык запросов по определению является открытым языком, где вы потенциально можете фильтровать по любому полю, используя любую поддерживаемую операцию.

public class MyUserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder<MyUser> entityPath = new PathBuilder<>(MyUser.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath<Integer> 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;
    }
}

Для представления таких открытых критериев фильтрации мы используем простую, но довольно гибкую реализацию – SearchCriteria:

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

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

key: имя поля — например: firstName, age, … и т. д. операция: операция — например: равенство, меньше, … и т. д. значение: значение поля — например: john, 25 , … и т.д.

    5. MyUserRepository

Теперь давайте взглянем на наш MyUserRepository.

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

Обратите внимание, что здесь мы используем сгенерированный Q-тип для объекта MyUser, который будет называться QMyUser .

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

6. Объединение предикатов

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

В следующем примере — мы работаем с компоновщиком — MyUserPredicatesBuilder — для объединения предикатов:

7. Протестируйте поисковые запросы

public class MyUserPredicatesBuilder {
    private List<SearchCriteria> 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;
    }
}

Далее — давайте проверим наш поисковый 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);
    }
}

Теперь давайте посмотрим, как найти пользователя с указанными именем и фамилией:

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

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

Далее давайте посмотрим, как найти пользователя с указанными и фамилией, и с минимальным возрастом

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

    Iterable<MyUser> results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

Теперь давайте посмотрим, как искать пользователя MyUser, которого на самом деле не существует:


@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable<MyUser> results = repo.findAll(builder.build());

    assertThat(results, contains(userTom));
    assertThat(results, not(contains(userJohn)));
}

Наконец, давайте посмотрим, как найти пользователя MyUser, учитывая только часть первое имя — как в следующем примере:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable<MyUser> results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

8. UserController

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable<MyUser> results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

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

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

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

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable<MyUser> 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);
    }
}

~ ~~ И ответ:

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

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

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

В этой третьей статье были рассмотрены первые шаги создания языка запросов для REST API с использованием библиотеки Querydsl.

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

Полную реализацию этой статьи можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.


«

Next »

REST Query Language – Advanced Search Operations

« Previous

REST Query Language with Spring Data JPA Specifications