bigywater.tech
Spring · 90

Specification 与动态查询

动态查询为什么用 Specification?条件可能没有的时候怎么写?join 一对多之后分页为什么少数据?拼 like 的时候用户输入会不会有问题?这篇在 Spring Boot 4.0.8、Spring Data JPA 4、Hibernate 7 上逐个实测。

where(null) 抛异常
Spring Data JPA 4 里 Specification.where(null) 与 and(null) 都抛 IllegalArgumentException
1 vs 2
join 一对多不加 distinct 的分页,Page 总数 1;加 distinct 后总数 2
50% → 2 行 / 1 行
自己拼 % 匹配 2 行,转义 % 之后匹配 1 行

组合

实测 · 本机执行输出
name like 'lee'  ⇒ 2 行 [Ann Lee, Bob Lee]
    select … from person p1_0 where lower(p1_0.name) like ? escape ''
minAge 30 and city Rome(Specification.where(…).and(…))  ⇒ 2 行 [Ann Lee, x' or '1'='1]
    select … from person p1_0 where p1_0.age>=? and p1_0.city=?
city Rome or city Oslo  ⇒ 8 行 [Ann Lee, Bob Lee, Cid Ray, Dee Ray, 50%off, 500off, Eve Poe, x' or '1'='1]
    select … from person p1_0 where p1_0.city=? or p1_0.city=?
not(city Rome)  ⇒ 4 行 [Cid Ray, Dee Ray, 50%off, 500off]
    select … from person p1_0 where p1_0.city<>?
Specification.where(null)  ⇒ 抛 IllegalArgumentException:Specification must not be null
spec.and(null)  ⇒ 抛 IllegalArgumentException:Other specification must not be null
Specification.unrestricted()  ⇒ 8 行 [Ann Lee, Bob Lee, Cid Ray, Dee Ray, 50%off, 500off, Eve Poe, x' or '1'='1]
    select … from person p1_0
所有条件都不成立(三个辅助方法都返回 Specification.unrestricted())  ⇒ 8 行 [Ann Lee, Bob Lee, Cid Ray, Dee Ray, 50%off, 500off, Eve Poe, x' or '1'='1]
    select … from person p1_0
  -- 动态查询:三个可选条件 name / minAge / city,8 种组合
  name=null minAge=null city=null → 8 行;SQL 条数 1,(没有 where)
  name=e minAge=null city=null → 4 行;SQL 条数 1, where lower(p1_0.name) like ? escape ''
  name=null minAge=25 city=null → 6 行;SQL 条数 1, where p1_0.age>=?
  name=e minAge=25 city=null → 2 行;SQL 条数 1, where lower(p1_0.name) like ? escape '' and p1_0.age>=?
  name=null minAge=null city=Rome → 4 行;SQL 条数 1, where p1_0.city=?
  name=e minAge=null city=Rome → 3 行;SQL 条数 1, where lower(p1_0.name) like ? escape '' and p1_0.city=?
  name=null minAge=25 city=Rome → 3 行;SQL 条数 1, where p1_0.age>=? and p1_0.city=?
  name=e minAge=25 city=Rome → 2 行;SQL 条数 1, where lower(p1_0.name) like ? escape '' and p1_0.age>=? and p1_0.city=?

and、or、not 分别生成 and、or、<>;没有条件用 Specification.unrestricted()。

分页

实测 · 本机执行输出
findAll(minAge 20, PageRequest.of(0, 3, Sort.by("age")))  ⇒ Page(内容 3 行,总数 7)
    select … from person p1_0 where p1_0.age>=? order by p1_0.age limit ?,?
    select count(p1_0.id) from person p1_0 where p1_0.age>=?
findAll(city Oslo, PageRequest.of(1, 2))  ⇒ Page(内容 2 行,总数 4)
    select … from person p1_0 where p1_0.city=? limit ?,?
    select count(p1_0.id) from person p1_0 where p1_0.city=?
count(city Oslo)  ⇒ 4
    select count(p1_0.id) from person p1_0 where p1_0.city=?
exists(city Mars)  ⇒ false
    select … from person p1_0 where p1_0.city=? limit ?
findOne(name like 'cid')  ⇒ Optional[Cid Ray]
    select … from person p1_0 where lower(p1_0.name) like ? escape '' limit ?

数据与 count 用同一份 Specification;exists 与 findOne 加 limit。

join

实测 · 本机执行输出
phone like '555' 不 distinct  ⇒ 2 行 [Ann Lee, Cid Ray]
    select … from person p1_0 join phone p2_0 on p1_0.id=p2_0.owner_id where p2_0.number like ? escape ''
phone like '555' + query.distinct(true)  ⇒ 2 行 [Ann Lee, Cid Ray]
    select distinct … from person p1_0 join phone p2_0 on p1_0.id=p2_0.owner_id where p2_0.number like ? escape ''
Page:phone like '555' 不 distinct,每页 2  ⇒ Page(内容 1 行,总数 1)
    select … from person p1_0 join phone p2_0 on p1_0.id=p2_0.owner_id where p2_0.number like ? escape '' limit ?,?
Page:phone like '555' + distinct,每页 2  ⇒ Page(内容 2 行,总数 2)
    select distinct … from person p1_0 join phone p2_0 on p1_0.id=p2_0.owner_id where p2_0.number like ? escape '' limit ?,?
    select distinct count(distinct p1_0.id) from person p1_0 join phone p2_0 on p1_0.id=p2_0.owner_id where p2_0.number like ? escape ''

带集合 join 的分页要 distinct,否则 Page 少数据。

用户输入

实测 · 本机执行输出
rawContains("50%")(自己拼 %)  ⇒ 2 行 [50%off, 500off]
    select … from person p1_0 where p1_0.name like ? escape ''
safeContains("50%")(转义 % 和 _ 并声明 escape)  ⇒ 1 行 [50%off]
    select … from person p1_0 where p1_0.name like ? escape '\\'
rawContains("x' or '1'='1")(想注入)  ⇒ 1 行 [x' or '1'='1]
    select … from person p1_0 where p1_0.name like ? escape ''

参数绑定挡住注入,通配符要自己转义。

分步演示

Specification
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Hibernate 7、Spring Data JPA 4 随 spring-boot-starter-data-jpa;MySQL 是本机自己起的 26.7.0 实例)。Person 8 行(含名字 50%off、500off 和一个带引号的名字),Phone 5 行;SQL 来自 Hibernate 的 StatementInspector,select 的列清单在输出中省略成 …;join 的 Specification 通过 root.join("phones") 实现
    • 没有验证:Spring Data JPA 4 新增的 PredicateSpecification、UpdateSpecification、DeleteSpecification;Querydsl 与 Example 查询;Criteria API 的元模型(JPA static metamodel,类型安全的属性引用);多表 join 的 fetch 与 N+1(见第 91 篇);动态排序;复杂 Specification 对索引的影响(没有看 EXPLAIN)
    京ICP备2026067502号-1京公网安备11010502063487号