63

I've set up a simple many-to-many relationship account : role with Hibernate but when I try to save an account in a unit test after it has had its role added I get an UnsupportedOperationException:

java.lang.UnsupportedOperationException
    at java.util.AbstractList.remove(AbstractList.java:144)
    at java.util.AbstractList$Itr.remove(AbstractList.java:360)
    at java.util.AbstractList.removeRange(AbstractList.java:559)
    at java.util.AbstractList.clear(AbstractList.java:217)
    at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:502)
    at org.hibernate.type.CollectionType.replace(CollectionType.java:582)
    at org.hibernate.type.TypeHelper.replace(TypeHelper.java:178)
    at org.hibernate.event.def.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:563)
    at org.hibernate.event.def.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:288)
    at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:261)
    at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
    at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:867)
    at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:851)
    at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:855)
    at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:686)
    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.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:240)
    at $Proxy33.merge(Unknown Source)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:360)
    at ....JpaProvider.save(JpaProvider.java:161)
    at ....DataModelTest.testAccountRole(DataModelTest.java:47)
    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.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:240)
    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.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:180)
    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)

What's going wrong here? Is my entity setup faulty or is this a hibernate or JPA limitation forcing me to split apart my m:m relationship into 3 1:n relations modeling the m:n relationship table as well (which I wanted to avoid since it does not have any additional information). I've modeled other 1:n entities in my prototype and that seemed to work out nicely so far...

Here's my setup, any thoughts whether it might be faulty are appreciated.

Entities (simplified):

@Entity
@Table(name="account")
public class Account extends AbstractPersistable<Long> {

    private static final long serialVersionUID = 627519641892468240L;

    private String username;


    @ManyToMany
    @JoinTable( name = "account_roles", 
                joinColumns = { @JoinColumn(name = "account_id")}, 
                inverseJoinColumns={@JoinColumn(name="role_id")})  
    private List<Role> roles;   


