1. 概述

在本教程中,我们将延续 Spring Data Querydsl Web 支持(第一部分) 的内容,进入第二阶段。✅
本次的重点是 关联实体(associated entities) 以及如何通过 HTTP 请求实现跨表查询。

项目结构仍基于 Maven,配置方式与第一部分一致。如果你还没看过前文,建议先回顾基础配置部分,避免踩坑。本文不再重复讲解环境搭建。

2. 实体设计

我们新增一个 Address 实体类,用于表示用户地址信息,并与 User 建立一对一关联。这里为了简化模型,使用 OneToOne 关系。

最终的两个实体如下:

@Entity 
public class User {

    @Id 
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
    private Address address;

    // getters & setters 
}
@Entity 
public class Address {

    @Id 
    @GeneratedValue
    private Long id;

    private String address;

    private String country;

    @OneToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "user_id") 
    private User user;

    // getters & setters
}

⚠️ 注意:mappedBy = "user" 表示关系由 Address 端维护,避免双向映射时出现重复插入或更新问题。

3. Spring Data 仓库接口

接下来为每个实体创建对应的 Repository。关键在于启用 Querydsl 支持,以便支持动态谓词查询。

AddressRepository 为例,展示如何定制 Querydsl 绑定行为:

public interface AddressRepository extends JpaRepository<Address, Long>, 
  QuerydslPredicateExecutor<Address>, QuerydslBinderCustomizer<QAddress> {
 
    @Override 
    default void customize(QuerydslBindings bindings, QAddress root) {
        bindings.bind(String.class)
          .first((SingleValueBinding<StringPath, String>) StringExpression::eq);
    }
}

关键点说明:

  • QuerydslPredicateExecutor 提供了基于 Predicate 的查询能力
  • QuerydslBinderCustomizer 允许我们自定义字段绑定逻辑
  • ✅ 上述代码将所有 String 类型字段的默认匹配方式设为 精确等于(eq),而不是模糊匹配,避免误伤

💡 小技巧:如果不做这个定制,默认情况下 Querydsl 会对字符串使用 like 匹配,容易导致意外结果。显式声明更安全。

4. 查询用 RestController

在第一部分中,我们已经实现了对 User 的动态查询接口。现在只需复用相同模式,扩展到 Address 表即可。

新增一个接口用于查询地址数据:

@GetMapping(value = "/addresses", produces = MediaType.APPLICATION_JSON_VALUE)
public Iterable<Address> queryOverAddress(
  @QuerydslPredicate(root = Address.class) Predicate predicate) {
    BooleanBuilder builder = new BooleanBuilder();
    return addressRepository.findAll(builder.and(predicate));
}

参数解析:

  • @QuerydslPredicate(root = Address.class):告诉 Spring Data Querydsl,HTTP 参数应映射到 Address 的 Q-type 谓词
  • BooleanBuilder:用于组合多个条件,虽然此处只有一个 predicate,但保留它便于后续扩展

5. 集成测试验证

我们通过集成测试验证跨表查询是否生效。使用 MockMvc 模拟 HTTP 请求,测试能否通过地址国家字段过滤用户。

示例:查询所有居住在西班牙的用户

请求 URL:

/users?addresses.country=Spain

对应测试用例:

@Test
public void givenRequest_whenQueryUserFilteringByCountrySpain_thenGetJohn() throws Exception {
    mockMvc.perform(get("/users?address.country=Spain"))
      .andExpect(status().isOk())
      .andExpect(content().contentType(contentType))
      .andExpect(jsonPath("$", hasSize(1)))
      .andExpect(jsonPath("$[0].name", is("John")))
      .andExpect(jsonPath("$[0].address.address", is("Fake Street 1")))
      .andExpect(jsonPath("$[0].address.country", is("Spain")));
}

生成的 SQL 语句

Querydsl 会自动解析 HTTP 参数并生成等效 SQL:

select user0_.id as id1_1_, 
       user0_.name as name2_1_ 
from user user0_ 
      cross join address address1_ 
where user0_.id=address1_.user_id 
      and address1_.country='Spain'

✅ 结果正确:通过 cross join 关联 useraddress 表,并根据 country 条件过滤。

参数命名规则

注意 URL 中的参数名:

  • /users?address.country=Spain → 错误(字段名不匹配)
  • /users?addresses.country=Spain → 正确(与 User 实体中字段名一致)

⚠️ 踩坑提醒:Java 字段是 address,但 Querydsl 生成的 Q-type 属性名是基于变量名的。如果字段名为 address,Q-type 中也是 address,但某些 IDE 或 Lombok 可能影响生成。建议打印 Q 类确认。

6. 总结

通过本篇实践,我们可以得出以下结论:

  • ✅ Querydsl 提供了一种简单粗暴 yet 强大的方式,让前端可以通过 HTTP 参数直接构建复杂查询
  • ✅ 支持跨表关联查询,无需手写 JPQL 或 Criteria API
  • ✅ 结合 @QuerydslPredicateQuerydslBinderCustomizer,可灵活控制字段绑定策略
  • ✅ 对 REST API 来说,这种“零代码”实现动态查询的方式极大提升了开发效率

📌 延伸场景:

  • 可进一步支持排序、分页(Pageable + Sort
  • 自定义绑定逻辑支持 inbetween、嵌套对象等高级操作
  • 安全性考虑:限制可查询字段,防止敏感信息泄露

完整示例代码已托管至 GitHub:
👉 https://github.com/example/tutorials/tree/master/persistence-modules/spring-data-rest-querydsl

项目基于 Maven 构建,导入即用,适合快速集成进现有 Spring Boot 项目。


原始标题:REST Queries Over Multiple Tables with Querydsl