自 JPA 於 2006 年問世以來,廣受 Java 開發社群青睞。其重大更新版 JPA 2.0 (JSR 317) 於 2009 年底定案,核心新特性之一便是 Criteria API,它賦予 Java 一項獨特能力:讓編譯器在編譯期驗證查詢正確性,並支援在運行時動態構建查詢。本文將深入介紹 Criteria API 及其緊密相關的元模型概念,示範如何開發編譯器可檢查的類型安全查詢,以減少運行時錯誤,並與傳統基於字串的 JPQL 查詢做比較。
JPA 1.0 引入的 JPQL 雖強大,但因基於字串且語法有限,存在明顯缺陷。以清單 1 為例,一個簡單的 JPQL 查詢選擇年齡大於 20 歲的 Person:
EntityManager em = ...;
String jpql = "select p from Person where p.age > 20";
Query query = em.createQuery(jpql);
List result = query.getResultList();
此代碼能通過編譯,但運行時會失敗,因 JPQL 語法錯誤(缺少別名 p)。正確語法應為 select p from Person p where p.age > 20。Java 編譯器無法發現此類錯誤,只能在運行時由 JPA 提供者拋出異常。
Criteria API 最大優勢在於禁止構造語法錯誤的查詢。清單 2 使用 CriteriaQuery 重寫上述查詢:
EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> cq = cb.createQuery(Person.class);
Root<Person> p = cq.from(Person.class);
Predicate condition = cb.gt(p.get(Person_.age), 20);
cq.where(condition);
TypedQuery<Person> q = em.createQuery(cq);
List<Person> result = q.getResultList();
核心構造解析:
CriteriaQuery 及各類查詢表達式的工廠。T 為結果類型。構建為查詢表達式樹(對應 FROM、WHERE、ORDER BY 等子句)。p.get(Person_.age)。getResultList() 直接返回 List<T>,免去強制轉型與 ClassCastException 風險。關鍵在於 Person_.age:它是規範元模型類 Person_ 的靜態字段,由 JPA 2.0 引入的 Metamodel API 定義。編譯器可據此進行類型檢查,例如 cb.gt(p.get(Person_.age), "xyz") 會直接報編譯錯誤,因 age 為數值型不可與字串比較。
元模型類描述持久化類的元數據。以 domain.Person 實體為例:
@Entity
public class Person {
@Id private long ssn;
private String name;
private int age;
// getters/setters
}
其對應的規範元模型類 Person_:
@StaticMetamodel(Person.class)
public class Person_ {
public static volatile SingularAttribute<Person, Long> ssn;
public static volatile SingularAttribute<Person, String> name;
public static volatile SingularAttribute<Person, Integer> age;
}
元模型類將每個持久化屬性聲明為 SingularAttribute<Entity, PropertyType> 類型的靜態公共字段。這使開發者能以強類型方式引用屬性(Person_.age),編譯器即可實施類型檢查。這與 Java 反射 API 本質不同:反射基於字串 (Person.class.getField("age")),無法在編譯期驗證。
JPA Metamodel API 提供專門接口描述持久化類型與屬性:
EntityType、EmbeddableType、MappedSuperclassType,語義區分優於反射 API 的單一 Class。SingularAttribute、CollectionAttribute、ListAttribute、MapAttribute 等,精確表達關聯類型。這些專門化接口不僅利於描述,更能協助構建類型安全查詢,減少運行時錯誤。
javax.persistence.metamodel.Metamodel 接口是持久化單元中所有實體元模型的容器,類比 ClassLoader。可通過 EntityManagerFactory.getMetamodel() 獲取,進而訪問 EntityType<Person> 等元模型元素。應用程序可在運行時瀏覽屬性,但編譯期類型安全依賴於靜態實例化的規範元模型類(如 Person_)。
JPA 2.0 規範了規範元模型類的精確結構(類名、字段名)。應用程序也可自行編寫非規範元模型,但可移植性較差。規範元模型字段僅聲明不初始化,運行時由 JPA 提供者賦值。
元模型類通常由註解處理器(如 OpenJPA 提供的工具)在編譯期自動生成。常見問題包括:生成文件放置位置、版本控制管理、實體與元模型同步維護等。當前多採用 Java 6 編譯器集成的 Annotation Processor 機制。
如查詢所有 Account 平均餘額:
CriteriaQuery<Double> cq = cb.createQuery(Double.class);
Root<Account> a = cq.from(Account.class);
cq.select(cb.avg(a.get(Account_.balance)));
等效 JPQL:select avg(a.balance) from Account a。Criteria API 採用流式 API 風格,方法鏈式調用,代碼可讀性強。
查詢餘額在 100 到 200 之間的帳戶:
CriteriaQuery<Account> cq = cb.createQuery(Account.class);
Root<Account> account = cq.from(Account.class);
Path<Integer> balance = account.get(Account_.balance);
cq.where(cb.and(cb.gt(balance, 100), cb.lt(balance, 200)));
Path<String> name = account.get(Account_.owner).get(Person_.name);
cq.where(cb.in(name).value("X").value("Y").value("Z"));
泛型機制確保 in 表達式僅接受 String 類型參數,避免 JPQL 中易犯的未加引號錯誤。
查詢擁有未發貨訂單的客戶:
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> c = cq.from(Customer.class);
SetJoin<Customer, PurchaseOrder> o = c.join(Customer_.orders);
Predicate p = cb.equal(o.get(PurchaseOrder_.status), Status.DELIVERED).not();
cq.where(p);
連接表達式(Join、SetJoin、ListJoin、MapJoin)泛型參數為源實體類型與目標實體類型(而非集合聲明類型)。每次 join() 返回新實例,複雜謂詞需復用同一連接變量。
Criteria API 原生支援強類型參數:
ParameterExpression<Integer> ageParam = cb.parameter(Integer.class);
Predicate cond = cb.gt(p.get(Person_.age), ageParam);
cq.where(cond);
TypedQuery<Person> q = em.createQuery(cq);
List<Person> result = q.setParameter(ageParam, 20).getResultList();
參數表達式攜帶類型信息,編譯期即可阻擋類型不匹配的綁定與比較,優於 JPQL 的命名/位置參數。
Criteria API 提供豐富的結果構造方式:
cq.select(root) 或依賴 createQuery(Entity.class) 默認行為。cb.construct(DTO.class, selections...) 調用 DTO 建構子。cb.array(selections...)。cb.tuple(selections...),支援索引、別名、TupleElement 多種訪問方式。multiselect() 方法根據 CriteriaQuery 的結果類型自動解釋選擇項,極具彈性。
當實體類型僅在運行時確定,可犧牲編譯期檢查,使用字串屬性名:
Class<Account> cls = Class.forName("domain.Account");
EntityType<Account> entity = em.getMetamodel().entity(cls);
CriteriaQuery<Account> cq = cb.createQuery(cls);
Root<Account> root = cq.from(entity);
Path<Integer> balance = root.<Integer>get("balance");
cb.function("CURRENT_USER", String.class, (Expression[])null) 可調用資料庫原生函數(如 MySQL 的 CURRENT_USER()),突破 JPQL 固定函數集限制。
CriteriaQuery 子句(select、where、orderBy)可在運行時修改。典型應用:先按姓名排序,再追加按郵遞區號降序:
cq.orderBy(cb.asc(p.get(Person_.name)));
// 執行首次查詢...
List<Order> newOrders = new ArrayList<>(cq.getOrderList());
newOrders.add(cb.desc(p.get(Person_.zipcode)));
cq.orderBy(newOrders);
// 執行細化查詢...
OpenJPA 等提供者支援 setCandidateCollection() 在記憶體中對前次結果進一步過濾,提升效能。
OpenJPA 擴展 cb.qbe(from, template) 依模板實例非空/非默認屬性自動生成聯合謂詞,簡化複雜條件構建。
JPA 2.0 Criteria API 結合 Metamodel API,為 Java 持久化查詢帶來革命性進步:編譯期類型安全消除語法錯誤,動態編程模型支援複雜查詢構建與運行時修改,資料庫函數擴展與依範例查詢等高級特性更釋放了無限潛能。掌握這套 API,將顯著提升持久層代碼的健壯性與可維護性。