    public List<Role> getRoles() {
        return roles;
    }
    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }



    @Entity
    @Table(name="role")
    public class Role extends AbstractPersistable<Long> {

        private static final long serialVersionUID = 8127092070228048914L;

        private String name;

        @ManyToMany
        @JoinTable( name = "account_roles",   
                    joinColumns={@JoinColumn(name="role_id")},   
                    inverseJoinColumns={@JoinColumn(name="account_id")})  
        private List<Account> accounts;


        public List<Account> getAccounts() {
            return accounts;
        }

        public void setAccounts(List<Account> accounts) {
            this.accounts = accounts;
        }

Unit Test:

@TransactionConfiguration
@ContextConfiguration({"classpath:dw-security-context-test.xml"})
@Transactional
@RunWith(SpringJUnit4ClassRunner.class)
public class DataModelTest {

    @Inject
    private AccountProvider accountProvider;    

    @Inject 
    private RoleProvider roleProvider;

    @Before
    public void mockAccountRolePermission(){
        Account account = MockAccount.getSavedInstance(accountProvider);
        Role role = MockRole.getSavedInstance(roleProvider);
    }

    @Test
    public void testAccountRole(){      
        Account returnedAccount = accountProvider.findAll().get(0);
        returnedAccount.setRoles(Arrays.asList(roleProvider.findAll().get(0)));
        accountProvider.save(returnedAccount);

    }
}

MockAccount (same for MockRole):

public class MockAccount {

    public static Account getInstance(){
        Account account = new Account();
        account.setUsername(RandomData.rndStr("userName-", 5));
        return account;
    }

    public static Account getSavedInstance(AccountProvider accountProvider){
        Account account = getInstance();
        accountProvider.save(account);
        return account;
    }

}

And finally the Provider:

@Repository
public class AccountProvider extends JpaProvider<Account, Long> {

}

where JPAProvider just wraps a lot of JPARepository methods (at least as far as it is important in this case):

public abstract class JpaProvider<T extends Object, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryDslPredicateExecutor<T> {
...
}

Any Ideas on why the save might be an UnsupportedOperation?

Pete
  • 10,720
  • 25
  • 94
  • 139

4 Answers4

131

It is because of your

Arrays.asList(roleProvider.findAll().get(0))

This creates an unmodifiable list (in fact, a non-resizable list). Hibernate seems to expect a modifiable list. Try using this instead:

public void testAccountRole(){      
    Account returnedAccount = accountProvider.findAll().get(0);

    List<Role> list = new ArrayList<Role>();
    list.add(roleProvider.findAll().get(0));    
    returnedAccount.setRoles(list);  

    accountProvider.save(returnedAccount);
}

This solution won't explain why exactly you got the other exception (might be documented in the Hibernate docs), but it might be a valid workaround.

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Hey, thanks for the instant reply! I'm banging my head on the table right this moment! I had it just like that when my colleague looking over my should said "why don't you do it like that..?" ;) Ah well.. working again, happy me! – Pete Sep 15 '11 at 09:05
  • 2
    I had the same issue with Kotlin code that was using List (thus immutable), and spent a few hours before i stumbled upon this answer. I changed my code to use MutableList and it works. – gotson Aug 15 '19 at 03:36
  • It really saved my life!! The only thing that saddens me that even with the lowest level of logs enabled, it could not show any relevant information to help find the real cause. – shashwat Aug 23 '19 at 17:31
  • Same here with Kotlin! changed my "toList()" to "toMutableList()" and everything was fine! Fortunately i found this answer immediately. You saved me hours XD – Tommaso Resti Oct 13 '19 at 17:35
  • I still don't understand why this is the case, but it works. Any link that explains this is appreciated. – Mercurial Nov 22 '21 at 19:24
  • I was using `List list = Collections.singleton(obj)`. I changed it to ArrayList and fixed it like a charm! – Raihanul Alam Hridoy Dec 15 '22 at 11:17
  • 1
    If you are using any kind of stream, collect it using .collect(Collectors.toCollection(ArrayList::new)) instead of simply .toList(). toList method returns unmodifiable list, that causes the exception afterwards – Yuriy Kirel Jun 12 '23 at 05:29
8

Hibernate's persistent variant of the Collection in question attempts to delegate to an abstract base class (PersistenceBag) that doesn't implement the add method.

Thomas Fritsch
  • 9,639
  • 33
  • 37
  • 49
  • 2
    This was the problem in my case. The solution was to get the PersistenceBag-based collection from the Hibernate managed entity, add it to a unmanaged arraylist, then add my new entry to that arraylist, then call the setter that collection on the Hibernate managed entity. – Jazzepi Apr 12 '19 at 19:01
  • @Jazzepi - is there a better way to do this? This seems like the only option I have at the moment, but surely there's a better way to tell Hibernate to use a modifiable list or to add to the list using some hibernate-friendly way, right? – Kosi Oct 18 '22 at 19:33
2

Weired thing. I tried to enclose my problem to:

SubstanceEntity substance = this.substanceRepository.findById(id).get();
this.substanceRepository.save(substance);

and the error was still present. I figured out that the problem is related to my springboot tests. Outside of the tests it doesent matter if I created an child entity using List.of(...) as long i dont wana make change on this instance, because When I load the list from my DB it becomes mutable. I think that inside the test environment the persisted entites are not realy persisted and parts of the entity returned from my repository can be still imutable.

So my solution was: Just NEVER use List.of (or similar imutable factories) inside my tests.

Patrick
  • 21
  • 3
0

I also had this issue and based on the stack trace also surmised that the issue was the collection being immutable. this was before coming across this post. However I spent hours trying all sorts of solution but couldn't get around the exception.
It's not that the accepted answer by Lukas Eder isn't correct, my issue was that the set of objects I was trying to persist also had collections within each object (relational objects); I therefore had to re-initialise each sub collection with a mutable collection before I was able to get around the exception.
The oddity though, in my case was that in production code, these sub-collections were immutable and saveAll() works just fine, but in my (JUnit) test context, the exception was thrown.

Dark Star1
  • 6,986
  • 16
  • 73
  • 121