Java EE 6带来了JPA的最新版本 — JSR 317:Java持久化2.0,JPA 2.0带来了许多新特性和增强,包括:
1、对象/关系映射增强;
2、Java持久化查询语言增强;
3、一种新的基于标准的查询API;
4、支持悲观锁定。
对象/关系映射增强
JPA 1.0支持集合的映射,但这些集合只能包含实体,JPA 2.0增加了集合映射的基础数据类型,如String和Integer,以及嵌入式对象的集合。JPA中的嵌入式对象是一个不能存在于它自身的对象,而是作为父对象的一部分存在,即它的数据不是存在于它自己的表中,而是嵌入在父对象的表中。
JPA 2.0增加了两个支持新的集合映射的注解:@ElementCollection 和 @CollectionTable。使用@ElementCollection注解指定集合的嵌入式对象,这些集合是独立存储在集合表中的,使用@CollectionTable注解指定集合表的详细信息,如它包含的列。
下面是一个嵌入式类,表示了车辆的访问服务,它存储了访问的日期,描述和费用,此外,车辆可以配备一或多个可选功能,每个功能是FeatureType类型的一个枚举值。
- public enum FeatureType { AC, CRUISE, PWR, BLUETOOTH, TV, ... }
- @Embeddable
- public class ServiceVisit {
- @Temporal(DATE)
- @Column(name="SVC_DATE")
- Date serviceDate;
- String workDesc;
- int cost;
- }
枚举值和嵌入式对象可以在一个表示车辆服务历史的实体中使用,如:
- @Entity
- public class Vehicle {
- @Id int vin;
- @ElementCollection
- @CollectionTable(name="VEH_OPTNS")
- . @Column(name="FEAT")
- Set<FeatureType> optionalFeatures;
- @ElementCollection
- @CollectionTable(name="VEH_SVC")
- @OrderBy("serviceDate")
- List<ServiceVisit> serviceHistory;
- ...
- }
Vehicle实体中的第一对注解@ElementCollection 和 @CollectionTable指定FeatureType值存储在VEH_OPTNS集合表中,第二对注解@ElementCollection 和 @CollectionTable指定ServiceVisit嵌入式对象存储在VEH_SVC集合表中。
虽然在例子中没有显示,@ElementCollection注解有两个属性:targetClass 和 fetch。targetClass属性指定基础类或嵌入式类的类名,如果字段或属性是使用泛型定义的,那这两个属性是可选的,上面这个例子就是这样。Fetch属性是可选的,它指定集合是延后检索还是立即检索,使用javax.persistence.FetchType常量,值分别用LAZY和EAGER,默认情况下,集合是延后匹配的。
JPA 2.0中还有其它许多关于对象/关系映射的增强,例如,JPA 2.0支持嵌套式嵌入,关系嵌入和有序列表,也增加了新的注解增强映射功能,通过@Access注解更灵活地支持特定的访问类型,更多用于实体关系的映射选项,如对单向一对多关系的外键映射支持,通过@MapsId注解支持派生身份,支持孤体删除。
Java持久化查询语言增强
JPA 1.0定义了一个广泛的Java持久化查询语言(非正式简称为JPQL。有关JPQL查询的缺陷,可参考51CTO之前发布的这篇文章),使用它你可以查询实体和它们的持久化状态。JPA 2.0对JPQL做了大量改进,如现在可以在查询中使用case表达式。在下面的查询中,如果雇员的评分为1,则通过乘以1.1对雇员的薪水进行了增长,如果评分为2,则乘以1.05,其它评分则乘以1.01。
- UPDATE Employee e
- SET e.salary =
- CASE WHEN e.rating = 1 THEN e.salary * 1.1
- WHEN e.rating = 2 THEN e.salary * 1.05
- ELSE e.salary * 1.01
- END
JPA 2.0也为JPQL增加了大量新的运算符,如NULLIF和COALESCE,当数据库使用其它非空数据解码时,NULLIF运算符是非常有用的,使用NULLIF,你可以在查询中将这些值转换为空值,如果参数等于NULLIF,NULLIF会返回空值,否则返回第一个参数的值。
假设薪水数据保存在employee表中,数据类型为整数,却掉的薪水解码为-9999,下面的查询返回薪水的平均值,为了正确地忽略却掉的薪水,查询使用NULLIF将-9999转换为空值。
- SELECT AVG(NULLIF(e.salary, -99999))
- FROM Employee e
COALESCE运算符接收一串参数,从列表中返回第一个非空值,相当于下面的case表达式:
- CASE WHEN value1 IS NOT NULL THEN value1
- WHEN value2 IS NOT NULL THEN value2
- WHEN value3 IS NOT NULL THEN value3
- ...
- ELSE NULL
- END
假设employee表包括一个办公电话号码和家庭电话号码列,无电话号码的列使用空值表示。下面的查询返回每个雇员的姓名和电话号码,COALESCE运算符指定查询返回办公电话号码,但如果为空,则返回家庭电话号码,如果两者都为空,则返回一个空值。
JPA 2.0向JPQL增加的其它运算符是INDEX,TYPE,KEY,VALUE和ENTRY。INDEX运算符指定查询时的排序顺序,TYPE运算符选择一个实体的类型,将查询限制到一或多个实体类型,KEY,VALUE和ENTRY运算符是JPA 2.0中的泛化映射功能的一部分。使用KEY运算符提取映射键,VALUE运算符提取映射值,ENTRY运算符选择一个映射实体。
- SELECT Name, COALESCE(e.work_phone, e.home_phone) phone
- FROM Employee e
此外,JPA 2.0增加了选择列表、以及集合值参数和非多态查询中运算符的支持。
标准的API
JPA 2.0中引入的另一个重要特性是标准的API,利用这个API可以动态地构建基于对象的查询,本质上,这个标准API等价于面向对象的JPQL,使用它,你可以使用基于对象的方法创建查询,不再需要JPQL使用的字符串操作。
标准API是基于元模型的,元模型是一个提供了架构级关于持久化单元托管类的元数据的抽象模型, 元模型让你构建强类型的查询,它也允许你浏览持久化单元的逻辑结构。
通常,一个注解处理器使用元模型生成静态元模型类,这些类提供持久化状态和持久化单元托管类的关系,但你可以对静态元模型类编码。下面是一个实体实例:
- @Entity public class Employee {
- @Id Long Id;
- String firstName;
- String lastName;
- Department dept;
- }
下面是对应的静态元模型类:
- import javax.persistence.meta,model.SingularAttribute;
- import javax.persistence.meta,model.StaticMetamodel;
- @Generated("EclipseLink JPA 2.0 Canonical Model Generation"
- @StaticMetamodel(Employee.class)
- public class Employee_ {
- public static volatile SingularAttribute<Employee, Long> id;
- public static volatile SingularAttribute<Employee, String> firstName;
- public static volatile SingularAttribute<Employee, String> lastName;
- public static volatile SingularAttribute<Employee, Department> dept;
- }
此外,JPA 2.0元模型API允许你动态访问元模型,因此当你使用标准API时,既可以静态访问元模型类,也可以动态访问元模型类。标准API提供了更好的灵活性,既可以使用基于对象的方法,又可以使用基于字符串的方法导航元模型,这意味着你有四种使用标准API的方法:
1、静态使用元模型类
2、静态使用字符串
3、动态使用元模型
4、动态使用字符串
无论你使用哪种方法,通过构造一个CriteriaQuery对象定义一个基于标准API的查询时,使用一个工厂对象CriteriaBuilder构造CriteriaQuery,可以从EntityManager 或 EntityManagerFactory类中获得CriteriaBuilder。下面的代码构造一个CriteriaQuery对象:
- EntityManager em = ... ;
- CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
注意CriteriaQuery对象是泛型类型,使用CriteriaBuilder 的createQuery方法创建一个CriteriaQuery,并为查询结果指定类型。在这个例子中,createQuery 方法的Employee.class参数指定查询结果类型是Employee。CriteriaQuery对象和创建它们的方法是强类型的。
接下来,为CriteriaQuery对象指定一个或多个查询源,查询源表示查询基于的实体。你创建一个查询源,然后使用AbstractQuery接口的from()方法将其添加到查询。AbstractQuery接口是众多接口中的一员,如CriteriaQuery,From和root,它们都定义在标准API中。CriteriaQuery接口继承AbstractQuery接口的属性。
from()方法的参数是实体类或EntityType实体的实例,from()方法的结果是一个Root对象,Root接口扩展From接口,它表示某个查询的from子句中可能出现的对象。
下面的代码增加一个查询源到CriteriaQuery对象:
- CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
- Root<Employee> emp = cq.from(Employee.class);
当你向CriteriaQuery对象添加一个或多个查询源后,你访问元模型,然后构造一个查询表达式,你如何做取决于你是以静态方式提交查询还是以动态方式提交查询,以及是使用元模型还是字符串导航元模型。下面是一个使用元模型类静态查询的例子:
- cq.select(emp);
- cq.where(cb.equal(emp.get(Employee_.lastName), "Smith"));
- TypedQuery<Employee> query = em.createQuery(cq);
- List<Employee> rows = query.getResultList();
CriteriaQuery接口的select() 和 where()方法指定查询结果返回的选择项目。
注意,你使用EntityManager创建查询时,可以在输入中指定一个CriteriaQuery对象,它返回一个TypedQuery,它是JPA 2.0引入javax.persistence.Query接口的一个扩展,TypedQuery接口知道它返回的类型。
在元模型术语中,Employee_是对应于Employee实体类的规范化元模型类,一个规范化元模型类遵循JPA 2.0规范中描述的某些规则。例如,元模型类的名字来源于托管类,一般都是在托管类名字后面追加一个下划线“_”。一个规范化元模型是一个包含静态元模型类的元模型,这个静态元模型对应于实体,映射的超类,以及持久化单元中的嵌入式类。实际上,这个查询使用了规范化元模型。下面是一个完整的查询:
- EntityManager em = ... ;
- CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
- Root<Employee> emp = cq.from(Employee.class);
- cq.select(emp);
- cq.where(cb.equal(emp.get(Employee_.lastName), "Smith"));
- TypedQuery<Employee> query = em.createQuery(cq);
- List<Employee> rows = query.getResultList();
下面是使用元模型API查询的动态版本:
使用元模型API的标准查询提供了与使用规范化元模型相同的类型,但它比基于规范化元模型的查询更冗长。
- EntityManager em = ... ;
- CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
- Root<Employee> emp = cq.from(Employee.class);
- EntityType<Employee> emp_ = emp.getModel();
- cq.select(emp);
- cq.where(cb.equal(emp.get(emp_.getSingularAttribute("lastName", String.class)),"Smith"));
- TypedQuery<Employee> query=em.createQuery(cq);
- List<Employee> rows=query.getResultList();
Root的getModel()方法返回根对应的元模型实体,它也允许运行时访问在Employee实体中声明的持久化属性。
getSingularAttribute()方法是一个元模型API方法,它返回一个持久化的单值属性或字段,在这个例子中,它返回值为Smith 的lastName属性。下面是使用字符串的元数据导航查询的静态版本:
- EntityManager em = ... ;
- CriteriaBuilder cb = em.getCriteriaBuilder();
- CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
- Root<Employee> emp = cq.from(Employee.class);
- cq.select(emp);
- cq.where(cb.equal(emp.get("lastName"), "Smith"));
- TypedQuery query = em.createQuery(cq);
- List <Employee>rows = query.getResultList();
这个基于字符串的方法要相对容易使用些,但却失去了元模型具有的类型安全。
支持悲观锁
锁是处理数据库事务并发的一种技术,当两个或更多数据库事务并发地访问相同数据时,锁可以保证同一时间只有一个事务可以修改数据。
锁的方法通常有两种:乐观锁和悲观锁。乐观锁认为多个并发事务之间很少出现冲突,也就是说不会经常出现同一时间读取或修改相同数据,在乐观锁中,其目标是让并发事务自由地同时得到处理,而不是发现或预防冲突。两个事务在同一时刻可以访问相同的数据,但为了预防冲突,需要对数据执行一次检查,检查自上次读取数据以来发生的任何变化。
悲观锁认为事务会经常发生冲突,在悲观锁中,读取数据的事务会锁定数据,在前面的事务提交之前,其它事务都不能修改数据。
JPA 1.0只支持乐观锁,你可以使用EntityManager类的lock()方法指定锁模式的值,可以是READ或WRITE,如:
- EntityManager em = ... ;
- em.lock (p1, READ);
对于READ锁模式,JPA实体管理器在事务提交前都会锁定实体,检查实体的版本属性确定实体自上次被读取以来是否有更新,如果版本属性被更新了,实体管理器会抛出一个OptimisticLockException异常,并回滚事务。
对于WRITE锁模式,实体管理器执行和READ锁模式相同的乐观锁操作,但它也会更新实体的版本列。
JPA 2.0增加了6种新的锁模式,其中两个是乐观锁。JPA 2.0也允许悲观锁,并增加了3种悲观锁,第6种锁模式是无锁。
下面是新增的两个乐观锁模式:
1、OPTIMISTIC:它和READ锁模式相同,JPA 2.0仍然支持READ锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC。
2、OPTIMISTIC_FORCE_INCREMENT:它和WRITE锁模式相同,JPA 2.0仍然支持WRITE锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC_FORCE_INCREMENT。
下面是新增的三个悲观锁模式:
1、PESSIMISTIC_READ:只要事务读实体,实体管理器就锁定实体,直到事务完成锁才会解开,当你想使用重复读语义查询数据时使用这种锁模式,换句话说就是,当你想确保数据在连续读期间不被修改,这种锁模式不会阻碍其它事务读取数据。
2、PESSIMISTIC_WRITE:只要事务更新实体,实体管理器就会锁定实体,这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现更新失败几率较高时使用这种锁模式。
3、PESSIMISTIC_FORCE_INCREMENT:当事务读实体时,实体管理器就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改。
你也可以指定新的锁模式NONE,在这种情况下表示没有锁发生。
JPA 2.0也提供了多种方法为实体指定锁模式,你可以使用EntityManager的lock() 和 find()方法指定锁模式。此外,EntityManager.refresh()方法可以恢复实体实例的状态。
下面的代码显示了使用PESSIMISTIC_WRITE锁模式的悲观锁:
- // read
- Part p = em.find(Part.class, pId);
- // lock and refresh before update
- em.refresh(p, PESSIMISTIC_WRITE);
- int pAmount = p.getAmount();
- p.setAmount(pAmount - uCount);
在这个例子中,它首先读取一些数据,然后应用PESSIMISTIC_WRITE锁,在更新数据之前调用EntityManager.refresh()方法,当事务更新实体时,PESSIMISTIC_WRITE锁锁定实体,其它事务就不能更新相同的实体,直到前面的事务提交。
更多JPA 2.0的新特性
除了前面描述的增强和新特性外,JPA 2.0可以使用Bean验证自动验证实体,这意味着你可以在实体上指定一个约束,例如,实体中字段的最大长度为15,当实体持久化,更新或移除时自动验证字段,你可以在persistence.xml配置文件中使用<validation-mode>元素指定自动验证生效的周期。