JPA criteria 查詢:類型安全與面向對象

http://my.oschina.net/zhaoqian/blog/133500


序言

自工作以來,除了以前比較流量的hibernate,就是一直使用ORM 規範 JPA了.而這幾天工作需要,研究了下JPA的標準查詢,名爲:JPA criteria查詢.相比JPQL,其優勢是類型安全,更加的面向對象.

使用標準查詢,開發人員可在編譯的時候就檢查查詢的正確與否.而以前也只是在Hibernate中聽說有過.具體不詳,沒用過.

用的maven插件生成的.具體看這些把.
Hibernate

org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor
http://relation.to/Bloggers/HibernateStaticMetamodelGeneratorAnnotationProcessor
OpenJPA

org.apache.openjpa.persistence.meta.AnnotationProcessor6
http://openjpa.apache.org/builds/latest/docs/manual/manual.html#d0e11094
DataNucleus

org.datanucleus.jpa.JPACriteriaProcessor
http://www.datanucleus.org/products/accessplatform_2_1/jpa/jpql_criteria_metamodel.html

一.JPA元模型概念,及使用

在JPA中,標準查詢是以元模型的概念爲基礎的.元模型是爲具體持久化單元的受管實體定義的.這些實體可以是實體類,嵌入類或者映射的父類.提供受管實體元信息的類就是元模型類.

描述受管類的狀態和他們之間的關係的靜態元模型類可以

  • 1.從註解處理器產生
  • 2.從程序產生
  • 3.用EntityManager訪問.

如下code,一個簡單的實體類package com.demo.entities;下,實體類Employee ,假設該實體有諸如id,name和age的基本屬性,還有與類Address的OneToMany關聯:

?
1
2
3
4
5
6
7
8
9
10
@Entity
@Table
public class Employee{ 
    private int id;  
    private String name;
    private int age;
    @OneToMany
    private List<Address> addresses;
    // Other code…
}

Employee類(com.demo.entities包中定義)的標準元模型類的名字將是使用 javax.persistence.StaticMetamodel註解的Employee_。元模型類的屬性全部是static和public的。Employee的每一個屬性都會使用在JPA2規範中描述的以下規則在相應的元模型類中映射:

  • 諸如id,name和age的非集合類型,會定義靜態屬性SingularAttribute<A, B> b,這裏b是定義在類A中的類型爲B的一個對象。
  • 對於Addess這樣的集合類型,會定義靜態屬性ListAttribute<A, B> b,這裏List對象b是定義在類A中類型B的對象。其它集合類型可以是SetAttribute, MapAttribute 或 CollectionAttribute 類型。

 以下是用註解處理器產生的元模型類package com.demo.entities;下:

?
1
2
3
4
5
6
7
8
9
10
11
12
import javax.annotation.Generated;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@Generated("org.hibernate.jpamodelgen.JPAMetaModelEntityProcesso")
@StaticMetamodel(Employee.class)
public class Employee_ {    
    public static volatile SingularAttribute<Employee, Integer> id;  
    public static volatile SingularAttribute<Employee, Integer> age;  
    public static volatile SingularAttribute<Employee, String> name;   
    public static volatile ListAttribute<Employee, Address> addresses;
}

就像它的名字表明的,註解處理器處理註解,幫助產生源代碼註解處理在編譯時就能激活。元模型類遵循JPA2.0規範中爲定義標準元模型類而描述的規則創建。

使用元模型類最大的優勢是憑藉其實例化可以在編譯時訪問實體的持久屬性.該特性使得criteria 查詢更加類型安全.

元模型API與Java中的標準反射API密切相關。主要不同在於使用標準反射API編譯器無法驗證其正確性。例如:下面的代碼會通過編譯測試:

?
1
2
Class myClass = Class.forName("com.demo.Test");
Field myField = myClass.getField("myName");
編譯器假定com.demo.Test中定義了屬性myName,一旦該類並沒有定義屬性myName,編譯器將拋出運行時異常。

