3

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)?

Janus Varmarken
  • 2,306
  • 3
  • 20
  • 42
  • From my understanding, your transaction would fail if there's a concurrent transaction on the object, regardless if that concurrent transaction modifies your object. In optimistic concurrency, transactions fail if the modified object's version and the current object's version do not match. – Jazzwave06 Jul 29 '16 at 18:18
  • What container/web application server are you using? Not all containers implement JPA or EJB standards the same way. Some completely ignore certain parameters or annotations. – Usi Jul 29 '16 at 18:29
  • @JeffreyColeman I'm using GlassFish open source edition. – Janus Varmarken Jul 29 '16 at 18:30
  • @JanusVarmarken Thanks for the response. Well in that case I'd say my first guess was probably wrong but I'll check the documentation for you when I have a chance. GlassFish typically implements everything as it's sort of a testing ground for JEE specifications. – Usi Jul 29 '16 at 18:46

2 Answers2

1

It Seems right.

However, I'll suggest to test it first... the more simple way to do this, is through debugging ... using your preferred IDE, set a debug point after the sentence:

entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT);

Later, try to invoke your addDose(prescriptionId) from two different clients, supplying the same prescriptionID ... and let one client finish first and see what happen with the another one.

Carlitos Way
  • 3,279
  • 20
  • 30
  • Thank you for the debugging suggestion. I added a +1. However, I am looking for a somewhat stronger confirmation of correctness than "it seems right" in order to accept an answer (e.g. an answer that explains the locking details specific to this scenario). – Janus Varmarken Aug 09 '16 at 23:54
0

This answer helped me understand the OPTIMISTIC_WRITE_LOCK better and convinced me that my implementation is correct.

A more elaborate explanation follows (quotation added as it appears in a report authored by myself):

While EJB transactions may help prevent concurrent changes to the persisted state of an entity, they are insufficient in this case. This is because they are unable to detect the change to the Prescription entity as its corresponding database row is not changed when a new Dose is added to it. This stems from the fact that the Dose is the owning side of the relationship between itself and its Prescription. In the database, the row that represents the Dose will have a foreign key pointing to the Prescription, but the row that represents the Prescription will have no pointers to any of its Doses. The problem is remedied by guarding the Prescription with an optimistic write lock that forces an update to the Prescription’s row (to be specific: its version field) when a new Dose is inserted.

Community
  • 1
  • 1
Janus Varmarken
  • 2,306
  • 3
  • 20
  • 42