Java EE 5平台引入了Java持久化API(Java Persistence API,JPA),它为Java EE和Java SE应用程序提供了一个基于POJO的持久化模块。JPA处理关系数据与Java对象之间的映射,它使对象/关系(O/R)映射标准化,JPA已经被广泛采用,已经成为事实上的O/R持久化企业标准。
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持久化查询语言,使用它你可以查询实体和它们的持久化状态。JPA 2.0对JPQL做了大量改进,如现在可以在查询中使用case表达式。在下面的查询中,如果雇员的评分为1,则通过乘以1.1对雇员的薪水进行了增长,如果评分为2,则乘以1.05,其它评分则乘以1.01。
UPDATE Employeee
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会返回空值,否则返回第一个参数的值。
SELECT AVG(NULLIF(e.salary, -99999)) FROM Employeee
COALESCE运算符接收一串参数,从列表中返回第一个非空值,相当于下面的case表达式
value1 IS NOT NULL THEN value1
WHEN value2 IS NOT NULL THEN value2 WHEN value3 IS NOT NULL THEN value3 ... ELSE NULL END
COALESCE运算符接收一串参数,从列表中返回第一个非空值,相当于下面的case表达式
SELECT Name, COALESCE(e.work_phone, e.home_phone) phone FROM Employeee
假设employee表包括一个办公电话号码和家庭电话号码列,无电话号码的列使用空值表示。下面的查询返回每个雇员的姓名和电话号码,COALESCE运算符指定查询返回办公电话号码,但如果为空,则返回家庭电话号码,如果两者都为空,则返回一个空值。
public class Employee { @Id Long Id; String firstName; String lastName; Department dept; }
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 volatileSingularAttribute<Employee, String> firstName; public static volatile SingularAttribute<Employee, String> lastName; public static volatile SingularAttribute<Employee, Department> dept; }
此外,JPA 2.0元模型API允许你动态访问元模型,因此当你使用标准API时,既可以静态访问元模型类,也可以动态访问元模型类。标准API提供了更好的灵活性,既可以使用基于对象的方法,又可以使用基于字符串的方法导航元模型,这意味着你有四种使用标准API的方法
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
注意CriteriaQuery对象是泛型类型,使用CriteriaBuilder 的createQuery方法创建一个CriteriaQuery,并为查询结果指定类型。在这个例子中,createQuery 方法的Employee.class参数指定查询结果类型是Employee。CriteriaQuery对象和创建它们的方法是强类型的。
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()方法指定查询结果返回的选择项目。
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查询的动态版本:
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();
使用元模型API的标准查询提供了与使用规范化元模型相同的类型,但它比基于规范化元模型的查询更冗长。
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锁锁定实体,其它事务就不能更新相同的实体,直到前面的事务提交。