From bf62efd220d0bd554db2a68e678d5aad816cd861 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 16 Sep 2021 16:52:20 +0200 Subject: [PATCH 1/3] Add failing testcases for wrong order in batch save and possible fix for one of them --- .../server/persist/BatchControl.java | 22 +++--- .../server/transaction/TransactionTest.java | 70 +++++++++++++++++++ .../tests/model/basic/relates/Relation1.java | 64 +++++++++++++++++ .../tests/model/basic/relates/Relation2.java | 64 +++++++++++++++++ .../tests/model/basic/relates/Relation3.java | 42 +++++++++++ 5 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java create mode 100644 ebean-test/src/test/java/org/tests/model/basic/relates/Relation1.java create mode 100644 ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java create mode 100644 ebean-test/src/test/java/org/tests/model/basic/relates/Relation3.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java index c2a2c834ce..4e13035ee2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java @@ -29,8 +29,6 @@ */ public final class BatchControl { - private static final Object DUMMY = new Object(); - /** * Used to sort queue entries by depth. */ @@ -52,7 +50,7 @@ public final class BatchControl { * Set of beans in this batch. This is used to ensure that a single bean instance is not included * in the batch twice (two separate insert requests etc). */ - private final IdentityHashMap persistedBeans = new IdentityHashMap<>(); + private final IdentityHashMap persistedBeans = new IdentityHashMap<>(); /** * Helper to determine statement ordering based on depth (and type). @@ -181,15 +179,22 @@ public int executeOrQueue(PersistRequestBean request, boolean batch) throws B * Add the request to the batch and return true if we should flush. */ private boolean addToBatch(PersistRequestBean request) { - Object alreadyInBatch = persistedBeans.put(request.entityBean(), DUMMY); + int depth = transaction.depth(); + BeanDescriptor desc = request.descriptor(); + // batching by bean type AND depth + String key = desc.rootName() + ":" + depth; + + String alreadyInBatch = persistedBeans.put(request.entityBean(), key); if (alreadyInBatch != null) { // special case where the same bean instance has already been // added to the batch (doesn't really occur with non-batching // as the bean gets changed from dirty to loaded earlier) - return false; + BatchedBeanHolder beanHolder = getBeanHolder(request, alreadyInBatch); + int ordering = depthOrder.orderingFor(depth); + return beanHolder.getOrder() > ordering; } - BatchedBeanHolder beanHolder = getBeanHolder(request); + BatchedBeanHolder beanHolder = getBeanHolder(request, key); int bufferSize = beanHolder.append(request); bufferMax = Math.max(bufferMax, bufferSize); @@ -342,14 +347,11 @@ private boolean isBeanHoldersEmpty() { * Return an entry for the given type description. The type description is * typically the bean class name (or table name for MapBeans). */ - private BatchedBeanHolder getBeanHolder(PersistRequestBean request) { + private BatchedBeanHolder getBeanHolder(PersistRequestBean request, String key) { int depth = transaction.depth(); BeanDescriptor desc = request.descriptor(); - // batching by bean type AND depth - String key = desc.rootName() + ":" + depth; - BatchedBeanHolder batchBeanHolder = beanHoldMap.get(key); if (batchBeanHolder == null) { int ordering = depthOrder.orderingFor(depth); diff --git a/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java new file mode 100644 index 0000000000..374e4519ee --- /dev/null +++ b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java @@ -0,0 +1,70 @@ +package io.ebeaninternal.server.transaction; + +import io.ebean.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.relates.*; + +public class TransactionTest extends BaseTestCase { + private Relation1 r1 = new Relation1("R1"); + private Relation2 r2 = new Relation2("R2"); + private Relation3 r3 = new Relation3("R3"); + + private Transaction txn; + + @BeforeEach + void beginTransaction() { + txn = DB.beginTransaction(); + txn.setBatchMode(true); + } + + @AfterEach + void commitTransaction() { + txn.commit(); + txn.close(); + } + + @Test + public void testMultiSave1() { + r2.setWithCascade(r3); + r1.setWithCascade(r2); + DB.save(r1); + } + + @Test + public void testMultiSave2() { + r2.setWithCascade(r3); + r1.setWithCascade(r2); + DB.save(r3); + DB.save(r1); + } + + @Test + public void testMultiSave3() { + r2.setWithCascade(r3); + r1.setWithCascade(r2); + DB.save(r3); + DB.save(r2); + DB.save(r1); + } + + @Test + public void testMultiSave4() { + r2.setNoCascade(r3); + r1.setWithCascade(r2); + DB.save(r3); + // Workaround: txn.flush(); + DB.save(r1); + } + + @Test + public void testMultiSave5() { + r2.setNoCascade(r3); + r1.setNoCascade(r2); + DB.save(r3); + DB.save(r2); + DB.save(r1); + } +} \ No newline at end of file diff --git a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation1.java b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation1.java new file mode 100644 index 0000000000..ba3d3cbf20 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation1.java @@ -0,0 +1,64 @@ +package org.tests.model.basic.relates; + + +import java.util.UUID; + +import javax.persistence.*; + +import io.ebean.annotation.ChangeLog; + +/** + * Relation entity + */ +@Entity +@ChangeLog +public class Relation1 { + + @Id + private UUID id = UUID.randomUUID(); + + private String name; + + public Relation1(String name) { + this.name = name; + } + + @ManyToOne + private Relation2 noCascade; + + @ManyToOne(cascade = CascadeType.ALL) + private Relation2 withCascade; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Relation2 getNoCascade() { + return noCascade; + } + + public void setNoCascade(Relation2 noCascade) { + this.noCascade = noCascade; + } + + public Relation2 getWithCascade() { + return withCascade; + } + + public void setWithCascade(Relation2 withCascade) { + this.withCascade = withCascade; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java new file mode 100644 index 0000000000..2af3bc4cbb --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java @@ -0,0 +1,64 @@ +package org.tests.model.basic.relates; + + +import java.util.UUID; + +import javax.persistence.*; + +import io.ebean.annotation.ChangeLog; + +/** + * Relation entity + */ +@Entity +@ChangeLog +public class Relation2 { + + @Id + private UUID id = UUID.randomUUID(); + + private String name; + + public Relation2(String name) { + this.name = name; + } + + @ManyToOne + private Relation3 noCascade; + + @ManyToOne(cascade = CascadeType.ALL) + private Relation3 withCascade; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Relation3 getNoCascade() { + return noCascade; + } + + public void setNoCascade(Relation3 noCascade) { + this.noCascade = noCascade; + } + + public Relation3 getWithCascade() { + return withCascade; + } + + public void setWithCascade(Relation3 withCascade) { + this.withCascade = withCascade; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation3.java b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation3.java new file mode 100644 index 0000000000..826e3564e4 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation3.java @@ -0,0 +1,42 @@ +package org.tests.model.basic.relates; + + +import java.util.UUID; + +import javax.persistence.*; + +import io.ebean.annotation.ChangeLog; + +/** + * Relation entity + */ +@Entity +@ChangeLog +public class Relation3 { + + @Id + private UUID id = UUID.randomUUID(); + + private String name; + + public Relation3(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} From f51f2269fbc763d49365ed19b0fc7473d6199845 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 17 Sep 2021 17:58:20 +1200 Subject: [PATCH 2/3] #2377 - Wrong order in batch save, fix transaction depth The guts of this fix is in DefaultPersister.saveAssocOne() with the change to use the new transaction.depthDecrement() method. For our failing test cases this changes the depth from -1, 0 to be 0, 1. We can see the impact of this change in the batch ordering via the logging of: io.ebean.SUM - txn[1001] BatchControl flush ... --- .../io/ebeaninternal/api/SpiTransaction.java | 25 +++++++++-- .../api/SpiTransactionProxy.java | 10 +++++ .../server/core/PersistRequest.java | 7 ++++ .../server/persist/DefaultPersister.java | 6 ++- .../ImplicitReadOnlyTransaction.java | 12 ------ .../server/transaction/JdbcTransaction.java | 30 ++++++------- .../server/transaction/NoTransaction.java | 9 ---- .../server/transaction/TransactionTest.java | 30 ++++++++----- .../tests/model/basic/relates/Relation2.java | 22 +++++++--- .../tests/model/basic/relates/Relation4.java | 42 +++++++++++++++++++ 10 files changed, 134 insertions(+), 59 deletions(-) create mode 100644 ebean-test/src/test/java/org/tests/model/basic/relates/Relation4.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java index d26ab3ecc5..6708d1a458 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java @@ -130,7 +130,7 @@ public interface SpiTransaction extends Transaction { Boolean getBatchGetGeneratedKeys(); /** - * Modify and return the current 'depth' of the transaction. + * Modify the current 'depth' of the transaction. *

* As we cascade save or delete we traverse the object graph tree. Going up * to Assoc Ones the depth decreases and going down to Assoc Manys the depth @@ -139,12 +139,31 @@ public interface SpiTransaction extends Transaction { * The depth is used for ordering batching statements. The lowest depth get * executed first during save. */ - void depth(int diff); + default void depth(int diff) { + // do nothing + } + + /** + * Decrement the depth BUT only if depth is greater than 0. + * Return the amount that depth should be incremented by (0 or 1). + */ + default void depthDecrement() { + // do nothing + } + + /** + * Reset the depth back to 0 - done at the end of top level insert and update. + */ + default void depthReset() { + // do nothing + } /** * Return the current depth. */ - int depth(); + default int depth() { + return 0; + } /** * Return true if dirty beans are automatically persisted. diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 03824903ba..c4f81bcab8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -342,6 +342,16 @@ public void depth(int diff) { transaction.depth(diff); } + @Override + public void depthDecrement() { + transaction.depthDecrement(); + } + + @Override + public void depthReset() { + transaction.depthReset(); + } + @Override public int depth() { return transaction.depth(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequest.java index 1eab09911d..3d30f70b3e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequest.java @@ -50,6 +50,13 @@ public enum Type { this.label = label; } + /** + * Reset the transaction depth back to 0. + */ + public void resetDepth() { + transaction.depthReset(); + } + @Override public void addTimingBatch(long startNanos, int size) { // nothing by default diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 5a8346372a..97f30cd8b8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -251,6 +251,7 @@ public List publish(Query query, Transaction transaction) { } else { update(request); } + request.resetDepth(); } draftHandler.updateDrafts(transaction, mgr); @@ -417,7 +418,7 @@ public void update(EntityBean entityBean, Transaction t) { } else { update(req); } - + req.resetDepth(); req.commitTransIfRequired(); req.flushBatchOnCascade(); @@ -455,6 +456,7 @@ public void insert(EntityBean bean, Transaction t) { try { req.initTransIfRequiredWithBatchCascade(); insert(req); + req.resetDepth(); req.commitTransIfRequired(); req.flushBatchOnCascade(); @@ -1123,7 +1125,7 @@ private void saveAssocOne(PersistRequestBean request) { && !prop.isReference(detailBean) && !request.isParent(detailBean)) { SpiTransaction t = request.transaction(); - t.depth(-1); + t.depthDecrement(); saveRecurse(detailBean, t, null, request.flags()); t.depth(+1); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java index 4948eb9ef1..a0b0bb06d8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -232,18 +232,6 @@ public boolean isSaveAssocManyIntersection(String intersectionTable, String bean throw new IllegalStateException(notExpectedMessage); } - @Override - public void depth(int diff) { - } - - /** - * Return the current depth. - */ - @Override - public int depth() { - return 0; - } - @Override public void markNotQueryOnly() { } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index b1909b1f86..1cd7118c46 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -484,34 +484,28 @@ public final boolean isSaveAssocManyIntersection(String intersectionTable, Strin return existingBean.equals(beanName); } - /** - * Return the depth of the current persist request plus the diff. This has the - * effect of changing the current depth and returning the new value. Pass - * diff=0 to return the current depth. - *

- * The depth of 0 is for the initial persist request. It is modified as the - * cascading of the save or delete traverses to the the associated Ones (-1) - * and associated Manys (+1). - *

- *

- * The depth is used to help the ordering of batched statements. - *

- * - * @param diff the amount to add or subtract from the depth. - */ @Override public final void depth(int diff) { depth += diff; } - /** - * Return the current depth. - */ @Override public final int depth() { return depth; } + @Override + public final void depthDecrement() { + if (depth != 0) { + depth += -1; + } + } + + @Override + public final void depthReset() { + depth = 0; + } + @Override public final void markNotQueryOnly() { this.queryOnly = false; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java index 8bccacfc63..9c043dfe9b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java @@ -310,15 +310,6 @@ public Boolean getBatchGetGeneratedKeys() { return null; } - @Override - public void depth(int diff) { - } - - @Override - public int depth() { - return 0; - } - @Override public boolean isExplicit() { return false; diff --git a/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java index 374e4519ee..c4bb4aafdf 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java +++ b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/TransactionTest.java @@ -7,10 +7,11 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.relates.*; -public class TransactionTest extends BaseTestCase { - private Relation1 r1 = new Relation1("R1"); - private Relation2 r2 = new Relation2("R2"); - private Relation3 r3 = new Relation3("R3"); +class TransactionTest extends BaseTestCase { + + private final Relation1 r1 = new Relation1("R1"); + private final Relation2 r2 = new Relation2("R2"); + private final Relation3 r3 = new Relation3("R3"); private Transaction txn; @@ -27,14 +28,14 @@ void commitTransaction() { } @Test - public void testMultiSave1() { + void testMultiSave1() { r2.setWithCascade(r3); r1.setWithCascade(r2); DB.save(r1); } @Test - public void testMultiSave2() { + void testMultiSave2() { r2.setWithCascade(r3); r1.setWithCascade(r2); DB.save(r3); @@ -42,7 +43,7 @@ public void testMultiSave2() { } @Test - public void testMultiSave3() { + void testMultiSave3() { r2.setWithCascade(r3); r1.setWithCascade(r2); DB.save(r3); @@ -51,7 +52,7 @@ public void testMultiSave3() { } @Test - public void testMultiSave4() { + void testMultiSave4() { r2.setNoCascade(r3); r1.setWithCascade(r2); DB.save(r3); @@ -60,11 +61,20 @@ public void testMultiSave4() { } @Test - public void testMultiSave5() { + void testMultiSave4_usingRelation4() { + Relation4 r4 = new Relation4("foo"); + r2.setR4NoCascade(r4); + r1.setWithCascade(r2); + DB.save(r4); + DB.save(r1); + } + + @Test + void testMultiSave5() { r2.setNoCascade(r3); r1.setNoCascade(r2); DB.save(r3); DB.save(r2); DB.save(r1); } -} \ No newline at end of file +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java index 2af3bc4cbb..a84ff7eb02 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java +++ b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation2.java @@ -1,12 +1,14 @@ package org.tests.model.basic.relates; -import java.util.UUID; - -import javax.persistence.*; - import io.ebean.annotation.ChangeLog; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import java.util.UUID; + /** * Relation entity */ @@ -23,6 +25,9 @@ public Relation2(String name) { this.name = name; } + @ManyToOne + private Relation4 r4NoCascade; + @ManyToOne private Relation3 noCascade; @@ -60,5 +65,12 @@ public Relation3 getWithCascade() { public void setWithCascade(Relation3 withCascade) { this.withCascade = withCascade; } - + + public Relation4 getR4NoCascade() { + return r4NoCascade; + } + + public void setR4NoCascade(Relation4 r4NoCascade) { + this.r4NoCascade = r4NoCascade; + } } diff --git a/ebean-test/src/test/java/org/tests/model/basic/relates/Relation4.java b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation4.java new file mode 100644 index 0000000000..9e2109f09f --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/relates/Relation4.java @@ -0,0 +1,42 @@ +package org.tests.model.basic.relates; + + +import io.ebean.annotation.ChangeLog; + +import javax.persistence.Entity; +import javax.persistence.Id; +import java.util.UUID; + +/** + * Relation entity + */ +@Entity +@ChangeLog +public class Relation4 { + + @Id + private UUID id = UUID.randomUUID(); + + private String name; + + public Relation4(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} From 1bf9cfde5067af876ebabc4d61b5ba0e832ecf23 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 20 Sep 2021 11:53:31 +1200 Subject: [PATCH 3/3] #2377 - Revert back the changes to BatchControl as no longer needed With the fix to the depth we no longer need these changes to BatchControl --- .../server/persist/BatchControl.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java index 4e13035ee2..37b41ff98c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java @@ -29,6 +29,8 @@ */ public final class BatchControl { + private static final Object DUMMY = new Object(); + /** * Used to sort queue entries by depth. */ @@ -50,7 +52,7 @@ public final class BatchControl { * Set of beans in this batch. This is used to ensure that a single bean instance is not included * in the batch twice (two separate insert requests etc). */ - private final IdentityHashMap persistedBeans = new IdentityHashMap<>(); + private final IdentityHashMap persistedBeans = new IdentityHashMap<>(); /** * Helper to determine statement ordering based on depth (and type). @@ -179,22 +181,15 @@ public int executeOrQueue(PersistRequestBean request, boolean batch) throws B * Add the request to the batch and return true if we should flush. */ private boolean addToBatch(PersistRequestBean request) { - int depth = transaction.depth(); - BeanDescriptor desc = request.descriptor(); - // batching by bean type AND depth - String key = desc.rootName() + ":" + depth; - - String alreadyInBatch = persistedBeans.put(request.entityBean(), key); + Object alreadyInBatch = persistedBeans.put(request.entityBean(), DUMMY); if (alreadyInBatch != null) { // special case where the same bean instance has already been // added to the batch (doesn't really occur with non-batching // as the bean gets changed from dirty to loaded earlier) - BatchedBeanHolder beanHolder = getBeanHolder(request, alreadyInBatch); - int ordering = depthOrder.orderingFor(depth); - return beanHolder.getOrder() > ordering; + return false; } - BatchedBeanHolder beanHolder = getBeanHolder(request, key); + BatchedBeanHolder beanHolder = getBeanHolder(request); int bufferSize = beanHolder.append(request); bufferMax = Math.max(bufferMax, bufferSize); @@ -347,10 +342,12 @@ private boolean isBeanHoldersEmpty() { * Return an entry for the given type description. The type description is * typically the bean class name (or table name for MapBeans). */ - private BatchedBeanHolder getBeanHolder(PersistRequestBean request, String key) { + private BatchedBeanHolder getBeanHolder(PersistRequestBean request) { int depth = transaction.depth(); BeanDescriptor desc = request.descriptor(); + // batching by bean type AND depth + String key = desc.rootName() + ":" + depth; BatchedBeanHolder batchBeanHolder = beanHoldMap.get(key); if (batchBeanHolder == null) {