micronaut-projects/micronaut-data

Cannot persist Entity with several inverse one-to-many relations due to UnsupportedOperationException

danielksb opened this issue · 2 comments

Expected Behavior

I should be possible to save the following entity with a CrudRepository:

@MappedEntity
public record Entity(
  @Id @AutoPopulated Long id,
  @Relation(value = Kind.ONE_TO_MANY, cascade = Cascade.ALL, mappedBy = "entity")
  List<SubEntityA> subEntityAs,
  // a second relationship is needed to trigger the error
  @Relation(value = Kind.ONE_TO_MANY, cascade = Cascade.ALL, mappedBy = "entity")
  List<SubEntityB> subEntityBs
){};

@MappedEntity
public record SubEntityA(
  @Id @AutoPopulated Long id,
  @Nullable Integer data,
  // a back reference is needed to trigger the error
  @Relation(Kind.MANY_TO_ONE)
  Entity entity
){};

@MappedEntity
public record SubEntityA(
  @Id @AutoPopulated Long id,
  @Nullable Integer data,
  @Relation(Kind.MANY_TO_ONE)
  Entity entity
){};

interface EntityRepository extends CrudRepository<Entity, Long> {}

Actual Behaviour

When saving with CrudRepository.save(entity) the following error is thrown:

io.micronaut.data.exceptions.DataAccessException: Error executing PERSIST: null

  at io.micronaut.data.runtime.operations.internal.BaseOperations.failed(BaseOperations.java:141)
  at io.micronaut.data.runtime.operations.internal.BaseOperations.persist(BaseOperations.java:90)
  at io.micronaut.data.runtime.operations.internal.EntityOperations.persist(EntityOperations.java:31)
  at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.lambda$persist$20(DefaultJdbcRepositoryOperations.java:706)
  at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.lambda$executeWrite$24(DefaultJdbcRepositoryOperations.java:781)
  at io.micronaut.data.connection.support.AbstractConnectionOperations.withExistingConnectionInternal(AbstractConnectionOperations.java:128)
  at io.micronaut.data.connection.support.AbstractConnectionOperations.execute(AbstractConnectionOperations.java:92)
  at io.micronaut.data.connection.ConnectionOperations.executeWrite(ConnectionOperations.java:82)
  at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.executeWrite(DefaultJdbcRepositoryOperations.java:778)
  at io.micronaut.data.jdbc.operations.DefaultJdbcRepositoryOperations.persist(DefaultJdbcRepositoryOperations.java:702)
  at io.micronaut.data.runtime.intercept.DefaultSaveEntityInterceptor.intercept(DefaultSaveEntityInterceptor.java:45)
  at io.micronaut.data.runtime.intercept.DataIntroductionAdvice.intercept(DataIntroductionAdvice.java:83)
  at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:138)
  at mytest.EntityRepository$Intercepted.save(Unknown Source)
  at mytest.EntityRepositoryIT.testCascadingSaveAndFetching(EntityRepositoryIT.java:21)
  at java.base/java.lang.reflect.Method.invoke(Method.java:568)
  at io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
  at io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:162)
  at io.micronaut.test.extensions.AbstractMicronautExtension$3.proceed(AbstractMicronautExtension.java:174)
  at io.micronaut.test.context.TestMethodInterceptor.interceptTest(TestMethodInterceptor.java:46)
  at io.micronaut.transaction.test.DefaultTestTransactionExecutionListener.lambda$interceptTest$0(DefaultTestTransactionExecutionListener.java:93)
  at io.micronaut.transaction.support.AbstractPropagatedStatusTransactionOperations.lambda$execute$2(AbstractPropagatedStatusTransactionOperations.java:68)
  at io.micronaut.transaction.TransactionCallback.apply(TransactionCallback.java:37)
  at io.micronaut.transaction.support.AbstractTransactionOperations.executeTransactional(AbstractTransactionOperations.java:333)
  at io.micronaut.transaction.support.AbstractTransactionOperations.executeWithNewTransaction(AbstractTransactionOperations.java:318)
  at io.micronaut.transaction.support.AbstractTransactionOperations.executeNew(AbstractTransactionOperations.java:235)
  at io.micronaut.transaction.support.AbstractTransactionOperations.doExecute(AbstractTransactionOperations.java:137)
  at io.micronaut.transaction.support.AbstractTransactionOperations.lambda$doExecute$0(AbstractTransactionOperations.java:122)
  at io.micronaut.data.connection.support.AbstractConnectionOperations.executeWithNewConnection(AbstractConnectionOperations.java:143)
  at io.micronaut.data.connection.support.AbstractConnectionOperations.execute(AbstractConnectionOperations.java:90)
  at io.micronaut.transaction.support.AbstractTransactionOperations.doExecute(AbstractTransactionOperations.java:120)
  at io.micronaut.transaction.support.AbstractPropagatedStatusTransactionOperations.execute(AbstractPropagatedStatusTransactionOperations.java:65)
  at io.micronaut.transaction.test.DefaultTestTransactionExecutionListener.interceptTest(DefaultTestTransactionExecutionListener.java:91)
  at io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:166)
  at io.micronaut.test.extensions.AbstractMicronautExtension.interceptTest(AbstractMicronautExtension.java:119)
  at io.micronaut.test.extensions.junit5.MicronautJunit5Extension.interceptTestMethod(MicronautJunit5Extension.java:129)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.lang.UnsupportedOperationException
  at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
  at java.base/java.util.ImmutableCollections$ListItr.set(ImmutableCollections.java:419)
  at io.micronaut.data.runtime.operations.internal.AbstractCascadeOperations.afterCascadedMany(AbstractCascadeOperations.java:184)
  at io.micronaut.data.runtime.operations.internal.SyncCascadeOperations.cascadeEntity(SyncCascadeOperations.java:157)
  at io.micronaut.data.runtime.operations.internal.AbstractSyncEntityOperations.cascadePost(AbstractSyncEntityOperations.java:83)
  at io.micronaut.data.runtime.operations.internal.BaseOperations.persist(BaseOperations.java:87)
  ... 36 more

The reason seems to be io.micronaut.data.runtime.operations.internal.AbstractSyncEntitiesOperations#getEntities which is returning an immutable list of entities. But in io.micronaut.data.runtime.operations.internal.AbstractCascadeOperations#afterCascadedMany an iterator is created for this list of entities and iterator.set(newc) is called. For this to work we need an iterator over a mutable list.

This is possibly a regression bug due to this PR: #2905

Steps To Reproduce

  1. see code sample above for entity mapping
  2. create an entity with subentity A and subentity B set and save it to the repository.

Environment Information

  • Error was observed in io.micronaut.data:micronaut-data-runtime:4.8.1 after an upgrade to micronaut-parent 4.5.0
  • When downgrading to Micronaut 4.4.3 which is using io.micronaut.data:micronaut-data-runtime:4.7.1 the code behaves as expected.

Example Application

No response

Version

4.5.0

Fixed by #3025