51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Spring Data JPA 和 Querydsl 构建 REST 查询语言

1、概览 {#1概览}

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

本文将带你了解如何使用 Spring Data JPA 和 Querydsl 构建 REST API 查询语言。

2、Querydsl 配置 {#2querydsl-配置}

首先,在 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 开头的查询类。

关于如何在 Spring Boot 中使用 QueryDSL,你可以参阅 这篇文章

3、MyUser 实体 {#3myuser-实体}

定义 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 自定义 Predicate {#4使用-pathbuilder-自定义-predicate}

根据一些任意约束条件创建一个自定义 Predicate

这里使用 PathBuilder 而不是自动生成的查询类,因为我们需要为更抽象的用法动态创建 Path

public class MyUserPredicate {
private SearchCriteria criteria;

public BooleanExpression getPredicate() {
    PathBuilder&lt;MyUser&gt; entityPath = new PathBuilder&lt;&gt;(MyUser.class, &quot;user&quot;);

    if (isNumeric(criteria.getValue().toString())) {
        NumberPath&lt;Integer&gt; path = entityPath.getNumber(criteria.getKey(), Integer.class);
        int value = Integer.parseInt(criteria.getValue().toString());
        switch (criteria.getOperation()) {
            case &quot;:&quot;:
                return path.eq(value);
            case &quot;&gt;&quot;:
                return path.goe(value);
            case &quot;&lt;&quot;:
                return path.loe(value);
        }
    } 
    else {
        StringPath path = entityPath.getString(criteria.getKey());
        if (criteria.getOperation().equalsIgnoreCase(&quot;:&quot;)) {
            return path.containsIgnoreCase(criteria.getValue().toString());
        }
    }
    return null;
}

}

注意 Predicate 的实现是如何通用地处理多种类型的操作的。这是因为查询语言顾名思义是一种开放式语言,你可以使用任何支持的操作,对任何字段进行过滤。

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

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

SearchCriteria 包含表示约束条件的详细信息:

  • key:字段名 - 例如:firstNameage
  • operation:操作,例如:等于、小于 ... 等
  • value:字段值,例如:john25

5、MyUserRepository {#5myuserrepository}

定义 MyUserRepository,继承 QuerydslPredicateExecutor,以使用 Predicate 过滤搜索结果:

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

注意,在这里使用的是 MyUser 实体对应查询类: QMyUser

6、组合 Predicate {#6组合-predicate}

接下来,看看如何组合 Predicate,以使用多个约束条件来过滤结果。

使用 MyUserPredicatesBuilder 来组合 Predicate

public class MyUserPredicatesBuilder {
    private List<SearchCriteria> params;
public MyUserPredicatesBuilder() {
    params = new ArrayList&lt;&gt;();
}

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 -&gt; {
        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、测试搜索过滤 {#7测试搜索过滤}

先初始化几个测试数据,用于测试:

@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(&quot;John&quot;);
    userJohn.setLastName(&quot;Doe&quot;);
    userJohn.setEmail(&quot;john@doe.com&quot;);
    userJohn.setAge(22);
    repo.save(userJohn);

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

}

根据 lastName 检索用户:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");
Iterable&lt;MyUser&gt; results = repo.findAll(builder.build());
assertThat(results, containsInAnyOrder(userJohn, userTom));

}

根据 firstNamelastName 检索用户:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");
Iterable&lt;MyUser&gt; results = repo.findAll(builder.build());

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

}

检索 lastName 等于 Doeage 大于 25 的用户。

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");
Iterable&lt;MyUser&gt; results = repo.findAll(builder.build());

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

}

检索不存在的记录:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");
Iterable&lt;MyUser&gt; results = repo.findAll(builder.build());
assertThat(results, emptyIterable());

}

根据 firstName 进行模糊检索:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");
Iterable&lt;MyUser&gt; results = repo.findAll(builder.build());

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

}

8、UserController {#8usercontroller}

最后,构建 REST API,整合所有内容。

定义 UserController,它有一个 findAll() 方法,该方法接收一个 search 查询参数:

@Controller
public class UserController {
@Autowired
private MyUserRepository myUserRepository;

@RequestMapping(method = RequestMethod.GET, value = &quot;/myusers&quot;)
@ResponseBody
public Iterable&lt;MyUser&gt; search(@RequestParam(value = &quot;search&quot;) String search) {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();

    if (search != null) {
        Pattern pattern = Pattern.compile(&quot;(\w+?)(:|&lt;|&gt;)(\w+?),&quot;);
        Matcher matcher = pattern.matcher(search + &quot;,&quot;);
        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":"tom@doe.com",
    "age":26
}]

9、总结 {#9总结}

本文介绍了如何使用 Spring Data JPA 和 Querydsl 构建 REST 查询语言,以及如何在 REST API 中应用。


Ref:https://www.baeldung.com/rest-api-search-language-spring-data-querydsl

赞(4)
未经允许不得转载:工具盒子 » 使用 Spring Data JPA 和 Querydsl 构建 REST 查询语言