1. 概述

本文将介绍 Spring Data 提供的 Querydsl Web 支持功能。

这是实现 REST 接口动态查询的一种非常优雅的方式,相比我们之前在《REST 查询语言系列》中讨论的其他方案(如 Specification、自定义解析器等),它更加类型安全且简洁。✅

如果你正在寻找一种简单粗暴但又足够强大的方式来支持 URL 参数动态查询,那这套组合值得你加入集合夹。

2. Maven 配置

首先,引入必要的依赖。核心是 spring-data-commons 和 Querydsl 相关组件:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.0.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-commons</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
    </dependency>
    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>${querydsl.version}</version>
    </dependency>
    ...
</dependencies>

⚠️ 注意:
Querydsl 的 Web 支持从 spring-data-commons 1.11 版本开始内置,无需额外集成。只要版本满足,开箱即用。

3. 用户仓库接口(UserRepository)

关键在于继承 QueryDslPredicateExecutorQuerydslBinderCustomizer,实现自定义绑定逻辑:

public interface UserRepository extends 
  JpaRepository<User, Long>, QueryDslPredicateExecutor<User>, QuerydslBinderCustomizer<QUser> {

    @Override
    default public void customize(QuerydslBindings bindings, QUser root) {
        bindings.bind(String.class).first(
          (StringPath path, String value) -> path.containsIgnoreCase(value));
        bindings.excluding(root.email);
    }
}

重点说明:

  • QueryDslPredicateExecutor:提供基于 Predicate 的查询能力
  • QuerydslBinderCustomizer:允许你定制请求参数到 Predicate 的映射规则
  • bindings.bind(String.class):对所有字符串字段统一设置为 忽略大小写的模糊匹配
  • bindings.excluding(root.email):明确排除 email 字段,防止通过 URL 查询邮箱

💡 踩坑提醒:
QUser 是 Querydsl 的元模型类,由注解处理器在编译期生成。确保已配置 querydsl-apt 插件,否则会报找不到此类。

4. 用户控制器(UserController)

控制器写法极其简洁,核心是 @QuerydslPredicate 注解:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public Iterable<User> findAllByWebQuerydsl(
  @QuerydslPredicate(root = User.class) Predicate predicate) {
    return userRepository.findAll(predicate);
}

✅ 核心亮点:

  • Spring MVC 会自动将 HTTP 请求参数解析为 Predicate
  • 参数名对应实体字段名(支持嵌套属性,如 address.city
  • 不需要写任何解析逻辑,框架全帮你搞定

示例请求:

http://localhost:8080/users?firstName=john

返回结果示例:

[
   {
      "id": 1,
      "firstName": "john",
      "lastName": "doe",
      "email": "john.doe@example.com",
      "age": 11
   }
]

支持的操作符(通过后缀控制):

操作符 示例参数 说明
默认 ?name=john 根据字段类型决定行为(String 走 contains,数值走 equals)
like ?name.like=jo 显式模糊匹配
gt / lt ?age.gt=18 大于、小于
in ?role.in=ADMIN,USER 包含于列表

5. 集成测试(Live Test)

使用 TestRestTemplate 或 RestAssured 进行端到端测试,验证查询逻辑是否生效。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class UserLiveTest {

    private ObjectMapper mapper = new ObjectMapper();
    private User userJohn = new User("john", "doe", "john.doe@example.com");
    private User userTom = new User("tom", "doe", "tom.doe@example.com");

    private static boolean setupDataCreated = false;

    @Before
    public void setupData() throws JsonProcessingException {
        if (!setupDataCreated) {
            givenAuth().contentType(MediaType.APPLICATION_JSON_VALUE)
                       .body(mapper.writeValueAsString(userJohn))
                       .post("http://localhost:8080/users");
 
            givenAuth().contentType(MediaType.APPLICATION_JSON_VALUE)
                       .body(mapper.writeValueAsString(userTom))
                       .post("http://localhost:8080/users");
            setupDataCreated = true;
        }
    }

    private RequestSpecification givenAuth() {
        return RestAssured.given().auth().preemptive().basic("user1", "user1Pass");
    }
}

测试用例 1:获取全部用户

@Test
public void whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get("http://localhost:8080/users");
    User[] result = response.as(User[].class);
    assertEquals(result.length, 2);
}

测试用例 2:按 firstName 查询(精确字段名)

@Test
public void givenFirstName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get("http://localhost:8080/users?firstName=john");
    User[] result = response.as(User[].class);
    assertEquals(result.length, 1);
    assertEquals(result[0].getEmail(), userJohn.getEmail());
}

测试用例 3:按 lastName 模糊查询(partial match)

@Test
public void givenPartialLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get("http://localhost:8080/users?lastName=do");
    User[] result = response.as(User[].class);
    assertEquals(result.length, 2);
}

测试用例 4:尝试通过 email 查询(应被忽略)

@Test
public void givenEmail_whenGettingListOfUsers_thenIgnored() {
    Response response = givenAuth().get("http://localhost:8080/users?email=john");
    User[] result = response.as(User[].class);
    assertEquals(result.length, 2);
}

⚠️ 结果验证:
尽管 URL 中传了 email=john,但由于我们在 customize() 方法中调用了 excluding(root.email),该条件被自动忽略,因此仍返回所有用户。

6. 总结

Spring Data Querydsl Web Support 是一个被低估但非常实用的功能,特别适合需要支持灵活查询的后台管理系统或内部服务。

✅ 优势总结:

  • 类型安全:基于元模型(Q-class),避免字符串拼接错误
  • 零解析代码:无需手动解析 request parameter
  • 高度可定制:通过 QuerydslBinderCustomizer 精细控制每个字段的行为
  • 天然防注入:基于 JPA Criteria,避免 SQL 注入风险

⚠️ 使用建议:

  • 适用于中后台场景,对外 API 建议加一层白名单过滤
  • 注意暴露字段的风险,敏感字段务必 excluding
  • 生产环境建议结合分页(Pageable)一起使用

如果你还没用过这个特性,建议在下一个项目中尝试一下——一旦用上,就再也回不去了


原始标题:REST Query Language with Querydsl Web Support

« 上一篇: Java 中的 JSON 处理
» 下一篇: Java Web Weekly 47