元模型API會強制編譯器檢查適當的值是否分配給實體類的持久屬性。例如:考慮Employee類的age屬性,它是Integer變量。若該屬性被賦值爲String類型的值,編譯器會拋出錯誤。該實現並不要求支持非標準特性。程序員編寫的元模型類通常稱爲非標準元模型類。當EntityManagerFactory 創建時,持久化提供者會初始化元模型類的屬性。

二.使用criteria 查詢簡單Demo

爲了更好的理解criteria 查詢,考慮擁有Employee實例集合的Dept實體,Employee和Dept的元模型類的代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//All Necessary Imports
@StaticMetamodel(Dept.class)
public class Dept_ {   
    public static volatile SingularAttribute<Dept, Integer> id;  
    public static volatile ListAttribute<Dept, Employee> employeeCollection;   
    public static volatile SingularAttribute<Dept, String> name;
}
//All Necessary Imports
@StaticMetamodel(Employee.class)
public class Employee_ {    
    public static volatile SingularAttribute<Employee, Integer> id;   
    public static volatile SingularAttribute<Employee, Integer> age;   
    public static volatile SingularAttribute<Employee, String> name;   
    public static volatile SingularAttribute<Employee, Dept> deptId;
}
下面的代碼片段展示了一個criteria 查詢,它用於獲取所有年齡大於24歲的員工:
?
1
2
3
4
5
6
7
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
Root<Employee> employee = criteriaQuery.from(Employee.class);
Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);
criteriaQuery.where(condition);
TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);
List<Employee> result = typedQuery.getResultList();

對應的SQL: SELECT * FROM employee WHERE age > 24

三.構建CriteriaQuery 實例API說明

1.CriteriaBuilder 安全查詢創建工廠,創建CriteriaQuery,創建查詢具體具體條件Predicate 等

CriteriaBuilder是一個工廠對象,安全查詢的開始.用於構建JPA安全查詢.可以從EntityManager 或 EntityManagerFactory類中獲得CriteriaBuilder. 
比如: CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

2.CriteriaQuery 安全查詢主語句

CriteriaQuery對象必須在實體類型或嵌入式類型上的Criteria 查詢上起作用。
它通過調用 CriteriaBuilder, createQuery 或CriteriaBuilder.createTupleQuery 獲得。
CriteriaBuilder就像CriteriaQuery 的工廠一樣。
CriteriaBuilder工廠類是調用EntityManager.getCriteriaBuilder 或 EntityManagerFactory.getCriteriaBuilder而得。 
Employee實體的 CriteriaQuery 對象以下面的方式創建:

?
1
2
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);

3.Root 定義查詢的From子句中能出現的類型

AbstractQuery是CriteriaQuery 接口的父類。它提供得到查詢根的方法。 
Criteria查詢的查詢根定義了實體類型,能爲將來導航獲得想要的結果,它與SQL查詢中的FROM子句類似。 
Root實例也是類型化的,且定義了查詢的FROM子句中能夠出現的類型。 
查詢根實例能通過傳入一個實體類型給 AbstractQuery.from方法獲得。 
Criteria查詢,可以有多個查詢根。 
Employee實體的查詢根對象可以用以下的語法獲得 : 
?
1
Root<Employee> employee = criteriaQuery.from(Employee.class);

4.Predicate 過濾條件

過濾條件應用到SQL語句的FROM子句中。 
在criteria 查詢中,查詢條件通過Predicate 或Expression 實例應用到CriteriaQuery 對象上。 
這些條件使用 CriteriaQuery .where 方法應用到CriteriaQuery 對象上。 
CriteriaBuilder 也是作爲Predicate 實例的工廠,Predicate 對象通過調用CriteriaBuilder 的條件方法( equal,notEqual, gt, ge,lt, le,between,like等)創建。 
Predicate 實例也可以用Expression 實例的 isNull, isNotNull 和 in方法獲得,複合的Predicate 語句可以使用CriteriaBuilder的and, or andnot 方法構建。 
下面的代碼片段展示了Predicate 實例檢查年齡大於24歲的員工實例:
?
1
2
Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);
criteriaQuery.where(condition);

過Employee_元模型類age屬性,稱之爲路徑表達式。若age屬性與String文本比較,編譯器會拋出錯誤,這在JPQL中是不可能的。

5.Predicate[] 多個過濾條件

List<Predicate> predicatesList = new ArrayList<Predicate>();

predicatesList.add(.....Pridicate....)

criteriaQuery.where(predicatesList.toArray(new Predicate[predicatesList.size()]));

OR語句


?
1
predicatesList.add(criteriaBuilder.or(criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.repairing),criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.diagnos)));


忽略大小寫(全大寫)


?
1
predicatesList.add(criteriaBuilder.like(criteriaBuilder.upper(root.get(RepairShop_.shopName)), StringUtils.upperCase(StringUtils.trim(this.shopName)) + "%"));
通過如上兩句添加多個.


6.TypedQuery執行查詢與獲取元模型實例

注意,你使用EntityManager創建查詢時,可以在輸入中指定一個CriteriaQuery對象,它返回一個TypedQuery,它是JPA 2.0引入javax.persistence.Query接口的一個擴展,TypedQuery接口知道它返回的類型。

所以使用中,先創建查詢得到TypedQuery,然後通過typeQuery得到結果.

當EntityManager.createQuery(CriteriaQuery)方法調用時,一個可執行的查詢實例會創建,該方法返回指定從 criteria 查詢返回的實際類型的TypedQuery 對象。

TypedQuery 接口是javax.persistence.Queryinterface.的子類型。在該片段中, TypedQuery 中指定的類型信息是Employee,調用getResultList時,查詢就會得到執行 
TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);
List<Employee> result = typedQuery.getResultList();
元模型實例通過調用 EntityManager.getMetamodel 方法獲得,EntityType<Employee>的元模型實例通過調用Metamodel.entity(Employee.class)而獲得,其被傳入 CriteriaQuery.from 獲得查詢根。

?
1
2
3
Metamodel metamodel = em.getMetamodel();EntityType<Employee>
Employee_ = metamodel.entity(Employee.class);
Root<Employee> empRoot = criteriaQuery.from(Employee_);

也有可能調用Root.getModel方法獲得元模型信息。類型 EntityType<Dept>的實例Dept_和name屬性可以調用getSingularAttribute 方法獲得,它與String文本進行比較:

?
1
2
3
4
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery();
Root<Dept> dept = criteriaQuery.from(Dept.class);
EntityType<Dept> Dept_ = dept.getModel();
Predicate testCondition = criteriaBuilder.equal(dept.get(Dept_.getSingularAttribute("name", String.class)), "Ecomm");

7.Expression 用在查詢語句的select,where和having子句中,該接口有 isNull, isNotNull 和 in方法

Expression對象用在查詢語句的select,where和having子句中,該接口有 isNull, isNotNull 和 in方法,下面的代碼片段展示了Expression.in的用法,employye的年齡檢查在20或24的。 
?
1
2
3
4
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class);
Root<Employee> employee = criteriaQuery.from(Employee.class);
criteriaQuery.where(employee.get(Employee_.age).in(20, 24));
em.createQuery(criteriaQuery).getResultList();

對應的 SQL: SELECT * FROM employee WHERE age in (20, 24)

下面也是一個更貼切的例子:

?
1
2
3
4
5
6
7
8
9
//定義一個Expression
Expression<String> exp = root.get(Employee.id);
//
List<String> strList=new ArrayList<>();
strList.add("20");
strList.add("24");     
predicatesList.add(exp.in(strList));
 
criteriaQuery.where(predicatesList.toArray(new Predicate[predicatesList.size()]));

8.複合謂詞

