Can't save entity with immutable collection twice within Transaction using Spring Data JPA / Hibernate

Viewed 631

I have the following service layer function in my Spring Boot + Spring Data JPA application:

@Service
public class MyService {

    @Autowired
    MyEntityRepository repository;

    @Transactional
    public void serviceMethod() {
        MyEntity newEntity = new MyEntity("code", "name", "description");
        newEntity = repository.save(newEntity); // --> all good here

        // ...

        newEntity.setName("updated name");
        newEntity = repository.save(newEntity); // --> all good here

        Set<ChildEntity> newChildrenEntities = Set.of(new ChildEntity("childName"));
        newEntity.setChildren(newChildrenEntities);
        newEntity = repository.save(newEntity); // --> Exception here!
        
        // ...
    }
}

And when the execution reaches the last save method, an Exception is raised:

java.lang.UnsupportedOperationException: null
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:73) ~[na:na]
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.clear(ImmutableCollections.java:79) ~[na:na]
    at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:581) ~[hibernate-core-5.4.23.Final.jar:5.4.23.Final]
    at org.hibernate.type.CollectionType.replace(CollectionType.java:757) ~[hibernate-core-5.4.23.Final.jar:5.4.23.Final]
    at org.hibernate.type.TypeHelper.replace(TypeHelper.java:167) ~[hibernate-core-5.4.23.Final.jar:5.4.23.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:451) ~[hibernate-core-5.4.23.Final.jar:5.4.23.Final]
...

If I switch the order of the operations, then the exception raises on the second method as well:

@Transactional
public void serviceMethod() {
    MyEntity newEntity = new MyEntity("code", "name", "description");

    Set<ChildEntity> newChildrenEntities = Set.of(new ChildEntity("childName"));
    newEntity.setChildren(newChildrenEntities);
    newEntity = repository.save(newEntity); // --> all good here

    newEntity.setName("updated name");
    newEntity = repository.save(newEntity); // --> Exception here!
}

However, if I change my newChildrenEntities implementation to a modifiable collection:

@Transactional
public void serviceMethod() {
    MyEntity newEntity = new MyEntity("code", "name", "description");
    newEntity = repository.save(newEntity); // --> all good here

    // ...

    newEntity.setName("updated name");
    newEntity = repository.save(newEntity); // --> all good here

    Set<ChildEntity> newChildrenEntities = new HashSet<>();
    newChildrenEntities.add(new ChildEntity("childName"));
    newEntity.setChildren(newChildrenEntities);
    newEntity = repository.save(newEntity); // --> all good here!
        
    // ...
}

Or if I remove the @Transactional annotation from my method, then everything works ok.

I'd like to know why exactly this happens, it seems related to how Hibernate handles collections.

1 Answers

Since Hibernate needs to control/alter the state of your entities when managing flushing/refreshing/loading, you see this exception during auto-flushing. I would suggest you don't try to use immutable collections for the entity mappings. You can instead make sure to expose only immutable versions to users of the API, but Hibernate might need to change the contents. If you think this is a bug or can be improved to support your use case, you can also create an issue in the issue tracker(https://hibernate.atlassian.net) with a test case(https://github.com/hibernate/hibernate-test-case-templates/blob/master/orm/hibernate-orm-5/src/test/java/org/hibernate/bugs/JPAUnitTestCase.java) that reproduces the issue.

Related