I’ve been having problems with jpa @version on appengine (testing in eclipse, sdk latest version, v2 of DataNucleus – AKA 3.1.1), so I’ve written a test servlet to try to reproduce the problem in a simple form. The result behaves even more freakishly that the original code, and it looks to me as if it’s a bug, but I’d like a few eyeballs on it to check out that I haven’t done something dumb.
The setup is very simple: a singleton main @Entity class which contains a OneToMany mapping of a List of sub @Entitys (although in the test described I only create one). Here:
@Entity
public class MainEntity implements Serializable {
private static final long serialVersionUID = 1L;
protected static Key singletonKey =
KeyFactory.createKey(MainEntity.class.getSimpleName(), 1);
// Primary Key
@Id
protected Key id = singletonKey;
// Use optimistic locking
@Version
protected long version;
// Ref to sub entity, LAZY to be explicit
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, orphanRemoval=true)
protected List<SubEntity> subs = new ArrayList<SubEntity>();
}
@Entity
public class SubEntity implements Serializable {
private static final long serialVersionUID = 1L;
// Primary Key
@Id
protected Key id;
// Use optimistic locking
@Version
protected long version;
// Get variable part of key
public String getKey() { return id.getName(); }
// Set variable part of key
public void setKey(String key) {
id = KeyFactory.createKey(MainEntity.singletonKey,
SubEntity.class.getSimpleName(), key);
}
// Pretend contents
protected int contents;
// modifier for contents
public void incContent(int step) { contents += step; }
}
So, what I do is create the instance of MainEntity and an instance of SubEntity and add() the SubEntity to MainEntity.subs and commit it all. Then, each time using a fresh EntityManager, I fetch two detached instances of the MainEntity (with the contents of subs loaded), modify the contents field of both of them, then separately merge() them back.
What I expect to see is the first merge go through OK and the second to throw an exception.
What actually happens is that both merges go through OK (so the backing store has the second version of the contents field), but the version field is still 1 on the SubEntity but 3 on the MainEntity. (!) I don’t understand why the main is getting its version number updated, why it isn’t throwing an exception itself since the MainEntity is getting updated with an incorrect version number, why the sub entity isn’t causing a locking exception, and more generally what is going on.
So, have I misunderstood something or does there seem to be a bug?
Many thanks for any comments.
Jonathan.
Here is the business bit of the main code (very slightly cleaned up):
private static final EntityManagerFactory emf =
Persistence.createEntityManagerFactory("general_use");
private void doG(PrintWriter out) {
// First make sure db is empty
resetDb(out);
// Create default contents
initDb(out, 1);
// Get the main entity, detached
MainEntity m1 = fetch(out);
// Get another version
MainEntity m2 = fetch(out);
// Modify both versions of the sub entity in the detached copies
m1.subs.get(0).incContent(2);
m2.subs.get(0).incContent(3);
// Merge them back into db
merge(out, m1);
// See what's in the db
fetch(out);
// Merging the second one should cause an OptimisticLockException
try {
merge(out, m2);
out.format("We should have gotten an exception here.");
} catch (Throwable th) {
out.format("We got an exception here: %s, %s", th, th.getCause());
}
// Look at db
fetch(out);
}
// This gets the MainEntity from the db by its key
private MainEntity fetch(PrintWriter out) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
MainEntity m = em.find(MainEntity.class, MainEntity.singletonKey);
m.toString(); // Make sure that nothing is lazily fetched.
// MainEntity has a toString() defined on it that uses all the fields
// in itself and the contents of subs, so this loads everything
em.getTransaction().commit();
em.close();
out.format("Fetch completed, detached state: %s", m);
return m;
}
// This merges a detached MainEntity and its related sub entity into the db
private void merge(PrintWriter out, MainEntity mp) {
MainEntity m;
out.format("Merge commenced, detached state: %s", mp);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
m = em.merge(mp);
out.format("Merge pre commit, attached state: %s", m);
// as above, m.toString() in the format in fact loads everything
em.getTransaction().commit();
em.close();
out.format("Merge completed, detached state: %s", m);
}
// This creates the singleton MainEntity and a number of SubEntitys with keys "Init db #n" which it points to
private void initDb(PrintWriter out, int noSubCopies) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
MainEntity m = new MainEntity();
em.persist(m);
em.getTransaction().commit();
em.close();
em = emf.createEntityManager();
em.getTransaction().begin();
m = em.find(MainEntity.class, m.id);
for (int i = 0; i < noSubCopies; i++) {
SubEntity s = new SubEntity();
s.setKey("Init db #" + i);
em.persist(s);
m.subs.add(s);
}
m.toString();
em.getTransaction().commit();
em.close();
out.format("InitDb completed, detached state: %s", m);
}
I’d say that’s likely a bug in the loading of the “subs” field. Check that it has the version set on the main and on the subs after you do the find of the “main” (before the commit/close). You can use JDOHelper.getVersion(obj) also since that will show the version of the object, as opposed to the field value. I don’t see any code in the GAE plugin to definitely have the version set under all circumstances.
If the
JDOHelper.getVersion(sub)orJDOHelper.getVersion(main)return null when within the txn (before detach), report it as a bug at http://code.google.com/p/datanucleus-appengine/issues/list