I have this transaction:
em.getTransaction().begin();
{
final Payment payment = em.find(Payment.class, id);
if (payment.status != Status.INIT)
throw new IllegalStateException("Cannot set to PAID, is not INIT but " + status);
payment.status = Status.PAID;
}
em.getTransaction().commit();
log.info("Payment " + id + " was paid");
However, as you can see here, the transaction does not prevent a race condition:
[11:10:18.265] INFO [PaymentServlet] [MSP] Status COMPLETED
[11:10:18.265] INFO [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid
[11:10:18.267] INFO [PaymentServlet] [MSP] Status COMPLETED
[11:10:18.267] INFO [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid
The payment is set to PAID twice. My exception is not thrown, nor is there a rollback or anything.
What am I doing wrong?
You need to use optimistic locking. Optimistic locking is where conflicting updates are rare so it is acceptable to rollback the occasional transaction when it occurs. Pessimistic locking causes the database to hold a lock on the object while it’s in use, effectively single-threading everything and potentially causing performance problems. See http://en.wikibooks.org/wiki/Java_Persistence/Locking#JPA_2.0_Locking for a more detailed explanation.
To solve the problem here, you should add a field to Payment (the traditional declaration is private Long version) and give it the JPA @Version annotation. If you’re managing your schema manually, ensure that a corresponding column exists in the right table. JPA will then use this field to check for conflicting updates and roll back the transaction if a conflict exists.
Update: More on pessimistic locking here: https://blogs.oracle.com/carolmcdonald/entry/jpa_2_0_concurrency_and In short, you can configure JPA to lock objects, but it’s extremely rare that it’s a good idea to do so. Put another way, if you were hand-coding queries to JDBC, you’d have to write in “for update” at the end of each select to cause pessimistic locking; the default is not to lock on read because it makes databases and database users cry.