首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JPA 2.0 Criteria API:打造動態類型安全的資料庫查詢

     編輯:Java 相關知識

自 JPA 於 2006 年問世以來,廣受 Java 開發社群青睞。其重大更新版 JPA 2.0 (JSR 317) 於 2009 年底定案,核心新特性之一便是 Criteria API,它賦予 Java 一項獨特能力:讓編譯器在編譯期驗證查詢正確性,並支援在運行時動態構建查詢。本文將深入介紹 Criteria API 及其緊密相關的元模型概念,示範如何開發編譯器可檢查的類型安全查詢,以減少運行時錯誤,並與傳統基於字串的 JPQL 查詢做比較。

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();

核心構造解析:

  • CriteriaBuilder:CriteriaQuery 及各類查詢表達式的工廠。
  • CriteriaQuery<T>:泛型查詢對象,T 為結果類型。構建為查詢表達式樹(對應 FROM、WHERE、ORDER BY 等子句)。
  • Root<T>:查詢根,相當於 FROM 子句,泛型參數表示實體類型。
  • Path<T>:從 Root 導航到的持久化屬性,如 p.get(Person_.age)。
  • Predicate:布林表達式,用於 WHERE 條件。
  • TypedQuery<T>:攜帶類型信息的可執行查詢,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")),無法在編譯期驗證。

Metamodel API 深度解析

JPA Metamodel API 提供專門接口描述持久化類型與屬性:

  • 類型接口:EntityType、EmbeddableType、MappedSuperclassType,語義區分優於反射 API 的單一 Class。
  • 屬性接口:SingularAttribute、CollectionAttribute、ListAttribute、MapAttribute 等,精確表達關聯類型。

這些專門化接口不僅利於描述,更能協助構建類型安全查詢,減少運行時錯誤。

運行時作用域與 Metamodel 接口

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 風格,方法鏈式調用,代碼可讀性強。

Where 謂詞與複合條件

查詢餘額在 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)));

In 謂詞與多值表達式

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 建構子。
  • Object[]:cb.array(selections...)。
  • Tuple:cb.tuple(selections...),支援索引、別名、TupleElement 多種訪問方式。

multiselect() 方法根據 CriteriaQuery 的結果類型自動解釋選擇項,極具彈性。

高級特性:動態查詢與擴展

弱類型 API 支援運行時決定實體

當實體類型僅在運行時確定,可犧牲編譯期檢查,使用字串屬性名:

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,將顯著提升持久層代碼的健壯性與可維護性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved