JPA 2.0 动态类型安全查询

JPA 2.0版本(JSR 317)引入的关键特性之一就是Criteria API,所带来的好处就是:查询可以通过Java编译器来验证其正确性,而且还提供了能够在运行时动态地构建查询的机制。

使用Criteria API开发Java编译器能够检查其正确定的查询,从而减少运行时的错误,这种查询优于传统的基于字符串的Java Persistence Query Language(JPQL)查询。

基本概念

持久化单元(Persist Unit)就是关于一组Entity的命名配置。持久化单元是一个静态概念。
持久化上下文(Persist Context)就是一个受管的Entity实例的集合。每一个持久化上下文都关联一个持久化单元,持久化上下文不可能脱离持久化单元独立存在。持久化上下文是一个动态概念。尽管持久化上下文非常重要,但是开发者不直接与之打交道,持久化上下文在程序中是透明的,我们通过EntityManager间接管理它。
实体的生命周期:新实体(new),持久化态(managed),游离态(detached),删除状态(removed)。

JPQL查询缺陷

JPA 1.0引进了JPQL,这是一种强大的查询语言,在很大程度上导致了JPA的流行。不过,基于字符串并使用有限语法的JPQL存在一些限制。

1
2
3
4
5
// 例子: JPQL查询
EntityManager entityManager = ...;
String jpql = "select p from Person where p.age > 20";
Query query entityManager.createQuery(jpql);
List result = query.getResultList();

JPA 1.0的查询执行模型:

  1. JPQL查询被指定为一个字符串;
  2. EntityManager是构造一个包含给定JPQL查询字符串的可执行查询实例的工厂,在一组实体类与底层数据源之间进行 O/R 映射的管理;
  3. 查询执行的结果包含无类型的List的元素。

注意:该例子存在一个验证错误,代码能够通过编译,但是运行会失败,因为JPQL查询字符串的语法有误。
正确的JPQL语句为:String jpql = "select p from Person p where p.age > 20";

类型安全检查

Criteria API的最大优势之一就是禁止构造语法错误的查询。

1
2
3
4
5
6
7
8
9
// 例子: 类型安全查询
EntityManager entityManager = ...;
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> criteriaQuery = criteriaBuilder.createQuery(Person.class);
Root<Person> root = criteriaQuery.from(Person.class);
Predicate predicate = criteriaBuilder.gt(root.get(Person_.age), 20);
criteriaQuery.where(predicate);
TypedQuery<Person> typedQuery = entityManager.createQuery(criteriaQuery);
List<Person> result = typerQuery.getResultList();

JPA 2.0的Criteria API查询执行模型:

  1. EntityManager创建CriteriaBuilder的一个实例,CriteriaBuilder是CriteriaQuery的工厂。
  2. CriteriaBuilder工厂构造一个CriteriaQuery实例,CriteriaQuery被赋予泛型类型。泛型类型声明CriteriaQuery在执行时返回的结果的类型。
  3. 在CriteriaQuery实例上设置查询表达式,CriteriaQuery 封装了传统查询的子句:
    • 将CriteriaQuery设置为从Person.class查询。
    • Root是一个查询表达式,表示持久化实体的范围,实际上表示“对所有类型为T的实例计算这个查询”。类似于SQL查询的FROM子句。
  4. 构造Predicate,Predicate是计算结果为true或false的常见查询表达式形式。
    谓词由CriteriaBuilder构造,CriteriaBuilder不仅是CriteriaQuery的工厂,同时也是查询表达式的工厂。CriteriaBuilder提供了传统JPQL支持的所有查询表达式的API方法,这些方法签名能够检查查询表达式的正确性。例如: Predicate predicate = criteriaBuilder.gt(root.get(Person_.age), 20);
  5. criteriaBuilder.gt(root.get(Person_.age), 20);,方法gt()的第一个参数:root.get(Person_.age)是一个路径表达式Expression。路径表达式时通过一个或多个持久化属性从根表达式进行导航得到的结果。Person_.age将在Metamodel API中解释。
  6. 第6行,在 CriteriaQuery 上将谓词设置为其 WHERE 子句。
  7. 第7行,EntityManager 创建一个可执行查询,其输入为 CriteriaQuery。这类似于构造一个输入为 JPQL 字符串的可执行查询。