Criteria Query也允許開發者編寫複合謂詞,通過該查詢可以爲多條件測試下面的查詢檢查兩個條件。首先,name屬性是否以M開頭,其次,employee的age屬性是否是25。邏輯操作符and執行獲得結果記錄。
?
1
2
3
4
5
6
criteriaQuery.where(
 criteriaBuilder.and(
  criteriaBuilder.like(employee.get(Employee_.name), "M%"),
  criteriaBuilder.equal(employee.get(Employee_.age), 25)
));
em.createQuery(criteriaQuery).getResultList();

連接查詢

在SQL中,連接跨多張表以獲取查詢結果,類似的實體連接通過調用 From.join 執行,連接幫助從一個實體導航到另一個實體以獲得查詢結果。 
Root的join方法返回一個 Join<Dept, Employee>類型(也可以是SetJoin,,ListJoin,MapJoin 或者 CollectionJoin類型)。

默認情況下,連接操作使用內連接,而外連接可以通過在join方法中指定JoinType參數爲LEFT或RIGHT來實現。

?
1
2
3
4
5
CriteriaQuery<Dept> cqDept = criteriaBuilder.createQuery(Dept.class);
Root<Dept> deptRoot = cqDept.from(Dept.class);
Join<Dept, Employee> employeeJoin = deptRoot.join(Dept_.employeeCollection);
cqDept.where(criteriaBuilder.equal(employeeJoin.get(Employee_.deptId).get(Dept_.id), 1));
TypedQuery<Dept> resultDept = em.createQuery(cqDept);

抓取連接

當涉及到collection屬性時,抓取連接對優化數據訪問是非常有幫助的。這是通過預抓取關聯對象和減少懶加載開銷而達到的。 
使用 criteria 查詢,fetch方法用於指定關聯屬性 
Fetch連接的語義與Join是一樣的,因爲Fetch操作不返回Path對象,所以它不能將來在查詢中引用。 
在以下例子中,查詢Dept對象時employeeCollection對象被加載,這不會有第二次查詢數據庫,因爲有懶加載。
?
1
2
3
4
5
CriteriaQuery<Dept> d = cb.createQuery(Dept.class);
Root<Dept> deptRoot = d.from(Dept.class);
deptRoot.fetch("employeeCollection", JoinType.LEFT);
d.select(deptRoot);
List<Dept> dList = em.createQuery(d).getResultList();

對應SQL: SELECT * FROM dept d, employee e  WHERE d.id = e.deptId

路徑表達式

Root實例,Join實例或者從另一個Path對象的get方法獲得的對象使用get方法可以得到Path對象,當查詢需要導航到實體的屬性時,路徑表達式是必要的。 
Get方法接收的參數是在實體元模型類中指定的屬性。 
Path對象一般用於Criteria查詢對象的select或where方法。例子如下:
?
1
2
3
CriteriaQuery<String> criteriaQuery = criteriaBuilder.createQuery(String.class);
Root<Dept> root = criteriaQuery.from(Dept.class);
criteriaQuery.select(root.get(Dept_.name));&nbsp;

參數化表達式

     在JPQL中,查詢參數是在運行時通過使用命名參數語法(冒號加變量,如 :age)傳入的。在Criteria查詢中,查詢參數是在運行時創建ParameterExpression對象併爲在查詢前調用TypeQuery,setParameter方法設置而傳入的。下面代碼片段展示了類型爲Integer的ParameterExpression age,它被設置爲24:
?
1
2
3
4
5
6
ParameterExpression<Integer> age = criteriaBuilder.parameter(Integer.class);
Predicate condition = criteriaBuilder.gt(testEmp.get(Employee_.age), age);
criteriaQuery.where(condition);
TypedQuery<Employee> testQuery = em.createQuery(criteriaQuery);
List<Employee> result = testQuery.setParameter(age, 24).getResultList();
Corresponding SQL: SELECT * FROM Employee WHERE age = 24;

