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<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; }
}
注意 Predicate
的实现是如何通用地处理多种类型的操作的。这是因为查询语言顾名思义是一种开放式语言,你可以使用任何支持的操作,对任何字段进行过滤。
为了表示这种开放式过滤标准,我们使用了一种简单但相当灵活的实现方式 - SearchCriteria
:
public class SearchCriteria {
private String key;
private String operation;
private Object value;
}
SearchCriteria
包含表示约束条件的详细信息:
key
:字段名 - 例如:firstName
、age
等operation
:操作,例如:等于、小于 ... 等value
:字段值,例如:john
、25
等
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<>(); } 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、测试搜索过滤 {#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("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); }
}
根据 lastName
检索用户:
@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));
}
根据 firstName
和 lastName
检索用户:
@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)));
}
检索 lastName
等于 Doe
和 age
大于 25
的用户。
@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)));
}
检索不存在的记录:
@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());
}
根据 firstName
进行模糊检索:
@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 {#8usercontroller}
最后,构建 REST API,整合所有内容。
定义 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); }
}
测试 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