Metamodel元模型

Person_.age,它表示 Person 的持久化属性 age。Person.age 是 Person 类中的公共静态字段,Person_ 是静态、已实例化的规范元模型类,对应于原来的 Person 实体类。

元模型类描述持久化类的元数据。规范的元模型类是静态的,因此它的所有成员变量都被声明为静态的(也是 public 的)。Person_.age是静态成员变量之一。

元模型Metamodel和Java Reflection API比较

Person_元模型类是引用 Person 的元信息的一种代替方法。这种方法类似于经常使用的 Java Reflection API,但概念上有很大的不同。
问题:可以使用反射获得关于 java.lang.Class 的实例的元信息,但是不能以++编译器能够检查的方式++引用关于 Person.class 的元信息。
例如,使用反射时,使用Field field = Person.class.getField("age");来引用Person.class中的age字段。这种方式造成编译器能够顺利编译该代码,但是不能确定是否可以正常工作。

  • 反射不能实现JPA 2.0的类型安全查询API要实现的功能。
  • 类型安全查询 API 必须让您的代码能够引用 Person 类中的持久化属性 age,同时让编译器能够在编译期间检查错误。

解决办法:JPA 2.0 提供的解决办法通过静态地公开相同的持久化属性实例化名为 Person_ 的元模型类(对应于 Person)。编译器可以根据元模型实施属性的类型检查。
JPA Metamodel API 提供接口(比如 MappedSuperclassType、EntityType 和 EmbeddableType,SingularAttribute 和 PluralAttribute)来描述元模型类的类型及其属性。

简单的持久化实体类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 简单的持久化实体类
*/
@Entity
public class Person {
  @Id
  private long ssn;
  private string name;
  private int age;
  // public gettter/setter methods
}

简单实体的规范元模型

1
2
3
4
5
6
7
8
/**
* 简单实体的规范元模型
*/
public class Person_ {
  public static volatile SingularAttribute<Person,Long> ssn;
  public static volatile SingularAttribute<Person,String> name;
  public static volatile SingularAttribute<Person,Integer> age;
}

运行时作用域

将一组严格的可相互引用的类称为运行时作用域

Class实例的作用域

一般而言,可以将 Java Reflection API 的传统接口与专门用于描述持久化元数据的 javax.persistence.metamodel 的接口进行比较。要进一步进行类比,则需要对元模型接口使用等效的运行时作用域概念。java.lang.Class 实例的作用域由 java.lang.ClassLoader 在运行时划分。一组相互引用的 Java 类实例必须在 ClassLoader 作用域下定义。作用域的边界是严格 或封闭 的,如果在 ClassLoader L 作用域下定义的类 A 试图引用不在 ClassLoader L 作用域之内的类 B,结果将收到可怕的 ClassNotFoundException 或 NoClassDef FoundError。

JPA 运行时作用域

运行时作用域在 JPA 1.0 中称为持久化单元。持久化单元作用域的持久化实体在 META-INF/persistence.xml 文件的 子句中枚举。

在 JPA 2.0 中,通过 javax.persistence.metamodel.Metamodel 接口让开发人员可以在运行时使用作用域。Metamodel 接口是特定持久化单元知道的所有持久化实体的容器。

这个接口允许通过元模型元素的对应持久化实体类访问元模型元素。

1
2
3
4
// 例子
EntityManager entityManager = ...;
Metamodel metamodel = entityManager.getMetamodel();
EntityType<Person> entityType = metamodel.entity(Person.class);

感谢:http://www.th7.cn/Program/java/201501/356865.shtml