bigywater.tech
Java · 419

JPA Criteria API:程序化构造类型化查询

Criteria API 的卖点是“类型化”和“程序化构造”。这篇用真实的 SQL 与计时量出它到底带来什么:SQL 与 JPQL 一样、运行时与编译期的错误反馈、构建开销与查询计划缓存、动态条件对 SQL 文本数量的影响,以及它没有拦住的类型错误。

同一条 SQL
JPQL 与 Criteria(cb.ge / cb.equal)发给 PostgreSQL 的 SQL:where p1_0.age>=? and p1_0.city=?
77.0 vs 61.5 µs
每次重新构建 CriteriaQuery 与 JPQL 字符串查询的单次耗时(含数据库往返)
8 种 vs 1 种
8 种条件组合的 Criteria 动态查询与“可选参数”JPQL 发给数据库的不同 SQL 文本数

JPQL 与 Criteria 的 SQL

实测 · 本机执行输出
  JPQL:select p from Person p where p.age >= :min and p.city = :city order by p.name
      SQL> select p1_0.id,p1_0.active,p1_0.age,p1_0.city,p1_0.name from Person p1_0 where p1_0.age>=? and p1_0.city=? order by p1_0.name fetch first ? rows only
  Criteria:cb.ge(age, 30)、cb.equal(city, "Beijing"),orderBy name:
      SQL> select p1_0.id,p1_0.active,p1_0.age,p1_0.city,p1_0.name from Person p1_0 where p1_0.age>=? and p1_0.city=? order by p1_0.name fetch first ? rows only
  Criteria:cb.equal(city, cb.literal("Beijing"))(显式字面量):
      SQL> select p1_0.id,p1_0.active,p1_0.age,p1_0.city,p1_0.name from Person p1_0 where p1_0.city='Beijing' fetch first ? rows only
  cb.literal 带单引号的用户输入 x' or '1'='1:
      SQL> select p1_0.id,p1_0.active,p1_0.age,p1_0.city,p1_0.name from Person p1_0 where p1_0.city='x'' or ''1''=''1'
      返回 0 行(单引号被转义成 '',没有变成 SQL 注入)
  JPQL 里直接写字面量 p.city = 'Beijing':
      SQL> select p1_0.id,p1_0.active,p1_0.age,p1_0.city,p1_0.name from Person p1_0 where p1_0.city='Beijing' fetch first ? rows only

运行时错误

实测 · 本机执行输出
  r.get("agee"):PathElementException:Could not resolve attribute 'agee' of 'Person'
  cb.equal(r.get("age"), "not-a-number"):CoercionException:Error coercing value
  r.get(Person_.age)(静态元模型)的属性类型是 int,正常执行返回 40 行

构建开销

实测 · 本机执行输出
  JPQL 字符串 + setParameter                      每次 61.5 µs;查询计划缓存 命中 10000 / 未命中 0
  每次重新构建 CriteriaQuery                         每次 77.0 µs;查询计划缓存 命中 0 / 未命中 0
  CriteriaQuery 构建一次、复用(ParameterExpression)   每次 67.3 µs;查询计划缓存 命中 0 / 未命中 0
  命名查询(@NamedQuery 等价:createNamedQuery)        每次 59.8 µs;查询计划缓存 命中 10000 / 未命中 0

动态条件

实测 · 本机执行输出
  8 种条件组合各执行 3 次(共 24 次)Criteria 动态查询:发给数据库的不同 SQL 文本共 8 种
      例 1:select …from Person p1_0
      例 2:select …from Person p1_0 where p1_0.name like ? escape ''
      例 3:select …from Person p1_0 where p1_0.age>=?
  同样 8 种组合 × 3 次,用一条“可选参数”JPQL(:p is null or …):发给数据库的不同 SQL 文本共 1 种(都是同一条带 ? 的 SQL,数据库的预编译语句可以复用;代价是执行计划是通用的,(? is null or col = ?) 往往用不上索引)

CriteriaUpdate / Delete

实测 · 本机执行输出
  CriteriaUpdate:把 city = Beijing 的 active 设为 false,影响 500 行
  CriteriaDelete:删除 age < 20 的行,影响 80 行

静态元模型的编译期反馈

实测 · 本机执行输出
  javac 退出码 1:
      <jtmp>/j279a/Bad.java:1: error: cannot find symbol
      import jakarta.persistence.criteria.*; public class Bad { static void f(CriteriaBuilder cb, Root<Person> r) { Predicate p = cb.equal(r.get(Person_.agee), 1); Predicate q = cb.equal(r.get(Person_.age),
                                                                                                                                                        ^
        symbol:   variable agee

分步演示

JPA Criteria API
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Hibernate ORM 6.6.4.Final + hibernate-jpamodelgen 6.6.4.Final、PostgreSQL JDBC 42.7.4、本机 PostgreSQL 16.15;开销数字是 5000 次循环第二轮的平均值,不是 JMH
    • 没有验证:hibernate.criteria.plan_cache_enabled(6.4+ 的 Criteria 计划缓存开关)打开后的开销、Querydsl / jOOQ 等其它类型安全查询构建器、fetch join 与分页的组合、Spring Data JPA 的 Specification、更复杂的子查询与窗口函数
    京ICP备2026067502号-1京公网安备11010502063487号