Thanks Milosz. That worked: SchemaFactory is my new friend. That also fixed the other scenario I alluded to in my original post.
Thanks for the kinder, gentler RTFM too. There's a lot of knobs and switches to digest in this area. Haven't used OpenJPA since the 1.0 days and that was on a fairly trivial schema (not that this one is terribly complex). On Wed, 2011-10-19 at 09:33 +0200, Miłosz Tylenda wrote: > Tim, > > How about using @ForeignKey or changing mapping defaults so that > OpenJPA knows the constraints defined in the database [1]? > > <property name="openjpa.jdbc.SchemaFactory" > value="native(ForeignKeys=true)"/> > > > Cheers, > Milosz > > [1] > http://openjpa.apache.org/builds/2.0.1/apache-openjpa-2.0.1/docs/manual/manual.html#ref_guide_mapping_jpa_fk > > > > > A little light shed when I set logging level to DEBUG. It appears that > > OpenJPA is trying to delete from "top to bottom": i.e. RECIPE then > > GROUPING then INGREDIENT then STEP. Since I have cascade deletes > > declared in the DDL I can understand how it would get errors when trying > > to delete from GROUPING etc. > > > > I am reluctant to relax the cascade deletes in DDL. So how can I > > persuade OpenJPA to delete from the bottom up or just delete from RECIPE > > and be done with it? I tried the following with no difference in > > outcome: > > * switching the UpdateManager to 'batching-operation-order' > > * paring down @OneToMany.cascade to just PERSIST. > > > > Still welcoming any insights... > > > > > > On Tue, 2011-10-18 at 11:43 -0400, Tim Watts wrote: > > > 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... */ > > > > > > } > > > > > > > > > > > >