appengineのjpa@versionで問題が発生しているため(Eclipse、sdk最新バージョン、DataNucleusのv2-AKA 3.1.1でテスト)、問題を簡単な形式で再現するためのテストサーブレットを作成しました。結果は元のコードよりもさらに奇妙に動作し、バグのように見えますが、私が何か馬鹿げたことをしていないことを確認するために、いくつかの目玉が必要です。
セットアップは非常に簡単です。サブ@エンティティのリストのOneToManyマッピングを含むシングルトンメイン@Entityクラスです(ただし、説明したテストでは1つしか作成しません)。ここ:
@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; }
}
したがって、私が行うことは、MainEntityのインスタンスとSubEntityのインスタンスを作成し、SubEntityをMainEntity.subsにadd()して、すべてをコミットすることです。次に、新しいEntityManagerを使用するたびに、MainEntityの2つのデタッチされたインスタンス(サブのコンテンツがロードされている)をフェッチし、両方のコンテンツフィールドを変更してから、別々にmerge()して戻します。
私が期待しているのは、最初のマージがOKを通過し、2番目が例外をスローすることです。
実際に発生するのは、両方のマージがOKを通過することです(したがって、バッキングストアにはコンテンツフィールドの2番目のバージョンがあります)が、バージョンフィールドはサブエンティティでは1ですが、メインエンティティでは3です。(!)メインがバージョン番号を更新している理由、メインエンティティが誤ったバージョン番号で更新されているために例外自体をスローしていない理由、サブエンティティがロック例外を引き起こしていない理由がわかりません、より一般的には何が起こっているのか。
それで、私は何かを誤解しましたか、それともバグがあるように見えますか?
コメントありがとうございます。
ジョナサン。
メインコードのビジネスビットは次のとおりです(ごくわずかにクリーンアップされています)。
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);
}