Search code examples
springhibernatespring-data-jpahibernate-mappingspring-rest

How to update @OneToMany Hibernate relationship


I can not update the list of dependent objects. I have an API which should update the list of accounts with clients. One Client - many Accounts.

I configured @OneToMany as indicated for proper update:

@OneToMany(mappedBy = "client", orphanRemoval = true, cascade = CascadeType.ALL)

Entities:

@Entity
@Getter
@Setter
public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "id_client")
    private Integer id;

    private String name;
    private int age;

    @OneToMany(mappedBy = "client", orphanRemoval = true, cascade = CascadeType.ALL)
    private List<Account> accounts = new ArrayList<>();
}

@Entity
@Getter
@Setter
public class Account {
    @Id
    @GeneratedValue
    @Column(name = "id_account")
    private Integer id;

    private int amount;
    private String currency;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
}

What I want to do on the data presented. I already have a client (id = 100) with two accounts (id = 10, 11).

I want to update so that the client has a different list of accounts id: 10, 12.

My test with test data.

<dataset>
    <Client id_client="100" name="John" age="23"/>
    <Client id_client="101" name="Mike" age="28"/>
    <Client id_client="102" name="Kevin" age="19"/>

    <Account id_account="10" amount="50" currency="USD" id_client="100"/>
    <Account id_account="11" amount="100" currency="USD" id_client="100"/>
    <Account id_account="12" amount="150" currency="EUR" id_client="101"/>
    <Account id_account="13" amount="200" currency="EUR" id_client="102"/>
</dataset>

Test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@TestExecutionListeners({
        TransactionalTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class,
        DbUnitTestExecutionListener.class
})
@Transactional
@DatabaseSetup("/data.xml")
public class HibTest {

    @PersistenceContext
    protected EntityManager em;

    protected Session session;

    @Before
    public void dbAllSet() {
        session = em.unwrap(Session.class);
    }

    @Test
    @Commit
    public void mergeCollections() {
        Client client = session.get(Client.class, 100); // with accounts: 10, 11

        List<Account> newUpdatedListAccount = newUpdatedListAccount();

        client.getAccounts().clear();
        client.getAccounts().addAll(newUpdatedListAccount);

        session.saveOrUpdate(client);
        session.flush();

        Account account12 = session.get(Account.class, 12);
        System.out.println(account12.getClient().getId()); // 101 nothing has changed, must be 100
    }

    private List<Account> newUpdatedListAccount() {
        ArrayList<Account> accounts = new ArrayList<>();
        accounts.add(session.get(Account.class, 12)); // new account from other client
        accounts.add(session.get(Account.class, 10)); // existing account in updated client
        return accounts;
    }
}

But the update does not work. And the update is not displayed in the sql log. How to update correctly? This is a very frequent case.


Solution

  • You have to set the 'client' in the account or much better - use an addAccount method:

    Your Client class

    @Entity @Getter @Setter
    public class Client {
    
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE)
        @Column(name = "id_client")
        private Integer id;
    
        private String name;
        private int age;
    
        // Here you say that hibernate shall use the field `client` in account for mapping !!!
        @OneToMany(mappedBy = "client", orphanRemoval = true, cascade = CascadeType.ALL)
        private List<Account> accounts = new ArrayList<>();
    
        // This make sure that our bidi-relation works well
        public void addAccount(Account account){
            accounts.add(account);
            if( account.getClient() != this ) {
                account.setClient(this);
            }
        }
    
        // This is convenient method
        public void addAccounts(Collection<Account> accounts){
            for( Account account : accounts ){
                this.addAccount(account);
            }
        }
    
        // And if you remove an account you have to remove the `client` from the account
        public void removeAccount(int id){
            for( Account account : accounts ){
                if( Objects.equals(account.getId(), id) ){
                    accounts.remove(account);
                    account.setClient(null);
                    break;
                }
            }
        }
    
        void clearAccounts() {
            for( Account account : accounts ){
                account.setClient(null);
            }
            accounts.clear();
        }
    
        // We lose control if anybody can set it's own list.
        public void setAccounts(List<Account> accounts){
            throw new UnsupportedOperationException("Do not use this");
        }
    
        // Same here - We lose control if anybody can change our List
        public List<Account> getAccounts (){
            return Collections.unmodifiableList(accounts);
        }
    
    }
    

    Your Account Class

    @Entity @Getter @Setter
    @Table(name = "accounts")
    public class Account {
    
        @Id
        @GeneratedValue
        @Column(name = "id_account")
        private Integer id;
    
        private int amount;
        private String currency;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "id_client")
        private Client client;
    
        // This make sure that our bidi-relation works well
        public void setClient(Client client){
            this.client = client;
            if( client != null && ! client.getAccounts().contains(this) ){
                client.addAccount(this);
            }
        }
    }
    

    Your Test

    public class HibTest {
    
        @PersistenceContext
        protected EntityManager em;
    
        protected Session session;
    
        @Before
        public void dbAllSet() {
            session = em.unwrap(Session.class);
        }
    
        @Test
        @Commit
        public void mergeCollections() {
            Client client = (Client) session.get(Client.class, 100); // with accounts: 10, 11
    
            List<Account> newUpdatedListAccount = newUpdatedListAccount();
    
    
            /* YOUR CODE
             *
             * You tell hibernate to clear the list with the account. If you save your changes nothing will happen because
             * you have told hibernate that the relation is the `client` field in the Account class (mappedby="client") and 
             * we didn't change the `client` field in the Account class.
             */
             // client.getAccounts().clear();
    
            /*
             * Same here - you add accounts with a reference to Client with ID 101 and we did not change it.
             */
            // client.getAccounts().addAll(newUpdatedListAccount);
    
    
            // do not use the client.getAccounts() list directly
            client.clearAccounts();       
            client.addAccounts(newUpdatedListAccount);
    
            session.saveOrUpdate(client);
            session.flush();
    
            Account account12 = (Account) session.get(Account.class, 12);
            System.out.println(account12.getClient().getId()); // 101 nothing has changed, must be 100
        }
    
        private List<Account> newUpdatedListAccount() {
            ArrayList<Account> accounts = new ArrayList<>();
            accounts.add((Account) session.get(Account.class, 12)); // new account from other client
            accounts.add((Account) session.get(Account.class, 10)); // existing account in updated client
            return accounts;
        }
    }