JPA:执行间接插入时锁定

我正在编写一个跟踪药物使用情况的软件。 我正在使用JPA与数据库进行交互。 我的模型由两个实体组成: PrescriptionDose 。 每个Prescription都有一个Dose实例的集合,代表作为该处方的一部分给予患者的剂量,如下所示:

Prescription.java

 @Entity @XmlRootElement public class Prescription { private long id; private Collection doses = new ArrayList(); /** * Versioning field used by JPA to track concurrent changes. */ private long version; // Other properties omitted... @Id @GeneratedValue(strategy = GenerationType.TABLE) public long getId() { return id; } public void setId(long id) { this.id = id; } // We specify cascade such that when we modify this collection, it will propagate to the DOSE table (eg when // adding a new dose to this collection, a corresponding record will be created in the DOSE table). @OneToMany(mappedBy = "prescription", cascade = CascadeType.ALL) public Collection getDoses() { // todo update to list or collection interface. return doses; } public void setDoses(Collection doses) { this.doses = doses; } @Version public long getVersion() { return version; } /** * Application code should not call this method. However, it must be present for JPA to function. * @param version */ public void setVersion(long version) { this.version = version; } } 

Dose.java

 @Entity @XmlRootElement public class Dose { private long id; private Prescription prescription; // Other properties omitted... @Id @GeneratedValue(strategy = GenerationType.TABLE) public long getId() { return id; } public void setId(long id) { this.id = id; } @XmlTransient @ManyToOne @JoinColumn(name = "PRESCRIPTION_ID") // Specifies name of column pointing back to the parent prescription. public Prescription getPrescription() { return prescription; } public void setPrescription(Prescription prescription) { this.prescription = prescription; } } 

Dose只能存在于Prescription的上下文中,因此通过将Dose添加到其处方的剂量集合中间接地将Dose插入数据库中:

DoseService.java

 @Stateless public class DoseService { @PersistenceContext(unitName = "PrescriptionUnit") private EntityManager entityMgr; /** * Insert a new dose for a given prescription ID. * @param prescriptionId The prescription ID. * @return The inserted {@code Dose} instance if insertion was successful, * or {@code null} if insertion failed (if there is currently no doses available for the given prescription ID). */ @TransactionAttribute(value = TransactionAttributeType.REQUIRED) public Dose addDose(long prescriptionId) { // Find the prescription. Prescription p = entityMgr.find(Prescription.class, prescriptionId); if (p == null) { // Invalid prescription ID. throw new IllegalArgumentException("Prescription with id " + prescriptionId + " does not exist."); } // TODO is this sufficient locking? entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT); Dose d = null; if (isDoseAvailable(p)) { // A dose is available, create it and insert it into the database. d = new Dose(); // Setup the link between the new dose and its parent prescription. d.setPrescription(p); p.getDoses().add(d); } try { // Flush changes to database. entityMgr.flush(); return d; } catch (OptimisticLockException ole) { // Rethrow application-managed exception to ensure that caller will have a chance of detecting failure due to concurrent updates. // (OptimisticLockExceptions can be swallowed by the container) // See "Recovering from Optimistic Failures" (page 365) in "Pro JPA 2" by M. Keith and M. Schincariol for details. throw new ChangeCollisionException(); } } /** * Checks if a dose is available for a given prescription. * @param p The prescription for which to look up if a dose is available. * @return {@code true} if a dose is available, {@code false} otherwise. */ @TransactionAttribute(value = TransactionAttributeType.MANDATORY) private boolean isDoseAvailable(Prescription p) { // Business logic that inspects p.getDoses() and decides if it is safe to give the patient a dose at this time. } } 

addDose(long)可以同时调用。 在确定剂量是否可用时,业务逻辑检查处方的剂量集合。 如果同时修改此集合,则事务应该失败(例如,通过并发调用addDose(long) )。 我使用LockModeType.OPTIMISTIC_FORCE_INCREMENT来实现这一点(而不是在DOSE表上获取表锁)。 在Keith和Schincariol的Pro JPA 2中,我读过:

写锁定保证了乐观读锁的所有function,但也承诺在事务中增加版本字段,无论用户是否更新了实体。 […]使用OPTIMISTIC_FORCE_INCREMENT的常见情况是保证实体关系变化的一致性(通常它们是与目标外键的一对多关系),当在对象模型中实体关系指针发生变化时,但在数据模型中实体表中没有列发生更改。

我对这种锁模式的理解是否正确? 如果处方的剂量集合有任何变化(添加,删除或更新集合中的任何剂量),我当前的策略是否确保addDose事务将失败?

看起来是对的。

但是,我建议先测试一下……更简单的方法是通过调试…使用你喜欢的IDE,在句子后面设置一个调试点:

 entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT); 

稍后,尝试从两个不同的客户端调用你的addDose(prescriptionId) ,提供相同的prescriptionID …并让一个客户端先完成,看看另一个客户端发生了什么。

这个答案帮助我更好地理解了OPTIMISTIC_WRITE_LOCK并使我确信我的实现是正确的。

下面是一个更详细的解释(报价添加,因为它出现在我自己撰写的报告中):

虽然EJB事务可能有助于防止对实体的持久状态进行并发更改,但在这种情况下它们是不够的。 这是因为他们无法检测对Prescription实体的更改,因为当向其添加新Dose时,其相应的数据库行不会更改。 这源于这样一个事实,即Dose是其与Prescription之间关系的拥有方。 在数据库中,表示Dose行将具有指向Prescription的外键,但代表Prescription行将没有指向其任何Dose的指针。 通过使用乐观的写锁来保护Prescription可以解决这个问题,当写入新的Dose时,该锁会强制更新Prescription的行(具体来说:它的版本字段)。