Pro JPA2 第十一章(高级主题)
11.1 SQL查询
尽管在JPA2.0中对JP QL进行了增强,但仍然只包含由许多数据库供应商所支持功能的一个子集.内联视图(inline view,在FROM子句中的子查询),分层查询,访问存储过程,以及用于操纵日期和时间值的附件函数表达式只是JP QL所不支持的功能中的一些.
其次,虽然供应商可以提供提示以帮助优化JP QL表达式,但是在有些情况下实现一个应用查询所需性能的唯一方法是把JP QL替换为一个手动优化的SQL版本的查询.它可能是对持久化提供程序所生成的查询的一些简单重构,或者也可能是特定于供应商的版本.其中会利用特定于数据库的查询提示和功能.
当然,仅仅因为您可以使用SQL并不意味着您应该这么做.持久化提供程序已变得非常适合生成高性能的查询,JP QL的许多局限性经常可以在应用程序代码中得以解决.我们建议,在初始时应该尽可能地避免使用SQL,只有在必要的时候再引入它.这将使您的查询更容易在多个数据库之间移植.11.1.1 本地查询与JDBC
public class OrgStructureBean implements OrgStructure { private static final String ORG_QUERY = "SELECT emp_id,name,salary FROM emp START WITH manager_id = ? CONNECT BY PRIOR emp_id = manager_id"; @Resource DataResource hrDs; public List findEmployeeReportingTo(int managerId) { Connection conn = null; PreparedStatement sth = null; try { conn = hrDs.getConnection(); sth = conn.prepareStatement(ORG_QUERY); sth.setLong(1,managerId); Result rs = sth.executeQuery(); ArrayList<Employee> result = new ArrayList<Employee>(); while(rs.next()) { Employee emp = new Employee(); emp.setId(rs.getInt(1)); emp.setName(rs.getString(2)); emp.setSalary(rs.getLong(3)); result.add(emp); } return result; } catch (SQLException e) { throw new EJBException(e); } } }
将上例中的代码使用JPA表示
public class OrgStructureBean implements OrgStructure { private static final String ORG_QUERY = "SELECT emp_id,name,salary FROM emp START WITH manager_id = ? CONNECT BY PRIOR emp_id = manager_id"; @PersistenceContext(unitName = "EmployeeService") EntityManager em; public List findEmployeesReportingTo(int managerId) { return em.createNativeQuery(ORG_QUERY,Employee.class).setParameter(1,managerId).getResultList(); } }
11.1.2 定义和执行SQL查询
使用注解来定义一个命名本地查询@NamedNativeQuery( name="orgStructureReportingTO", query="SELECT emp_id,name,salary FROM emp START WITH manager_id = ? CONNECT BY PRIOR emp_id = manager_id", resultClass = Employee.class )
执行命名SQL查询
public class OrgStructureBean implements OrgStructure { @PersistenceContext(unitName="EmployeeService") EntityManager em; public List<Employee> findEmployeeReprotingTo(int managerId) { return em.createNamedQuery("orgStructureReportingTo",Employee.class).setParameter(1,managerId).getResultList(); } }
使用SQL的INSERT和DELETE语句
@StateLess @TranscationAttribute(TranscationAttributeType.REQUIRES_NEW) public class LoggerBean implements Logger { private static final String INSERT_SQL = "INSERT INTO message_log(id,message,log_dttm) VALUES(id_seq.nextval,?,SYSDATE)"; private static final String DELETE_SQL = "DELETE FROM message_log"; @PersistenceContext(unitName = "Logger") EntityManager em; public void logMessage(String message) { em.createNativeQuery(INSERT_SQL).setParameter(1,message).executeUpdate(); } public void clearMessageLog(){ em.createNativeQuery(DELETE_SQL).executeUpdate(); } }
11.1.3 SQL结果集映射
使用@SqlResultSetMapping注解来定义SQL结果集映射,可以把其防止在一个实体类上,由一个名称和一个或多个实体和列映射组成.@SqlResultSetMapping( name="employeeResult", entities=@EntityResult(entityClass=Employee.class) )
- 映射外键
外键不需要显式地映射为SQL结果集映射的一部分,当查询引擎尝试把查询结果映射到一个实体时,它同样为单值关联考虑外键列.
SELECT emp_id,name.salary,manager_id,dept_id,address_id FROM emp START WITH manager_id is null CONNECT BY PRIOR emp_id = manager_id
所有的MANAGER_ID,DEPT_ID和ADDRESS_ID列都映射到Employee实体关联的联结列.这个查询返回的Employee实例可以使用getManager(),getDepartment()与getAddress()等方法,并且将获得预期的效果.
多个结果映射
查询可能一次返回一个以上的实体,当两个实体之间存在一对一关系时,用这种方法很不错.SELECT emp_id,name,salary,manager_id,dept_id,address_id,id,street,city,state,zip FROM emp,address WHERE address_id = id
@SqlResuultSetMapping( name="EmployeeWithAddress", entities={@EntityResult(entityClass=Employee.class), @EntityResult(entityClass=Address.class)} )
映射列别名
SELECT emp_id,name,salary,manager_id,dept_id,address_id,address.id,street,city,state,zip FROM emp,address WHERE address_id = id
@SqlResultSetMapping( name="EmployeeWithAddress", entities={@EntityResult(entityClass=Employee.class, fields=@FieldResult(name="id",column="EMP_ID")),@EntityResult(entityClass=Address.class)} )
映射标量结果列
SQL查询不限于只返回实体结果SELECT e.name AS emp_name,m.name AS manager_name FROM emp e, emp m WHERE e.manager_id = m.emp_id (+) START WITH e.manager_id IS NULL CONNECT BY PRIOR e.emp_id = e.manager_id
非实体结果类型,称作标量结果类型(scalar result type),使用@ColumnResult注解进行映射.可以在这个映射注解的列特性上分配一个或多个列映射.唯一可用于列映射的特性是列名.
@SqlResultMapping( name="EmployeeAndManager", columns={@ColumnResult(name="EMP_NAME"), @ColumnResult(name="MANAGER_NAME")} )
来看一种更为复杂的.
SELECT d.id,d.name AS dept_name, e.emp_id,e.name,e.salary,e.manager_id,e.dept_id e.address_id,s.tot_emp,s.avg_sal FROM dept d,( SELECT * FROM emp e WEHRE EXISTS(SELECT 1 FROM emp WHERE amanger_id = e.emp_id) ) e , ( SELECT d.id,COUNT(*) AS tot_emp,AVG(e.salary) AS avg_sal FROM dept d,emp e WHERE d.id = e.dept_id(+) GROUP BY d.id ) s WHERE d.id = e.dept_id (+) AND d.id = s.id
@SqlResultSetMapping( name="DepartmentSummary", entities={ @EntityResult(entityClass=Department.class, fileds=@FieldResult(name="name",column="DEPT_NAME")), @EntityResult(entityClass=Employee.class) }, columns={@ColumnResult(name="TOT_EMP"), @ColumnResult(name="AVG_SAL")} )
- 映射外键
11.2 声明周期回调
- 11.2.1 声明周期事件
组成声明周期的事件类型分为4类:持久化,更新,删除和加载.它们实际上是数据级别的事件,对应插入,更新,删除和读取等数据库操作.除了加载之外,没种类型都有一个Pre事件和Post事件.在加载类别中,只有一个PostLoad事件,因为在尚未构建的实体只上存在PreLoad毫无意义,因此发生的生命周期事件完整的套件包括:PrePersist,PostPersist,PreUpdate,PostUpdate,PreRemove,PostRemove和PostLoad. 回调方法
定义回调方法可能存在几种不同的形式,其中最基本的方式是简单地在实体类上定义一个方法,指定方法为一个回调方法涉及两个步骤:根据给定的签名定义方法,以及以适当的生命周期事件注解对该方法进行注解.
所需的签名定义非常简单.回调方法可以有任何名称,但是必须有一个签名,其不带任何参数并且返回void类型.
回调方法可能不会抛出检查异常,因为回调方法的方法定义不允许包括throws子句.
不过他们可能会抛出运行时异常,如果在一个事务中抛出运行时异常,那么它们将会导致提供程序不进会放弃该事务中后续生命周期事件方法的调用,而且也会标记该事务为回滚.
通过生命周期事件注解进行注释可以指示一个方法为回调方法.相关的注解为:@Prepsesist,@PostPersist,@PreUpdate,@PostUpdate,@PreRemove,@PostRemove和@PostLoad.@Entity public class Employee { @Id private int id; @Transient private long syncTime; @PostPersist @PostUpdate @PostLoad private void resetSyncTime(){ syncTime=System.currentTimeMillis(); } public long getCachedAge(){ return System.currentTimeMillis() - syncTime; } }
实体监听器
如果不介意事件的回调逻辑是否包含在实体中,那么实体中的回调方法是合适的.但是,如果您想要把时间处理行为放到实体类之外的另一个不同的类时会如何呢?
一个实体监听器不是一个实体,它是一个类,在其上可以定义一个或多个生命周期回调方法.由实体的生命周期时间来调用.然后,类似于实体上的回调方法,对于每种时间类型,在每个监听器类上只可以注解一个方法.不过,一个实体上可以应用多个事件监听器.
实体监听器类必须是无状态的,意味着它们不应当生命任何字段.单个实例可以在多个实体实例之间共享,甚至可能同时为多个实体实例调用.为实体附件实体监听器
通过使用@EntityListeners注解,实体可指定其生命周期事件应该通知的实体监听器.可以在注解中列出一个或多个实体监听器.@Entity @EntityListeners({EmployeeDebugListeners.class,NameValidator.class}) public class Employee implements NamedEntity { @Id private int id; @Transient private long syncTime; public String getName(){ return name; } @PostPersist @PostUpdate @PostLoad private void resetSyncTime() { syncTime = System.currentTimeMillis(); } public long getCachedAge(){ return System.currentTimeMillis() - syncTime; } } public interface NamedEntity { public String getName(); } public class NameValidator { static final int MAX_NAME_LEN = 40; @PrePersist public void validate(NamedEntity obj) { if(obj.getName().length() > MAX_NAME_LEN) throw new ValidationException("Identifire out of range"); } } public class EmployeeDebugListener { @PrePersist public void prePersist(Employee emp) { System.out.println("Persist on employee id :" +emp.getId()); } @PreUpdate public void preUpdate(Employee emp) { } @PreRemove public void preRemove(Employee emp) { } @PostLoad public void postLoad(Employee emp) { } }
- 默认的实体监听器
当声明了默认的实体监听器列表时,将会根据他们在生命中所列出的顺序进行遍历,并且调用包含了为当前时间注解或声明了方法的每个监听器.默认的实体监听器总是会在一个给定实体的@EntityListeners注解所列出的任何实体监听器之前得到调用
通过使用@ExcludeDefaultListeners注解,任何实体都可以选择排除应用于它的默认实体监听器.当一个实体采用此注解进行注解时,对于这种类型的实体实例,不会调用为生命周期时间声明的默认监听器.
- 11.2.1 声明周期事件
11.3 验证
验证有一个注解模型和动态的API,可以从任何层以及几乎任何bean上对他们进行调用.约束注解可以防止在需要验证的对象字段或属性之上,设置在对象类本上之上,而后当验证程序运行时,将会检查这些约束.11.3.1 使用约束
public class Employee { @NotNull private int id; @NotNull(message="Employee name must be specified") @Size(max=40) private String name; @Past private Date startDate; }
完整约束列表:
约束 特性 描述 @Null null 元素必须为null @NotNull null 元素必须不是null @AssertTrue null 元素必须为true @AssertFalse null 元素必须为false @Min long value() 元素必须具有大于等于最小值的值 @Max long value() 元素必须具有一个小于等于最大值的值 @DecimalMin String value() 元素必须具有大于等于最小值的值 @DecimalMax String value() 元素必须具有一个小于等于最大值的值 @Size int min()
int max()元素必须具有一个在指定的限制之间的值 @Digits int integer()
int fraction()元素必须是一个在指定的范围内的数字 @Past null 元素必须是在过去的一个日期 @Fature null 元素必须是在未来的一个日期 @Pattern String regexpr()
Flag[] flags元素必须与指定的正则表达式匹配. 11.3.2 调用验证
public class EmployeeOperationsEJB implements EmployeeOperations { @Resource Validator validator; public void newEmployee(Employee emp) { validator.validate(emp); } }
在一个非容器环境中,可以从javax.validation.ValidatorFactory中获得一个Validator实例.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator();
11.3.3 验证组
同一个对象可能需要再不同的时间以多个不同的约束集进行验证,为了实现这个目的,将创建单独的验证组,并指定约束术语哪个组或哪些组.public interface FullTime extends Default {} public interface PartTime extends Default {}
除了类本身之外,在组类中并没有什么值,因此只是把它们定义为简单的接口.
public class Employee { @NotNull private int id; @NotNull @Size(max=40) private String name; @NotNull(groups=FullTIme.class) @Null(groups=PartTime.class) private long salary; @NotNull(groups=PartTime.class) @null(groups=FullTime.class) private double hourlyWage; }
指定多个组
public class Employee { @NotNull(groups={FullTime.class,PartTime.class}) private int id; @NotNull(groups={FullTime.class,PartTime.class}) @Size(groups={FullTIme.class,PartTime.class},max=40) private String name; }
11.3.4 创建新的约束
验证最有价值的方面之一是能够为指定的应用程序添加新的约束,甚至能够在应用程序之间共享.
每个新的约束都由两部分组成:注解定义以及实现或验证类.约束注解
@Constraint(validatedBy={EvenNumberValidator.class}) @Target({METHOD,FIELD}) @Retention(RUNTIME) public @interface Even { String message() default "Number must be even"; Class<? [] groups default {}; Class<? extends ContraintPayload>[] payload() default {}; boolean includeZero() default true; }
正如上例所示,@Retention策略必须设置为RUNTIME,而且@Target必须包含至少一个或多个TYPE,FIELD,METHOD或者ANNOTATION_TYPE.还必须使用@Constraint元注解来注释定义.
每个约束注解中有三个元素是强制性的,message,groups和payload.约束实现类
对于每个约束注解必须有一个或多个约束实现类,每个类必须实现javax.validation.ConstraintValidator接口和提供验证检查值的逻辑.public class EvenNumberValidator implements ContraintValidator<Even,Integer> { boolean includesZero; public void initalize(Even constraint) { includesZero = constraint.includeZero(); } public boolean isValid(Integer value,ConstraintValidatorCntext ctx) { if (value == null) return true; if (value == 0) return includesZero; return value % 2 == 0; } }
11.3.5 JPA中的验证
当验证JPA实体时,JPA提供程序需要一个特定的集成,对于这种集成存在几方面的理由.
第一个也是最重要的,一个实体可能具有延迟加载特性,并且因为验证程序不会依赖于JPA,或者没有关于JPA的知识,所以即使当一个特性未加载时它也不会知道.验证过程可能无意中会导致把整个对象图加载到内存中.另一种情况是如果验证发生在客户端的JPA实体上,那么未加载的特性甚至不可加载.在这种情况下,验证将产生一个异常.虽然不至于与加载整个对象图一样糟糕,但是显然是不可取的.
JPA集成的最实际理由是,通常我们希望在特定的生命周期阶段自动地调用验证.@Entity public class Employee { @Id @NotNull private int id; @NotNull @Size(max=40) private String name; @Past private Date startDate; @Embedded @Valid private EmployeeInfo info; @ManyToOne private Address address; } @Embeddable public class EmployeeInfo { @Past private Date dob; @Embedded private PersionInfo spouse; }
- 11.3.6 启用验证
如果在JPA配置级别没有重写设置,那么当验证提供程序处于类路径上时,验证默认是开启的,为了显式地控制是否应该启用或禁用验证,有两种可能.
* persistence.xml文件中的validation-mode元素.该元素可以设置为三种可能的值
* AUTO 当验证提供程序在类路径上存在时,启用验证(默认).
* CALLBACK 如果没有可用的验证提供程序,那么启用验证并抛出一个错误.
* NONE 禁用验证
* javax.persistence.validation.mode 持久化属性.此属性可以在传递给createEntityManagerFactory()方法的Map中指定,并且如果存在的话,那么重写validation-mode设置.可能的值包括等价于validation-mode值的字符串. 11.3.7 设置生命周期的验证组
默认情况下,每个PrePersist和PreUpdate生命周期事件都将处罚受影响实体之上的验证,其紧跟着事件回调,并使用Default验证组.为了更改在三种不同的生命周期事件类型中需要验证的组,可以指定下列任何属性,作为persistence.xml文件中propeties一节的属性,或者在传递到createEntityManagerfactory()的Map中指定.- javax.persistence.validation.group.pre-persist 设置在PrePersist时验证组
- javax.persistence.validation.group.pre-update 设置在PreUpdate时验证组
javax.persistence.validation.group.pre-remove 设置在PreRemove时验证组
@Entity public class Employee { @Id @NotNull private int id; @NotNull @Size(max=40) private String name; @Past private Date startDate; @Size(groups=Remove.class,min=0,max=0) private long vacationDays; }
- 11.4 并发性
- 11.4.1 实体操作
托管实体术语一个单一的持久化上下文,在任何给定的时间都不应该由一个以上的持久化上下文进行管理.然后,这是应用程序的责任,不一定通过持久化提供程序强制如此.把相同的实体合并到两个不同的开放持久化上下文中可能会产生未定义的结果.
实体管理器和管理它们的持久化上下文不打算被一个以上的并行执行线程进行访问.应用程序不能期望它是同步的,同事需要负责确保它保留在获得它的线程中. - 11.4.2 实体访问
虽然实体是由一个持久化上下文管理,但是应用程序不可以直接从多个线程来访问它,但是.当实体分离时,应用程序可以选择允许并发地访问它们.如果这样做,那么必须通过在实体上编码的方法来进行同步.但是,不建议并发访问实体状态,因为实体模型不能很好地适用于并发模式.最好说简单地复制实体并把复制了的实体传递给其他线程进行访问,同时当需要持久化它们时,合并所有的更改并返回到持久化上下文中.
- 11.4.1 实体操作
11.5 刷新实体状态
当我们知道或怀疑在数据库中存在更改,而且它们没有在托管实体中出现的情况下,可以使用EntityManager 接口的refresh()方法.刷新操作只在一个实体为托管时应用,因为当处于分离时,通常只需要发出一个查询,以从数据库中获取一个实体的更新版本.@TransactionAttribute(TransactionType.NOT_SUPPORTED) public class EmployeeServiceBean implements EmployeeService { public static final long REFRESH_THRESHOLD = 300000; @PersistenceContext(unitName="EmployeeService",type=PersistenceContextType.EXTENDED) EntityManager em; Employee emp; long loadTime; public void loadEmployee(int id) { emp = em.find(Employee.class,id); if(emp==null) throw new IllegalArgumentException("Unknown employee id: " +id); loadTime = System.currentTimeMillis(); } public void deductEmployeeVacation(int days) { refreshEmployeeIfNeeded(); emp.setVacationDays(emp.getVacationDays() - days); } public void adjustEmployeeSalary(long salary) { refreshEmployeeNeeded(); emp.setSalary(salary); } @Remove @TransactionAttribute(TransactionAttributeType.REQUIRED) public void finished(){} private void refreshEmployeeIfNeeded(){ if((System.currentTimeMillis() - loadTime) > REFRESH_THRESHOLD) { em.refresh(emp); loadTime = System.currentTimeMillis(); } } }
这个例子的事务特性是NOT_SUPPORTED,这就意味着当通过bean的各种业务方法对Employee实例进行更改时,这些更改不会被写入到数据.只有当电泳finished()方法时才会写入.
11.6 锁定
锁定(locking)位于许多不同级别的层面,而且对于JPA而言是内在的.它将会用于并假设位于整个API和规范的不同位置.11.6.1 乐观锁
乐观锁定遵循这样的原理:存在一个好的时机,其中更改实体的事务是在间隔期间唯一会时机更改实体的事务.把它转换成决策,即是直到更改会实际作用到数据库时才获取实体上的锁定,这通常发生在事务结束时.
在书信时或事务结束阶段,当把数据实际发送到数据库以对其进行更新时,获取实体锁定并检查数据库中的数据.刷新事务必须查看在该事务读取和改变实体的时间间隔内,是否存在任何其他的事务已经提交了实体的更改.如果发生了更改,那么意味着刷新事务具有未包含这些更改的数据,因此不应该把它自己的更改写入到数据库,以免重写中间事务所形成的更改.这种情况下,它必须回滚事务,并报出一个称为OptimisticLockException的特殊异常.版本控制
您可能会问提供程序如何知道某人在提交事务读取实体的中间时间你诶对实体进行了更改,答案是提供程序为实体维护了一个版本控制系统.那么为了这么做,实体必须声明一个专用的持久化字段或实训个,以存储在事务中获得的实体的版本号.这个版本号还必须存储到数据库中.当回到数据库更新实体时提供程序可以检查该实体在数据库中的版本,以检查它是否匹配先前获取的版本.@Entity public class Employee { @Id private int id; @Version private int version; }
关于版本字段的两个注意事项:
无论是在托管实体或是在数据库中,他们都不保证会作为批量更新操作的一部分进行更新.有些供应商提供了在批量更新时版本字段自动更新的支持,但是这是不可移植的.对于那些不支持自动版本更新的供应商,实体版本可以作为UPDATE语句的一部分进行手动更新:
UPDATE Employee e SET e.salary = e.salary + 1000,e.version = e.version + 1 WHERE EXISTS (SELECT p FROM e.projects p WHERE p.name = 'Release2')
- 值得注意的第二点是,只有当修改非关系字段或游泳外键关系的字段时,版本字段才会自动更新,如果你想要一个非所有的,集合值的关系造成实体版本的更新,那么这种方式就不太合适了,不过下面将介绍.
高级乐观锁模式
默认情况下,JPA假定在ANSI/ISO SQL规范中定义的和在事务隔离用于中所熟知的是读提交(Read Committed)隔离.这个标准的隔离级别仅仅是保证,在事务内部进行的任何更改,直到更改事务已提交之后才是对其他事务可见的,为了提供面对交叉写时的附加数据一致性检查,使用版本锁定的正常执行会与读提交隔离一起工作.为了满足比这个锁定更严格的锁定约束,需要使用一种附加的锁定策略,为了保证可移植,这些策略只可以用于具有版本字段的实体.- EntityManager.lock() 用于锁定已经在持久化上下文中的对象的显式方法.
- EntityManager.refresh() 允许传入一个锁定模式并应用于正在刷新的持久化上下文中的对象.
- EntityManager.find() 允许传入一个锁定模式并应用于正在返回的对象.
- Query.setLocakMode() 设置在执行查询期间有效的锁定模式.
每个EntityManager方法都必须在一个事务内调用,虽然可以在任何时间调用Query.setLocakMode()方法,但是必须在事务上下文中执行一个具有锁定模式集的查询.
因为是在已存在于持久化上下文中的对象上调用lock()和refresh(,所以取决于特定的实现,除了仅仅把对象标志位锁定之外,可能会可能不会采取任何操作.乐观读取锁定
下一个级别的事务隔离术语是可重复读(Repeatable Read),其防止所谓的不可重复读取的异常.当在同一事务中对相同的数据事务查询两次时,与第一次返回的数据相比,第二次查询将返回数据的不同版本,因为另一个事务在中间时间修改了它.换言之,可重复读隔离级别意味着一旦一个事务访问数据,且另一个事务修改了数据,就必须组织至少一个事务的提交.
为了乐观地锁定一个实体,可以把LocakModeType.OPTIMISTIC锁定模式传递给锁定方法之一.得到的锁定将保证,获得实体读取锁定的事务和其他任何试图改变那个实体实例的事务都不会成功.至少有一个将会失败,但是和数据库隔离级别一样,哪一个会失败取决于实现.
读取锁定实现的方式完全取决于提供程序.即使称之为乐观读取锁定,一个提供程序也可以选择以非常严格的方式实现,且获得实体上的即时写入锁定,在这种情况下,任何其他试图更改实体的事务都将失败或阻塞,直到锁定事务完成.然儿,提供程序通常将会乐观地读取锁定(read-lock)对象,这意味着当调用锁定方法时,提供程序实际上将不会因为锁定而去访问数据库,相反,它将会等到事务结束,并且在提交时重新读取实体,以检查自从上次在事务中被读取只有,实体是否已经发生改变.如果没有改变,那么读取锁定获得成功,但是如果实体已经发生变化,那么读取失败并且事务将会回滚.
关于这种乐观形式的读取锁定实现的一个推论是,,在事务的哪个时刻调用锁定方法实际上无关紧要.可以恰好在知道提交之前调用它,这将会产生完全相同的结果.所有的方法所做的都是标识实体在提交时重新读取,在事务期间,它实际上不管住实体何时被添加到这个列表中,因为实际的读取操作直到事务结束时才会发生.您可以把lock()或者带锁定的refresh()调用看做是生效时刻,因为它是读取版本并记录到托管实体的时刻.
使用这种锁定的典型情况是一个实体因为一致性而固有的依赖于一个或多个其他实体.在实体之间经常会存在关系,但并非总是如此.public class EmployeeServiceBean implements EmployeeService { @PersistenceContext(unitName="EmployeeService") EntityManager em; public SalaryReport generateDepartmentSalaryReport(List<Integer> deptIds){ SalaryReport report = new SalaryReport(); long total = 0; for(Integer deptId : deptIds){ long deptTotal = totalSalaryDepartment(deptId); report.addDeptSalaryLine(deptId,deptTotal); total += deptTotal; } report.addSummarySalaryLine(total); return report; } protected long totalSalaryInDepartment(int deptId) { long total = 0; Department dept = em.find(Department.class,deptId); for(Employee emp:dept.getEmployees()){ total += emp.getSalary(); } return total; } public void changeEmployeeDepartment(int deptId,int empId) { Employee emp = em.find(Employee.class,empId); emp.getDepartment().removeEmployee(emp); Department dept = em.find(Department.class,deptId); dept.addEmployee(emp); emp.setDepartment(dept); } }
这段代码看起来好像并没有什么问题,但是问题在于我们在操作期间不会锁定任何员工对象以避免其被修改的事实.我们发出了多个查询,容易造成在相同的对象中查看到不同的状态,这是不可重复读的现象.
使用乐观读取锁定protected long totalSalaryInDepartment(int deptId){ long total = 0; Department dept = em.find(Department.class,deptId); for(Employee emp:dept.getEmployees()) { em.lock(emp,LockModeType.OPTIMISTIC); total += emp.getSalary(); } }
我们提到运行实现即时锁定或者直到事务的结束时才延迟获取锁定.大多数主要的实现会延迟锁定直到提交时间,通过这么做,将会提供更好的性能和可扩展性而不用牺牲任何语义.
乐观写入锁定
第二种级别的高级乐观锁定称为乐观写入锁定(optimistic write lock),凭借其名称可正确地提示我们实际上正在锁定待写入的对象.写入锁定不仅保证所有乐观读取锁定所能做的,而且也承诺事务中版本字段的递增,无论用户更新实体与否.如果在这个事务提交之前,另一个事务也试图修改相同的实体,那么它将会保证提供一个乐观锁定失败.这相当于强迫实体更新以触发版本号增加,并且这就是把此选项为OPTIMISTIC_FORCE_INCREMENT的原因.显而易见的结论是,如果实体是由应用程序来更新或删除,那么它从不需要显式地对其写入锁定,而且如果无论如何都要对它进行写入锁定,那么最好的情况将是冗余的,而最坏的情况可能会导致一次附加的更新,这取决于实现.
回顾一下,当更改作用到一个非所有(non-owned)的关系上时,通常不会发生对版本列的更新.由于这个原因,使用OPTIMISTIC_FORCE_INCREMENT的常见情况是保证跨实体关系变化的一致性,此时在对象模型中,实体关系的指针会发生变化,但在数据模型中,实体表中不会更改任何列.
操作未发现更改和没有发生锁定异常的第二个原因在于,对实体的写入操作没有实际发生,所以没有理由对实体进行任何更新.
解决方案是使用OPTIMISTIC_FORCE_INCREMENT选项.这将会导致如果不了解关系的更新而进行更改,那么在其他持久化上下文中的任何更新都会失败.public class EmployeeManagementBean implements EmployeeManagement { @PersistenceContext(unitName="EmployeeService",type=PersistenceContextType.EXTENDED) EntityManager em; public void addUniform(int id, Uniform uniform) { Employee emp = em.find(Employee.class ,id); em.lock(emp,LockModeType.OPTIMISTIC_FORCE_INCREMENT); emp.adduniForm(uniform); uniform.setEmployee(emp); } } public class cleaningFeeManagementBean implements CleaningFeeManagement { static final Float UNIFORM_COST = 4.7f; @PersistenceContext(unitName="EmployeeService",type=PersistenceContextType.EXTENDED) EntityManager em; public void calculateCleaningCost(int id){ Employee emp = em.find(Employee.class,id); Float cost = emp.getUniforms().size() * UNIFORM_COST; emp.setCost(emp.getCost() + cost); } }
从乐观失败中恢复
乐观失败意味着修改的一个或多个实体不足够新,从而不允许记录它们的更改.正在修改的实体版本是过时的,而且在数据库中已经对实体进行了更改,因此将抛出一个OptimisticLockException异常.
一旦一个方法已经完成并作出了更改,容器就将试图提交事务,在这么做的过程中,持久化提供程序将从是无辜安力奇获得事务同步通知,从而把它的持久化上下文刷新到数据库中.当提供程序尝试写入时,它会在版本号检查期间发现,其中一个对象自从此进程读取之后已经被另一个进程所修改,因此它会抛出一个OptimisticLockException异常.问题在于容器会把这种异常与其他的任何运行时异常以同样的方式对待.容器会简单地把该异常记录日志,而且抛出一个EJBException异常.
关于这个问题的解决方案是,当我们恰好在准备好完成该方法之前,在容器托管的事务内部执行一个flush操作,这回强制写入数据库,并且仅在方法结束时锁定自愿,从而最小化对并发性的影响.它还会使得当我们在控制之下时能够处理乐观失败,没有容器会干涉和可能处理异常.如果我们从flush()调用中获得一个异常,那么可以抛出一个调用者能够识别的应用程序异常.public class EmployeeServiceBean implements EmployeeService { @PersistenceContext(unitName="EmployeeService") EntityManager em; public void deductEmployeeVacation(int id, int days) { Employee emp = em.find(Employee.class,id); emp.setVacationDays(emp.getVacationDays() - days); flushChanages(); } public void adjustEmployeeSalary(int id , long salary) { Employee emp = em.find(Employee.class,id); emp.setSalary(salary); flshChanages(); } protected void flushChanges() { try { em.flush(); } catch(OptimisticLockException optlockEx) { throw new ChangeCollisionException(); } } } @ApplicationException public class ChangeCollisionException extends RuntimeException { public ChangeCollisoinException() { super(); } } @TransactionManager(TransactionManagementType.BEAN) public class EmpServiceClientBean implements EmpServiceClient { @Autowired EmployeeService empService; public void adjustVacation(int id, int days) { try { empService.deductEmployeeVacation(id,days); } catch (ChangeCollisionException ccEx) { empService.deductEmployeeVacation(id,days); } } }
11.6.2 悲观锁定
悲观锁定意味着在一个或多个对象上立即获得一个锁定,而不是乐观地等到提交阶段,希望数据自从上次读取之后再数据库中没有发生变化.悲观锁定是同步的,因为当锁定调用返回时,将保证在当前事务已经完成并且释放锁定之前,锁定的对象不会被另一个事务修改.因此,悲观锁定不会因为并发更改而导致事务失败.
事实上它们经常会限制应用程序的可扩展性,因为不必要的锁定将会串行化许多能够轻易地并行发生的操作.现实情况中,只有少量的应用程序会真正想要悲观锁定,而且那些确实想要的应用程序仅在有限的查询子集上需要.规则是当您认为需要悲观锁定时,请三思而后行.如果在相同的对象上并发写入的可能性很高,同事乐观失败出现的可能性也很高,那么在这种情况下,您可能需要悲观锁定,这是由于重试的代价过高,因此您最好悲观地完成锁定.如果您绝对不能重试事务,并且愿意牺牲一些可扩展性,那么也可能会导致您使用悲观锁定.悲观锁定模式
悲观写入锁定
public class VacationAccrualBean implements VacationAccrualService { @PersistenceContext(unitName="Employee") EntityManager em; public void accrueEmployeeVacation(int id) { Employee emp = em.find(Employee.class,id); EmployeeStatus status = emp.getStatus(); double accruedDays = calculateAccrual(status); if (accruedDays > 0) { em.lock(emp.LockModeType.PESSIMISTIC_WRITE); emp.setVacationDays(emp.getVacationDays() + accruedDays); } } }
如果我们拥有一个版本字段,那么即使使用了悲观锁定,乐观锁定检查也总会发生,它将捕获过时的版本,同时将获得一个OptimisticLockException异常.解决方案是提前在find()方法中获取针对员工的锁定或者调用一次refresh()锁定.
public void accrueEmployeeVacation(int id) { Employee emp = em.find(Employee.class,id); EmployeeStatus status = emp.getStatus(); double accruedDays = calculateAccrual(status); if (accruedDays > 0) { em.refresh(emp,LockModeType.PESSIMISTIC_WRITE); if(status != emp.getStatus()) { accruedDays = calculateAccrual(emp.getStatus()); } if(accruedDays > 0) { emp.setVacationDays(emp.getVacationDays() + accruedDays) } } }
- 悲剧读取锁定
有些数据库支持无需获取写入锁定而获得可重复读隔离的锁定机制.当不期望写入实体时,PESSIMISTIC_READ模式可用于悲观地实现可重复读语义.
当一个使用悲观读取锁定所锁定的实体最终被修改时,这个锁定将会升级为一个悲观写入锁定.但是,升级直到实体刷新时才会发生,所以它的效果有限,因为直到事务提交时才会抛出一个失败的锁定获取异常,导致这个锁定相当于一个乐观锁定. - 悲观的强制增量锁定
另一个模式是PESSIMISTIC_FORCE_INCREMENT模式,它的目标是获取悲观锁定的场景,即使只是读取实体.这种模式也将会递增锁定实体的版本字段,无论它是否已经发生更改.这与悲观读取锁定和乐观吸入锁定的场景有点重叠.强制版本字段的递增可以通过关系维护一定程度的版本一致性
- 悲观的范围
“版本控制”一节提到了更改任何所拥有的关系都将会导致所有方实体的版本字段的更新.
当它采用悲观锁定时,获取其他实体表中实体的排它锁会增加死锁发生的可能性.为了避免这种情况,悲观锁定查询的默认行为是,如果表没有映射到实体,那么不会获取它的锁定.在某些人需要获取该锁定作为一个悲观查询一部分的情况下,存在一个额外的属性以启用这种行为.可以在查询中设置javax.persistence.lock.scope属性,将其值设置为PessimisticLockScope.EXTENDED.当设置后,单向关系的目标表,元素集合表和拥有的多对多关系的联接表都会有其相应的行被悲观锁定.
通常应该避免这个属性,除非锁定这些表是绝对必要的,如不能用任何其他方式方便地锁定联接表.启用此属性以确保不会导致死锁的先决条件是使用严格的顺序,并充分了解映射和操作的顺序. 悲观超时
虽然JPA没有标准地描述提供程序必须如何支持悲观锁定获取的超时模式,但是JPA确实定义了一个提供程序可以使用的提示.虽然不是强制性的,大师主要的JPA提供程序可能偶读支持javax.persistence.lock.timeout提示.其值可以是”0”,意味着不需要阻塞以等待锁定;或者是一些整数,其描述了等待锁定的毫秒数.可以把它传递给任何接受锁定模式以及属性或提示Map的EntityManager API方法:Map<String,Object> props = new HashMap<>(); props.put("javax.persistence.lock.timeout",5000); em.find(Employee.class,42,LockModeType.PESSIMISTIC_WRITE,props);
也可以在查询上把它设置为提示:
TypedQuery<Employee> q = em.createQuery("SELECT e FROM EMPLOYEE e WHERE e.id = 42",Employee.class); q.setLockMode(LockModeType.PESSIMISTIC_WRITE); q.setHint("javax.persistence.lock.timeout",5000);
但是并不存在为指定超时提示时的默认行为,在提供程序和数据库之间,默认行为可能会是阻塞,”无等待”或者也可能存在一个默认超时.
- 从悲观失败中恢复
当由于不能再查询中获取锁定而发生失败时,或者由于任何数据库所认为的对事物非致命的原因而发生失败时,将抛出一个LockTimeoutException异常,调用方可以捕获它,而且如果它希望出现的话,可以简单地重复调用.然后,如果失败严重到足以导致事务失败,那么将会抛出一个PessimisticLockException异常,并标记事务为回滚.
11.7 缓存
缓存是一个相当广泛的术语,通常意味着将内容保存在内存中用于以后快速访问.- 11.7.1 通过层排序
第一层实际上位于应用程序层.在缓存中可以写入任何应用程序所想要的任意数量的实体,只需简单地通过引用而保持它们.应该意识到实体可能在某个时刻会变成分离的实体,它们在应用程序空间中待的时间越长,它们将月可能变得过时.应用程序缓存自有其用处,但是通常都会令人沮丧,因为在任何后续的JPA查询结果中或持久化上下文中从未包括缓存的实体实例.
另外,可以把一个实体管理器所引用的持久化上下文认为是一个缓存,因为其保持了所有托管实体的引用.如果在硬件体系结构中所做的一样,如果把缓存的不同层分类成不同的级别,那么将把持久化上下文成为第一个真正的JPA缓存级别,因为它是持久化提供程序可以从中检索内存实体的第一个位置.当在一个事务范围内的实体管理器中运行时,吃菊花上下文可以被称为一个事务缓存,因为它只是围绕着事务的持续时间.当实体管理器是一个扩展类型时,它的持久化上下文缓存的生命周期会更长,只有当已经清楚或关闭实体管理器时才会消失.
某些人会把实体管理器工厂中的缓存称为二级缓存,不过显然这个名字只有在它与持久化上下文之间没有缓存层时才有道理,但是不是所有的提供程序都是这种情况.在所有的提供程序中相当普遍的一件事情是,包含缓存的工厂中的实体管理器将共享缓存中的实体数据,所以它更好的名称是共享缓存(shared cache). 11.7.2 共享缓存
在实体级别进行操作是与缓存接口的最佳方法,并且也是用于API的最自然和最方便的粒度.
在JPA中通过一个简单的javax.persistence.Cache接口来操作共享缓存.可以通过调用EntityManagerFactory.getChache()从实体管理器工厂获得实现Cache的对象.即使一个提供程序不支持缓存,也将返回一个Cache对象.public class SimpleTest { static EntityManagerFactory emf; EntityManager em; @BeforeClass public static ovid classSetUp(){ emf=Persistence.createEntityManagerFactory("HR"); } @AfterClass public static void classCleanUp(){ emf.close(); } @Before public void setUp(){ em = emf.createEntityManager(); } @After public void cleanUp(){ em.close(); emf.getCache().evictAll(); } @Test public void testMethod(){ } }
- 缓存的静态配置
持久性单元的缓存配置是通过设置persistence.xml文件中的shared-cache-mode元素或者等价的javax.persistence.sharedCache.mode属性来控制的.它有五个选项,其中之一是默认的NOT_SPECIFIED.这意味着如果没有再persistence.xml文件中通过设置javax.persistence.sharedCache.mode属性的存在来显式地指定共享缓存设置,那么将由提供程序来决定缓存或者不缓存,这取决于它自身的默认值和倾向.
两个其他选项,ALL和NONE,表示完全启用或者完全禁用共享缓存.
当一个实体类极不稳定和高度并发时,只禁用那个类的实例缓存偶尔也会用.通过设置共享缓存为DISABLE_SELECTIVE,然后通过使用@Cacheable(false)注解保持不缓存的特定实体类,可以实现.DISABLE_SELECTIVE选项将导致默认行为是缓存持久性单元的每个实体,每当一个实体类注解为@Cacheable(false)时,您将有效地重写默认选项,并禁用对暗中实体类型的实例的缓存.您可以对您所想要的任意数量的实体类这么做.
如果现在必须注解更多的类,可以将上一种属性的值更改为ENABLE_SELECTIVE,这意味着默认对所有实体禁用缓存,除了那些已经注解为@Cacheable(true)的实体. 动态缓存管理
使用缓存模式属性public List<Stock> findExpensiveStocks(double threshold) { TypedQuery<Stock> q = em.createQuery("SELECT s FROM Stock s WHERE s.price > :amount",Stock.class); q.setProperty("javax.persistence.cache.retrieveMode",CacheRetrieveMode.BYPASS); q.setProperty("javax.persistence.cache.storeMode",CacheStoreMode.REFRESH); q.setParameter("amount",threshold); return q.getResultList(); }
- 缓存的静态配置
- 11.7.1 通过层排序