Search code examples
javahibernatespring-bootspring-data-jpahibernate-mapping

@ManyToMany saving relations is available only one time in one direction


I will try create @ManyToMany relation in my SpringBoot application. I use JPARespository, I create 2 models:

User

@Entity(name = "User")
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    private String name;

    @ManyToMany//(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @LazyCollection(value = LazyCollectionOption.FALSE)
    @JoinTable(
            name = "users_groups",
            joinColumns = @JoinColumn(name = "users_id"),
            inverseJoinColumns = @JoinColumn(name = "groups_id")
    )
    private Set<UGroup> groups;

    public User() {
        this.groups = new HashSet<>();
    }

    public void addGroup(UGroup uGroup) {
        this.groups.add(uGroup);
        uGroup.users.add(this);
    }

    public void removeGroup(UGroup uGroup) {
        this.groups.remove(uGroup);
        uGroup.users.remove(this);
    }
}

UGroups

@Data
@Entity(name = "UGroup")
@Table(name = "groups")
public class UGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    private String name;

    @ManyToMany(mappedBy = "groups", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @LazyCollection(value = LazyCollectionOption.FALSE)
    public Set<User> users;

    public UGroup() {
        this.users = new HashSet<>();
    }

    public void addUser(User user) {
        this.users.add(user);
        user.getGroups().add(this);
    }

    public void removeUser(User user) {
        this.users.remove(user);
        user.getGroups().remove(this);
    }
}

I load start data via DataConfiguration class:

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        User user = new User();
        user.setName("Damian");
        userRepository.save(user);

        user = new User();
        user.setName("Marta");
        userRepository.save(user);

        user = new User();
        user.setName("Natalia");
        userRepository.save(user);

        UGroup uGroup = new UGroup();
        uGroup.setName("Mieszkanie");
        uGroup = uGroupRepository.save(uGroup);

        user = userRepository.findById(1).get();
        user.addGroup(uGroup);
        user = userRepository.save(user);

        // unable to execute
        /*
        user = userRepository.findById(2).get();
        user.addGroup(uGroup);
        user = userRepository.save(user);
         */

        // unable to execute 2
        /*
        uGroup = uGroupRepository.findById(4).get();
        uGroup.addUser(userRepository.findById(3).get());
        uGroup = uGroupRepository.save(uGroup);
         */
    }

When i execute only insert group for 1 user all is good. Although when I try execute insert group for 1 and 2 users or insert 3 user for group it cause java.lang.StackOverflowError: null exception.

I don know where is error. Can anyone help me and explain me where and why I should change something? Thanks in advance.


Solution

  • With Join Table,it it possible to map ManyToMany bidirectional relation.These are the modified entity classes

    import org.hibernate.annotations.LazyCollection;
    import org.hibernate.annotations.LazyCollectionOption;
    
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    @Entity(name = "User")
    @Table(name = "users")
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String name;
    
        public Integer getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public Set<UGroup> getGroups() {
            return groups;
        }
    
        public void setGroups(Set<UGroup> groups) {
            this.groups = groups;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @ManyToMany(mappedBy = "users",cascade = CascadeType.ALL)
        @LazyCollection(value = LazyCollectionOption.FALSE)
        private Set<UGroup> groups=new HashSet<UGroup>();
    
        public User() {
        }
    
        public void addGroup(UGroup uGroup) {
            this.groups.add(uGroup);
            uGroup.users.add(this);
        }
    
        public void removeGroup(UGroup uGroup) {
            this.groups.remove(uGroup);
            uGroup.users.remove(this);
        }
    }
    
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    @Entity(name = "UGroup")
    @Table(name = "groups")
    public class UGroup {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String name;
    
        @ManyToMany(cascade = CascadeType.ALL)
        @JoinTable(name = "USER_GROUP_MAP" ,joinColumns = @JoinColumn(name="GROUP_ID",referencedColumnName = "ID")
                ,inverseJoinColumns = @JoinColumn(name="USER_ID",referencedColumnName = "ID"))
        @LazyCollection(value = LazyCollectionOption.FALSE)
        public Set<User> users=new HashSet<>();
    
        public UGroup() {
        }
    
        public void addUser(User user) {
            this.users.add(user);
            user.addGroup(this);
        }
    
        public void removeUser(User user) {
            this.users.remove(user);
            user.removeGroup(this);
        }
    
        public Integer getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Set<User> getUsers() {
            return users;
        }
    
        public void setUsers(Set<User> users) {
            this.users = users;
        }
    }
    

    For ManyToMany relations join table is necessary.

    SELECT * FROM USER_GROUP_MAP;
    GROUP_ID    USER_ID  
    4            1
    4            2
    4            3
    

    This is the result with the code you have posted.You should override hashcode and equals method in both the entity classes in order to avoid duplication of data in the tables.

    Code used for testing the above relationship mappings.

    @Override
    public void onApplicationEvent(ContextRefreshedEvent applicationEvent) {
        User user = new User();
        user.setName("Damian");
        userRepository.save(user);
    
        user = new User();
        user.setName("Marta");
        userRepository.save(user);
    
        user = new User();
        user.setName("Natalia");
        userRepository.save(user);
    
        UGroup uGroup = new UGroup();
        uGroup.setName("Mieszkanie");
        uGroup = uGroupRepository.save(uGroup);
    
        user = userRepository.findById(1).get();
        user.addGroup(uGroup);
        user = userRepository.save(user);
    
        // unable to execute
    
        user = userRepository.findById(2).get();
        user.addGroup(uGroupRepository.findById(4).get());
        user = userRepository.save(user);
    
    
        // unable to execute 2
    
        uGroup = uGroupRepository.findById(4).get();
        uGroup.addUser(userRepository.findById(3).get());
        uGroup = uGroupRepository.save(uGroup);
    
    }