I am writing a piece of software that tracks medication usage. I am using JPA to interact with the database. My model consists of two entities: a Prescription
and a Dose
. Each Prescription
has a collection of Dose
instances which represents the doses given to the patient as part of this prescription like so:
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;
}
}
A Dose
can only exist in the context of a Prescription
, and hence a Dose
is inserted into the database indirectly by adding it to its prescription's collection of doses:
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)
can be called concurrently. When deciding if a dose is available, the business logic inspects the prescription's collection of doses. The transaction should fail if this collection is concurrently modified (e.g. by concurrent call to addDose(long)
). I use the LockModeType.OPTIMISTIC_FORCE_INCREMENT
to achieve this (instead of acquiring a table lock on the DOSE table). In Pro JPA 2 by Keith and Schincariol I've read that:
The write lock guarantees all that the optimistic read lock does, but also pledges to increment the version field in the transaction regardless of whether a user updated the entity or not. [...] the common case for using OPTIMISTIC_FORCE_INCREMENT is to guarantee consistency across entity relationship changes (often they are one-to-many relationships with target foreign keys) when in the object model the entity relationship pointers change, but in the data model no columns in the entity table change.
Is my understanding of this lock mode correct? Does my current strategy ensure that the addDose
transaction will fail if there is ANY change whatsoever to the prescription's collection of doses (be that either add, remove or update of any dose in the collection)?