REST Query Language with Spring Data JPA and Querydsl – 使用Spring Data JPA和Querydsl的REST查询语言

最后修改: 2015年 1月 31日

1. Overview

1.概述

In this tutorial, we’re looking at building a query language for a REST API using Spring Data JPA and Querydsl.

在本教程中,我们将研究如何使用Spring Data JPA和Querydsl,为REST API构建查询语言。

In the first two articles of this series, we built the same search/filtering functionality using JPA Criteria and Spring Data JPA Specifications.

本系列的前两篇文章中,我们使用JPA Criteria和Spring Data JPA Specifications构建了相同的搜索/过滤功能。

So – why a query language? Because – for any complex enough API – searching/filtering your resources by very simple fields is simply not enough. A query language is more flexible, and allows you to filter down to exactly the resources you need.

那么 – 为什么要使用查询语言?因为对于任何足够复杂的 API 来说,通过非常简单的字段搜索/过滤您的资源是远远不够的。查询语言更加灵活,并允许您精确地过滤到您所需要的资源。

2. Querydsl Configuration

2.Querydsl配置

First – let’s see how to configure our project to use Querydsl.

首先,让我们看看如何配置我们的项目来使用Querydsl。

We need to add the following dependencies to pom.xml:

我们需要在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>

We also need to configure the APT – Annotation processing tool – plugin as follows:

我们还需要配置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>

This will generate the Q-types for our entities. 

这将为我们的实体生成Q-types>。

3. The MyUser Entity

3、MyUserEntity

Next – let’s take a look at the “MyUser” entity which we are going to use in our Search API:

接下来–让我们看一下”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. Custom Predicate With PathBuilder

4.自定义谓词WPathBuilder

Now – let’s create a custom Predicate based on some arbitrary constraints.

现在–让我们根据一些任意的约束条件创建一个自定义的Predicate

We’re using PathBuilder here instead of the automatically generated Q-types because we need to create paths dynamically for more abstract usage:

我们在这里使用PathBuilder,而不是自动生成的Q-types,因为我们需要动态地创建路径,以实现更抽象的使用。

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

Note how the implementation of the predicate is generically dealing with multiple types of operations. This is because the query language is by definition an open language where you can potentially filter by any field, using any supported operation.

请注意,谓词的实现是如何通用地处理多种类型的操作的。这是因为根据定义,查询语言是一种开放的语言,你可以通过任何字段,使用任何支持的操作来过滤。

To represent that kind of open filtering criteria, we’re using a simple but quite flexible implementation – SearchCriteria:

为了表示这种开放的过滤标准,我们使用了一个简单但相当灵活的实现 – SearchCriteria

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

The SearchCriteria holds the details we need to represent a constraint:

SearchCriteria持有我们需要表示一个约束的细节。

  • key: the field name – for example: firstName, age, … etc
  • operation: the operation – for example: Equality, less than, … etc
  • value: the field value – for example: john, 25, … etc

5. MyUserRepository

5.MyUserRepository

Now – let’s take a look at our MyUserRepository.

现在–让我们看一下我们的MyUserRepository

We need our MyUserRepository to extend QuerydslPredicateExecutor so that we can use Predicates later to filter search results:

我们需要我们的MyUserRepository扩展QuerydslPredicateExecutor,这样我们就可以在以后使用Predicates来过滤搜索结果。

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

Note that we’re using here the generated Q-type for the MyUser entity, which will be named QMyUser.

注意,我们在这里使用的是为MyUser实体生成的Q类型,它将被命名为QMyUser.

6. Combine Predicates

6.结合谓词

Next– let’s take a look at combining Predicates to use multiple constraints in results filtering.

接下来,让我们看看如何组合谓词,在结果过滤中使用多个约束。

In the following example – we work with a builder – MyUserPredicatesBuilder – to combine Predicates:

在下面的例子中,我们使用一个构建器–MyUserPredicatesBuilder–来组合Predicates

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

7. Test the Search Queries

7.测试搜索查询

Next – let’s test our Search API.

接下来–让我们测试一下我们的搜索API。

We’ll start by initializing the database with a few users – to have these ready and available for testing:

我们先用几个用户来初始化数据库–让这些用户准备好并可用于测试。

@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("john@doe.com");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

Next, let’s see how to find users with given last name:

接下来,让我们看看如何用给定的姓氏找到用户。

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

Now, let’s see how to find a user with given both first and last name:

现在,让我们看看如何找到一个有给定名字和姓氏的用户

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

Next, let’s see how to find user with given both last name and minimum age

接下来,让我们看看如何找到具有给定姓氏和最小年龄的用户

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

Now, let’s see how to search for MyUser that doesn’t actually exist:

现在,让我们看看如何搜索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());
}

Finally – let’s see how to find a MyUser given only part of the first name – as in the following example:

最后–让我们看看如何找到一个MyUser只给出部分名字的–如下面的例子。

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

8. UserController

8.UserController

Finally, let’s put everything together and build the REST API.

最后,让我们把一切放在一起,建立REST API。

We’re defining a UserController that defines a simple method findAll() with a “search“ parameter to pass in the query string:

我们定义了一个UserController,它定义了一个简单的方法findAll(),有一个”search“参数来传递查询字符串。

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

Here is a quick test URL example:

下面是一个快速测试URL的例子。

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

And the response:

而回应是。

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

9. Conclusion

9.结论

This third article covered the first steps of building a query language for a REST API, making good use of the Querydsl library.

这第三篇文章涵盖了为REST API构建查询语言的第一步,很好地利用了Querydsl库。

The implementation is of course early on, but it can easily be evolved to support additional 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.

本文的完整实现可以在GitHub项目中找到 – 这是一个基于Maven的项目,所以应该很容易导入并按原样运行。

Next »

REST Query Language – Advanced Search Operations

« Previous

REST Query Language with Spring Data JPA Specifications