UnsupportedOperationException merge-saving many-to-many relation with hibernate and JPA

HibernateJpaMany to-Many

Hibernate Problem Overview


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?

Hibernate Solutions


Solution 1 - Hibernate

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.

Solution 2 - Hibernate

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPeteView Question on Stackoverflow
Solution 1 - HibernateLukas EderView Answer on Stackoverflow
Solution 2 - HibernateSakshi SachdevaView Answer on Stackoverflow