排序結果

     Criteria查詢的結果能調用CriteriaQuery.orderBy方法排序,該方法接收一個Order對象做爲參數。通過調用  CriteriaBuilder.asc 或 CriteriaBuilder.Desc,Order對象能被創建。以下代碼片段中,Employee實例是基於age的升序排列。  
?
1
2
3
4
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class);
 Root<Employee> employee = criteriaQuery.from(Employee.class);
 criteriaQuery.orderBy(criteriaBuilder.asc(employee.get(Employee_.age)));
  em.createQuery(criteriaQuery).getResultList();
  對應  SQL: SELECT * FROM Employee ORDER BY age ASC

分組

CriteriaQuery 實例的groupBy 方法用於基於Expression的結果分組。查詢通過設置額外表達式,以後調用having方法。下面代碼片段中,查詢按照Employee類的name屬性分組,且結果以字母N開頭: 
CriteriaQuery<Tuple> cq = criteriaBuilder.createQuery(Tuple.class); 
?
1
2
3
4
5
6
Root<Employee> employee = cq.from(Employee.class);
  cq.groupBy(employee.get(Employee_.name));
  cq.having(criteriaBuilder.like(employee.get(Employee_.name), "N%"));
cq.select(criteriaBuilder.tuple(employee.get(Employee_.name),criteriaBuilder.count(employee)));
  TypedQuery<Tuple> q = em.createQuery(cq);
  List<Tuple> result = q.getResultList();
對應  SQL:    SELECT name, COUNT(*) FROM employeeGROUP BY name HAVING name like 'N%'

查詢投影

Criteria查詢的結果與在Critiria查詢創建中指定的一樣。結果也能通過把查詢根傳入 CriteriaQuery.select中顯式指定。Criteria查詢也給開發者投影各種結果的能力。 

使用construct()

使用該方法,查詢結果能由非實體類型組成。在下面的代碼片段中,爲EmployeeDetail類創建了一個Criteria查詢對象,而EmployeeDetail類並不是實體類型。 
?
1
2
3
4
5
CriteriaQuery<EmployeeDetails> criteriaQuery = criteriaBuilder.createQuery(EmployeeDetails.class);
  Root<Employee> employee = criteriaQuery.from(Employee.class);
  criteriaQuery.select(criteriaBuilder.construct(EmployeeDetails.class, employee.get(Employee_.name), employee.get(Employee_.age)));
  em.createQuery(criteriaQuery).getResultList();
  Corresponding SQL: SELECT name, age FROM employee<span style="white-space: normal;">&nbsp;</span>

返回Object[]的查詢

Criteria查詢也能通過設置值給CriteriaBuilder.array方法返回Object[]的結果。下面的代碼片段中,數組大小是2(由String和Integer組成)。 
?
1
2
3
4
CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);
  Root<Employee> employee = criteriaQuery.from(Employee.class);
  criteriaQuery.select(criteriaBuilder.array(employee.get(Employee_.name), employee.get(Employee_.age)));
  em.createQuery(criteriaQuery).getResultList();
對應  SQL: SELECT name, age FROM employee

返回元組(Tuple)的查詢

數據庫中的一行數據或單個記錄通常稱爲元組。通過調用CriteriaBuilder.createTupleQuery()方法,查詢可以用於元組上。CriteriaQuery.multiselect方法傳入參數,它必須在查詢中返回。 
?
1
2
3
4
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
   Root<Employee> employee = criteriaQuery.from(Employee.class);
   criteriaQuery.multiselect(employee.get(Employee_.name).alias("name"), employee.get(Employee_.age).alias("age"));
   em.createQuery(criteriaQuery).getResultList();
對應 SQL: SELECT name, age FROM employee 

結論

     Criteria查詢是一種以更加面向對象的方式查詢數據庫的方法、在本文中,我討論了JPA2中類型安全的Criteria查詢,以及對於理解Criteria查詢非常重要的元模型的概念。也討論了Criteria查詢中的各種API。

轉的,覺得寫的不錯,整理到一個文章裏了,地址:http://tanlan.iteye.com/blog/1101110


發佈了14 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章