Hi, I'm getting some exceptions in my unit tests due to optimistic locking errors and I don't quite understand why. There are 2 scenarios but for brevity I'll just focus on one here and maybe that'll shed light on the other case. BTW, I'm using OpenJPA 2.0.1.
Basically, I try to delete an entity which has 2 levels of entity collections. Since the entities have CascadeType.ALL I expected that a simple em.remove() would cascade to the others without error. Instead I get the optimistic lock errors. Any insights appreciated. And my apologies for including so much source; I tried to pare it down to the essentials. Below is the test which fails. (Some background: The methods makeStubRecipe(), addGroup/Ingredient/Step() are just helpers that populate fields with arbitrary data. The basic structure of the data is: a Recipe has a list of Groupings; a Grouping has a list of Ingredients and a list of Steps.): @Test public void testDelete_Cascades() { Recipe recipe = makeStubRecipe(); Grouping g = addGroup(recipe); addIngredient(g); addStep(g); // Add to db et.begin(); em.persist(recipe); et.commit(); assertFalse(em.contains(recipe)); int gid = recipe.getGroupings().get(0).getGroupId(); int iid = recipe.getGroupings().get(0).getIngredients().get(0).getIngredientId(); int sid = recipe.getGroupings().get(0).getSteps().get(0).getStepId(); // Now delete recipe = em.find(Recipe.class, recipe.getRecipeId()); et.begin(); em.remove(recipe); et.commit(); // <- FAILS HERE assertNull(em.find(Recipe.class, recipe.getRecipeId())); assertNull(em.find(Grouping.class, gid)); assertNull(em.find(Ingredient.class, iid)); assertNull(em.find(Step.class, sid)); } If I add the following right after et.begin() it succeeds: for (Grouping g2 : recipe.getGroupings()) { for (Ingredient i: g2.getIngredients()) { em.remove(i); } for (Step s : g2.getSteps()) { em.remove(s); } em.remove(g2); } But it seems like I shouldn't have to programmatically cascade the removes. Am I simply mistaken? If so, what's the value of declaring a cascading relationship? The DDL, BTW, does have 'on delete cascade' on the foreign key constraints. Here's the exception: <openjpa-2.0.1-r422266:989424 fatal store error> org.apache.openjpa.persistence.RollbackException: Optimistic locking errors were detected when flushing to the data store. The following objects may have been concurrently modified in another transaction: [org.cliftonfarm.feed.domain.Grouping-1069, org.cliftonfarm.feed.domain.Ingredient-1107, org.cliftonfarm.feed.domain.Step-1119] at org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:584) at org.cliftonfarm.feed.domain.RecipeTest.testDelete_Cascades(RecipeTest.java:1451) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31) at org.junit.rules.TestWatchman$1.evaluate(TestWatchman.java:48) at org.junit.runners.BlockJUnit4ClassRunner.runNotIgnored(BlockJUnit4ClassRunner.java:79) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:71) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31) at org.junit.runners.ParentRunner.run(ParentRunner.java:236) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197) Caused by: <openjpa-2.0.1-r422266:989424 nonfatal store error> org.apache.openjpa.persistence.OptimisticLockException: Optimistic locking errors were detected when flushing to the data store. The following objects may have been concurrently modified in another transaction: [org.cliftonfarm.feed.domain.Grouping-1069, org.cliftonfarm.feed.domain.Ingredient-1107, org.cliftonfarm.feed.domain.Step-1119] at org.apache.openjpa.kernel.BrokerImpl.newFlushException(BrokerImpl.java:2291) at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:2139) at org.apache.openjpa.kernel.BrokerImpl.flushSafe(BrokerImpl.java:2037) at org.apache.openjpa.kernel.BrokerImpl.beforeCompletion(BrokerImpl.java:1955) at org.apache.openjpa.kernel.LocalManagedRuntime.commit(LocalManagedRuntime.java:81) at org.apache.openjpa.kernel.BrokerImpl.commit(BrokerImpl.java:1479) at org.apache.openjpa.kernel.DelegatingBroker.commit(DelegatingBroker.java:925) at org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:560) ... 29 more Caused by: <openjpa-2.0.1-r422266:989424 nonfatal store error> org.apache.openjpa.persistence.OptimisticLockException: An optimistic lock violation was detected when flushing object instance "org.cliftonfarm.feed.domain.Grouping-1069" to the data store. This indicates that the object was concurrently modified in another transaction. FailedObject: org.cliftonfarm.feed.domain.Grouping-1069 at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flushAndUpdate(PreparedStatementManagerImpl.java:123) at org.apache.openjpa.jdbc.kernel.BatchingPreparedStatementManagerImpl.flushAndUpdate(BatchingPreparedStatementManagerImpl.java:81) at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flushInternal(PreparedStatementManagerImpl.java:99) at org.apache.openjpa.jdbc.kernel.PreparedStatementManagerImpl.flush(PreparedStatementManagerImpl.java:87) at org.apache.openjpa.jdbc.kernel.ConstraintUpdateManager.flush(ConstraintUpdateManager.java:550) at org.apache.openjpa.jdbc.kernel.ConstraintUpdateManager.flush(ConstraintUpdateManager.java:120) at org.apache.openjpa.jdbc.kernel.BatchingConstraintUpdateManager.flush(BatchingConstraintUpdateManager.java:59) at org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:103) at org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:76) at org.apache.openjpa.jdbc.kernel.JDBCStoreManager.flush(JDBCStoreManager.java:731) at org.apache.openjpa.kernel.DelegatingStoreManager.flush(DelegatingStoreManager.java:131) ... 36 more The JPA properties I'm setting on the EntityManagerFactory are: #----------------------------------------------------------------- javax.persistence.jdbc.driver=org.apache.derby.jdbc.ClientDriver javax.persistence.jdbc.url=jdbc:derby://localhost/feed javax.persistence.jdbc.user=<whatever> javax.persistence.jdbc.password=<whatever> javax.persistence.sharedCache.mode=UNSPECIFIED openjpa.Specification="JPA 2.0" openjpa.AutoDetach=close,commit openjpa.DynamicDataStructs=true openjpa.Log=log4j openjpa.TransactionMode=local openjpa.jdbc.SynchronizeMappings=refresh openjpa.jdbc.ResultSetType=scroll-insensitive openjpa.jdbc.UpdateManager=batching-constraint #----------------------------------------------------------------- Finally, here's the entity sources with most of the boring details cut out: //---------------------------------------------------------------- @MappedSuperclass public abstract class Versionable { @Version private Integer version; public Integer getVersion() { return version; } } //---------------------------------------------------------------- @Entity public class Recipe extends Versionable implements Serializable { private static final long serialVersionUID = -2136187716531617857L; /** Unique ID for this Recipe */ @Id @Column (name="RECIPE_ID", insertable=false, nullable=false, updatable=false) @GeneratedValue (strategy=GenerationType.IDENTITY) private Integer recipeId; @OneToMany (mappedBy="recipe", cascade={CascadeType.ALL}, fetch=FetchType.LAZY, orphanRemoval=true) @OrderBy ("position") private List<Grouping> groupings = new LinkedList<Grouping>(); /* ... uninteresting data fields omitted... */ //--------------------------------------------------------------- ACCESSORS public Integer getRecipeId() { return recipeId; } public List<Grouping> getGroupings() { return groupings; } public void setGroupings(List<Grouping> groupings) { this.groupings = new LinkedList<Grouping>(); this.groupings.addAll(groupings); for (Grouping g : this.groupings) { // Welcome to the family: g.setRecipe(this); } } /* ... uninteresting accessors & other methods omitted... */ } //---------------------------------------------------------------- @Entity public class Grouping extends Versionable implements Serializable { private static final long serialVersionUID = 4129360725018241352L; /** Unique ID for this Grouping */ @Id @Column (name="GROUP_ID", insertable=false, nullable=false, updatable=false) @GeneratedValue (strategy=GenerationType.IDENTITY) private Integer groupId; /** Recipe to which this Grouping belongs */ @ManyToOne @JoinColumn (name="RECIPE_ID", insertable=true, nullable=false, updatable=true) private Recipe recipe; @OneToMany (mappedBy="grouping", cascade={CascadeType.ALL}, fetch=FetchType.LAZY, orphanRemoval=true) @OrderBy ("position") private List<Ingredient> ingredients = new LinkedList<Ingredient>(); @OneToMany (mappedBy="grouping", cascade={CascadeType.ALL}, fetch=FetchType.LAZY, orphanRemoval=true) @OrderBy ("position") private List<Step> steps = new LinkedList<Step>(); /* ... uninteresting data fields omitted... */ //--------------------------------------------------------------- ACCESSORS public Integer getGroupId() { return groupId; } public Recipe getRecipe() { return recipe; } public void setRecipe(Recipe recipe) { this.recipe = recipe; } public List<Ingredient> getIngredients() { return ingredients; } public void setIngredients(List<Ingredient> ingredients) { this.ingredients = new LinkedList<Ingredient>(); this.ingredients.addAll(ingredients); for (Ingredient i : this.ingredients) { // Welcome to the family: i.setGrouping(this); } } public List<Step> getSteps() { return steps; } public void setSteps(List<Step> steps) { this.steps = new LinkedList<Step>(); this.steps.addAll(steps); for (Step s : this.steps) { // Welcome to the family: s.setGrouping(this); } } /* ... uninteresting accessors & other methods omitted... */ } //---------------------------------------------------------------- @Entity public class Ingredient extends Versionable implements Serializable { private static final long serialVersionUID = 8616212484953302289L; /** Unique ID for this Ingredient */ @Id @Column (name="INGREDIENT_ID", insertable=false, nullable=false, updatable=false) @GeneratedValue (strategy=GenerationType.IDENTITY) private Integer ingredientId; /** The Grouping to which this Ingredient belongs */ @ManyToOne @JoinColumn (name="GROUP_ID", insertable=true, nullable=false, updatable=true) private Grouping grouping; /* ... uninteresting data fields omitted... */ //--------------------------------------------------------------- ACCESSORS public Integer getIngredientId() { return ingredientId; } public Grouping getGrouping() { return grouping; } public void setGrouping(Grouping grouping) { this.grouping = grouping; } /* ... uninteresting accessors & other methods omitted... */ } //---------------------------------------------------------------- @Entity public class Step extends Versionable implements Serializable { private static final long serialVersionUID = -3765886461428007870L; /** Unique ID for this Step */ @Id @Column (name="STEP_ID", insertable=false, nullable=false, updatable=false) @GeneratedValue (strategy=GenerationType.IDENTITY) private Integer stepId; /** The Grouping to which this Step belongs */ @ManyToOne @JoinColumn (name="GROUP_ID", insertable=true, nullable=false, updatable=true) private Grouping grouping; /* ... uninteresting data fields omitted... */ //--------------------------------------------------------------- ACCESSORS public Integer getStepId() { return stepId; } public Grouping getGrouping() { return grouping; } public void setGrouping(Grouping grouping) { this.grouping = grouping; } /* ... uninteresting accessors & other methods omitted... */ }