4
votes

I have a JPA entity class User with a username @ID and no parent entity group. I need to make sure that when two parallel transactions try to persist a new User with the same username, only one is committed and the other one is rolled back.

Example:

User bob = new User("bob");
EntityTransaction transaction = em.getTransaction();

try {
  transaction.begin();
  User u = em.find(User.class, bob.id());
  if (u == null) {
    em.persist(bob);
  } else {
    // identical user already existed before the transaction
    throw new UserExistsException(); 
  }
  transaction.commit();
} catch (RollbackException e) {
    // identical user was created during the transaction
    throw new UserExistsException();
}

According to the Datastore docs, transactions follow an optimistic locking approach:

"When a transaction starts, App Engine uses optimistic concurrency control by checking the last update time for the entity groups used in the transaction. Upon commiting a transaction for the entity groups, App Engine again checks the last update time for the entity groups used in the transaction. If it has changed since our initial check, App Engine throws an exception." (https://developers.google.com/appengine/docs/java/datastore/transactions)

Will this work when persisting new (root) entities, which did not exist before the transaction? In my case, would App Engine check whether another transaction has meanwhile persisted a User with the same id? If so, do I need an explicit @Version field for that purpose?

1
I just came across this discussion post, which claims that the low-level API would throw a ConcurrentModificationException in exactly this case. Can anyone confirm, ideally also for JPA/JDO?David Geiger
I found your question after posting this stackoverflow.com/questions/27370112/… did you solve this for yourself?jchristof

1 Answers

1
votes

To put some long overdue closure on the matter: The answer is "yes" and the above code should work as expected. In short, the optimistic concurrency control mechanism will compare the (new) entity group used in both transactions using the (root) entity's kind "User" and the given identifier "bob". The Datastore docs also explicitly address the creation case now:

When two or more transactions try to change the same entity group at the same time (either updating existing entities or creating new ones), the first transaction to commit will succeed and all others will fail on commit.

With JPA, you'd get a RollbackException in this case. The low-level API will raise a ConcurrentModificationException. If you're using Objectify (which I'd strongly recommend), the failed transaction will automatically be retried. You should therefore make sure that, within the transaction, you first check if the entity exists unless you want to overwrite it at the second attempt.