我正在编写一个跟踪药物使用情况的软件。我正在使用JPA与数据库进行交互。我的模型由两个实体组成:Prescription
和Dose
。每个Prescription
都有Dose
个实例的集合,代表作为此处方的一部分给予患者的剂量,如下所示:
Prescription.java
@Entity
@XmlRootElement
public class Prescription {
private long id;
private Collection<Dose> doses = new ArrayList<Dose>();
/**
* 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 (e.g. 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<Dose> getDoses() {
// todo update to list or collection interface.
return doses;
}
public void setDoses(Collection<Dose> 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
添加到其处方的剂量集合中间接插入数据库@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.
}
}
:
DoseService.java
addDose(long)
addDose(long)
可以同时调用。在决定剂量是否可用时,业务逻辑检查处方的剂量集合。如果同时修改此集合(例如,通过并发调用LockModeType.OPTIMISTIC_FORCE_INCREMENT
),则事务应该失败。我使用addDose
来实现这一点(而不是在DOSE表上获取表锁)。在Pro JPA 2 by Keith and Schincariol我已经读过:
写锁定保证了乐观读锁的所有功能,但是 还承诺增加交易中的版本字段 无论用户是否更新了实体。 [...] 使用OPTIMISTIC_FORCE_INCREMENT的常见情况是保证 实体关系变化的一致性(通常是 在对象中时与目标外键的一对多关系 模型实体关系指针发生了变化,但在数据模型中 实体表中没有列发生变化。
我对这种锁定模式的理解是否正确?我的当前策略是否确保public void myMethod() {
if (condition1) {
doSomething1();
if (condition2) {
doSomething2();
} else {
doSomething3();
}
}
}
交易如果对处方的剂量集合有任何改变(无论是添加,删除或更新集合中的任何剂量)都会失败?
答案 0 :(得分:1)
看起来是对的。
但是,我建议先测试一下......更简单的方法是通过调试...使用你喜欢的IDE,在句子后面设置一个调试点:
entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
稍后,尝试从两个不同的客户端调用您的addDose(prescriptionId)
,提供相同的prescriptionID ...并让一个客户端先完成,看看另一个客户端发生了什么。
答案 1 :(得分:0)
This answer帮助我更好地理解OPTIMISTIC_WRITE_LOCK
,并使我确信我的实施是正确的。
下面是一个更详细的解释(引用在我自己撰写的报告中添加时引用):
虽然EJB事务可能有助于防止对实体的持久状态进行并发更改,但在这种情况下它们是不够的。 这是因为他们无法检测到
Prescription
实体对其相应数据库的更改 添加新Dose
时,行不会更改。这源于 事实上Dose
是关系的拥有方 在它自己和Prescription
之间。在数据库中,行 表示Dose
将有一个指向的外键Prescription
,但代表Prescription
的行会 没有指向任何Dose
的指针。这个问题可以解决 使用强制写入锁定来保护Prescription
插入新Prescription
时对Dose
行的更新(具体为:其版本字段)。