1. 概述
在本教程中,我们将探讨如何使用 Spring Data JPA 和 Querydsl 构建一个 REST API 的查询语言。
在本系列前两篇文章中,我们分别使用了 JPA Criteria 和 Spring Data JPA 的 Specifications 实现了类似的搜索/过滤功能。
那么问题来了:为什么需要一个查询语言?
因为对于一个足够复杂的 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(Annotation Processing Tool)插件:
<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-type 类,例如 QMyUser
,用于构建类型安全的查询。
3. MyUser
实体类
接下来,我们定义一个用于搜索的实体类 MyUser
:
@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
为了实现灵活的动态查询,我们可以使用 PathBuilder
构建通用的 Predicate
。
下面是一个 MyUserPredicate
类的实现:
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;
}
}
⚠️ 注意:这里我们没有使用生成的 Q-type,而是使用 PathBuilder
来动态构建路径,这样更适用于抽象和通用的查询场景。
配套的 SearchCriteria
类如下:
public class SearchCriteria {
private String key;
private String operation;
private Object value;
}
字段含义如下:
key
:字段名,例如firstName
,age
operation
:操作符,如:
,>
,<
value
:字段值,例如john
,25
5. MyUserRepository
接口
为了让我们的 Repository 支持 Querydsl 的 Predicate
查询,需要让它继承 QuerydslPredicateExecutor
:
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);
}
}
✅ 通过 customize()
方法,我们可以自定义绑定规则,比如对字符串字段默认进行不区分大小写的模糊匹配,并排除某些字段(如 email
)。
6. 组合多个 Predicate
为了支持多个条件的组合查询,我们创建一个 MyUserPredicatesBuilder
:
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<BooleanExpression> 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;
}
}
✅ 通过 with()
方法可以链式添加多个查询条件,最终通过 build()
构建出一个组合的 BooleanExpression
。
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@example.com");
userJohn.setAge(22);
repo.save(userJohn);
userTom = new MyUser();
userTom.setFirstName("Tom");
userTom.setLastName("Doe");
userTom.setEmail("tom.doe@example.com");
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)));
}
姓氏 + 年龄组合查询
@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());
}
模糊匹配名字
@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. 构建 REST 接口:UserController
最后,我们将所有功能整合进一个 REST 接口:
@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
返回结果示例:
[{
"id":2,
"firstName":"tom",
"lastName":"doe",
"email":"tom.doe@example.com",
"age":26
}]
9. 总结
本篇文章介绍了如何使用 Spring Data JPA 和 Querydsl 构建一个灵活的 REST 查询语言。
我们通过 SearchCriteria
和 PathBuilder
实现了动态构建查询条件,并结合 QuerydslPredicateExecutor
实现了多条件组合查询。
虽然当前实现较为基础,但已经具备良好的扩展性,可以轻松支持更多操作符和字段类型。
完整代码可在 GitHub 项目 中找到。项目基于 Maven,可直接导入运行。
✅ 踩坑提醒:
- 注意 Querydsl 生成 Q-type 的配置是否正确,否则编译时会报找不到类
- 正则表达式要确保能正确解析查询字符串
- 注意
containsIgnoreCase
的使用场景和性能问题