DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • The First Annual Recap From JPA Buddy
  • CRUD Operations on Deeply Nested Comments: Scalable Spring Boot and Spring Data approach
  • Introduction to Spring Data JPA - Part 6 Bidirectional One to One Relations
  • Introduction to Spring Data JPA — Part 4 Bidirectional One-to-Many Relations

Trending

  • Orchestrating Microservices with Dapr: A Unified Approach
  • Secrets Sprawl and AI: Why Your Non-Human Identities Need Attention Before You Deploy That LLM
  • How Kubernetes Cluster Sizing Affects Performance and Cost Efficiency in Cloud Deployments
  • Scaling DevOps With NGINX Caching: Reducing Latency and Backend Load
  1. DZone
  2. Data Engineering
  3. Databases
  4. Introduction to Spring Data JPA Part 8: Many-to-Many Bidirectional

Introduction to Spring Data JPA Part 8: Many-to-Many Bidirectional

By 
Vinu Sagar user avatar
Vinu Sagar
·
May. 17, 20 · Tutorial
Likes (8)
Comment
Save
Tweet
Share
63.3K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we will discuss the following:

  • Many to Many Bidirectional.
  • @JsonIdentityInfo.
  • @JoinTable with JoinColumns and InverseJoins.
  • Bad Effects of CascadeType.ALL, Refactoring the code.
  • Using Model Classes.
  • Replacing Annotations like @JsonIdentityInfo, @JsonManagedReference, @JsonIgnore - Refactoring the code for GET Requests.

Let us start by modeling the entities:
Modeling entitiesYou can see the definition of Role in the User entity and the definition of User in Role entity. Hence, we can call it bidirectional. The requirement is that one User can have many Roles, and one Role can be associated with many Users. Hence, it is a Many-to-Many relationship. Let us see what Hibernate gives us by default.
Hibernate generated relationshipA mapping table with users_id and roles_id. Now, let us look at the entities. 

Please find the source code at https://github.com/gudpick/jpa-demo/tree/many-to-many-bidirectional-starter

Let us look at the main changes we have made.

The Role Entity

Java
xxxxxxxxxx
1
13
 
1
@Entity
2
@Table(name = "t_role")
3
@JsonIdentityInfo(
4
        generator = ObjectIdGenerators.PropertyGenerator.class,
5
        property = "id")
6
public class Role  {
7
    @Id
8
    @GeneratedValue(strategy = GenerationType.IDENTITY)
9
    private Long id;
10
    private String name;
11
    private String description;
12
    @ManyToMany(targetEntity = User.class, mappedBy = "roles", cascade = CascadeType.ALL)
13
    private List<User> users;


The User Entity

Java
xxxxxxxxxx
1
16
 
1
@Entity
2
@Table(name = "t_user")
3
@JsonIdentityInfo(
4
        generator = ObjectIdGenerators.PropertyGenerator.class,
5
        property = "id")
6
public class User  {
7
    @Id
8
    @GeneratedValue(strategy = GenerationType.IDENTITY)
9
    private Long id;
10
    private String firstName;
11
    private String lastName;
12
    private String mobile;
13
    @Column(unique = true)
14
    private String email;
15
    @ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL )
16
    private List<Role> roles;


One major change we can find here is the definition of relations.

Java
xxxxxxxxxx
1
 
1
@ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL )
2
private List<Role> roles;
3
4
@ManyToMany(targetEntity = User.class, mappedBy = "roles", cascade = CascadeType.ALL)
5
private List<User> users;


Here, Role will be the parent entity, and we are using mappedBy="role" in the role entity. The @ManytoMany annotation shows that it is a Many to Many relationship, and using @ManytoMany annotations at both sides of the relation (i.e.: in Role Entity and User Entity) shows that it is a bidirectional relation.

Now, we are using a new annotation @JsonIdentityInfo. In the previous aricles, we have StackOverflow errors due to circular references. We have been using @JsonIgnore, @JsonManagedReference, and @JsonBackReference to take care of the error. This new annotation, @JsonIdentityInfo, will handle the circular reference errors for us.

We are having the User Controller as follows.

Java
xxxxxxxxxx
1
22
 
1
@PostMapping("/user/create")
2
public ResponseEntity<Object> createUser(@RequestBody User user) {
3
    return userService.createUser(user);
4
}
5
@GetMapping("/user/details/{id}")
6
public User getUser(@PathVariable Long id) {
7
    if(userRepository.findById(id).isPresent())
8
        return userRepository.findById(id).get();
9
    else return  null;
10
}
11
@GetMapping("/user/all")
12
public List<User> getUsers() {
13
    return userRepository.findAll();
14
}
15
@PutMapping("/user/update/{id}")
16
public ResponseEntity<Object> updateUser(@PathVariable Long id, @RequestBody User user) {
17
    return userService.updateUser(user, id);
18
}
19
@DeleteMapping("user/delete/{id}")
20
public ResponseEntity<Object> deleteUser(@PathVariable Long id) {
21
    return userService.deleteUser(id);
22
}


RoleController

Java
xxxxxxxxxx
1
22
 
1
@PostMapping("/role/create")
2
public ResponseEntity<Object> createRole(@RequestBody Role role) {
3
    return  roleService.addRole(role);
4
}
5
@DeleteMapping("/role/delete/{id}")
6
public ResponseEntity<Object> deleteRole(@PathVariable Long id) {
7
    return roleService.deleteRole(id);
8
}
9
@GetMapping("/role/details/{id}")
10
public Role getRole(@PathVariable Long id) {
11
    if(roleRepository.findById(id).isPresent())
12
        return roleRepository.findById(id).get();
13
    else return null;
14
}
15
@GetMapping("/role/all")
16
public List<Role> getRoles() {
17
    return roleRepository.findAll();
18
}
19
@PutMapping("/role/update/{id}")
20
public ResponseEntity<Object> updateRole(@PathVariable Long id, @RequestBody Role role) {
21
    return roleService.updateRole(id, role);
22
}


Now, let us run the application. Let us create a user with two roles using the following JSON object.

POST : localhost:2003/user/create

JSON
xxxxxxxxxx
1
17
 
1
    {
2
            "firstName": "Hello Good Afternoon",
3
            "lastName":"world",
4
            "mobile": "9876435234",
5
            "email":"hi@mail.com",
6
            "roles": [
7
                {
8
                    "name": "MANAGER",
9
                    "description": "Mid Level Managers"
10
                },
11
                {
12
                    "name" : "ACCOUNTS",
13
                    "description": "ACCOUNTS USERS"
14
                }
15
                ]
16
        }
17


This will give a success message. Now, let us create a Role with two users.

POST: localhost:2003/role/create

JSON
xxxxxxxxxx
1
18
 
1
{
2
    "name": "ADMIN",
3
    "description": "Administrator",
4
    "users": [
5
        {
6
            "firstName": "hello",
7
            "lastName":"world",
8
            "mobile": "9876435234",
9
            "email":"hello@mail.com"
10
        },
11
        {
12
            "firstName": "Hello Good Morning",
13
            "lastName":"world",
14
            "mobile": "9876435234",
15
            "email":"world@mail.com"
16
        }
17
        ]
18
}


It should return a success message. Let us see the @JsonIdentityInfo impact on the GET requests. You can try removing this annotation and get the circular reference errors. 

GET: localhost:2003/user/all

You should get the result as follows.

JSON
xxxxxxxxxx
1
27
 
1
[
2
    {
3
        "id": 1,
4
        "firstName": "Hello Good Afternoon",
5
        "lastName": "world",
6
        "mobile": "9876435234",
7
        "email": "hi@mail.com",
8
        "roles": [
9
            {
10
                "id": 1,
11
                "name": "MANAGER",
12
                "description": "Mid Level Managers",
13
                "users": [
14
                    1
15
                ]
16
            },
17
            {
18
                "id": 2,
19
                "name": "ACCOUNTS",
20
                "description": "ACCOUNTS USERS",
21
                "users": [
22
                    1
23
                ]
24
            }
25
        ]
26
    }
27
]


and let us check for the user with id as 1.

GET: localhost:2003/user/details/1

The result will be:

JSON
xxxxxxxxxx
1
25
 
1
{
2
    "id": 1,
3
    "firstName": "Hello Good Afternoon",
4
    "lastName": "world",
5
    "mobile": "9876435234",
6
    "email": "hi@mail.com",
7
    "roles": [
8
        {
9
            "id": 1,
10
            "name": "MANAGER",
11
            "description": "Mid Level Managers",
12
            "users": [
13
                1
14
            ]
15
        },
16
        {
17
            "id": 2,
18
            "name": "ACCOUNTS",
19
            "description": "ACCOUNTS USERS",
20
            "users": [
21
                1
22
            ]
23
        }
24
    ]
25
}


Now, let us try the update.

PUT: localhost:2003/user/update/3

JSON
xxxxxxxxxx
1
13
 
1
    {
2
            "firstName": "Hello Good Evening",
3
            "lastName":"world",
4
            "mobile": "9876435234",
5
            "email":"hi@mail.com",
6
            "roles": [
7
                {
8
                    "name": "QC",
9
                    "description": "QC Managers"
10
                }
11
            
12
                ]
13
        }


It should give a success message. Now, let us try the delete method. We are using the Cascade type as ALL.

Now, if you check the database, we find four roles and three users. Let us try deleting the role with ID 1 and role with ID 4.

localhost:2003/role/delete/1

DELETE : localhost:2003/role/delete/4

Now, if you check the database you will find that there are no users present in the database. Now, let us run the Application again. Assuming that you have the following settings in the application.properties file.

Properties files
xxxxxxxxxx
1
 
1
spring.jpa.hibernate.ddl-auto=create


POST: localhost:2003/role/create

POST: localhost:2003/user/create

DELETE: localhost:2003/user/delete/1

DELETE: localhost:2003/user/delete/3

Now, you will see that all the data in the database is deleted. Well, this is not what is intended. Let us refactor our code and make sure we get the desired result.

Java
xxxxxxxxxx
1
 
1
@ManyToMany(targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH} )
2
private List<Role> roles;
3
4
@ManyToMany(targetEntity = User.class, mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH})
5
private List<User> users;


I am removing CascadeType.ALL and add making changes as above. The intention is to remove CascadeType.Remove, which was causing havoc in the delete operation. Now, when we run the application and try the following. Please use the JSON objects as given before.

POST: localhost:2003/role/create

POST:localhost:2003/user/create

Now, let us try deleting a role as below.

DELETE: localhost:2003/role/delete/1

We will get an exception as below.

 could not execute statement; SQL [n/a];

 constraint [fkj47yp3hhtsoajht9793tbdrp4]; 

nested exception is org.hibernate.exception.ConstraintViolationException:

 could not execute statement 

Well, let us correct this by making a small change in the deleteRole method. Consider this as a starting point. You can move on from here. Keep me posted in the comment section for better ways of doing it. Will discuss and debate. 

Java
xxxxxxxxxx
1
 
1
public ResponseEntity<Object> deleteRole(Long id) {
2
    if(roleRepository.findById(id).isPresent()){
3
        roleRepository.deleteById(id);
4
        if(roleRepository.findById(id).isPresent()){
5
            return ResponseEntity.unprocessableEntity().body("Failed to delete the specified record");
6
        } else return ResponseEntity.ok().body("Successfully deleted specified record");
7
    } else
8
        return ResponseEntity.unprocessableEntity().body("No Records Found");
9
}


Let us change this method as follows

Java
xxxxxxxxxx
1
11
 
1
public ResponseEntity<Object> deleteRole(Long id) {
2
    if(roleRepository.findById(id).isPresent()){
3
        if(roleRepository.getOne(id).getUsers().size() == 0) {
4
            roleRepository.deleteById(id);
5
            if (roleRepository.findById(id).isPresent()) {
6
                return ResponseEntity.unprocessableEntity().body("Failed to delete the specified record");
7
            } else return ResponseEntity.ok().body("Successfully deleted specified record");
8
        } else return ResponseEntity.unprocessableEntity().body("Failed to delete,  Please delete the users associated with this role");
9
    } else
10
        return ResponseEntity.unprocessableEntity().body("No Records Found");
11
}


I have introduced a check to find whether there are any users associated with the role.

Java
xxxxxxxxxx
1
 
1
if(roleRepository.getOne(id).getUsers().size() == 0)


If true, we are not allowing to delete the role, so an exception will not be thrown. Instead, we get a custom message as below.

Plain Text
xxxxxxxxxx
1
 
1
Failed to delete. Please delete the users associated with this role


Please find source code at https://github.com/gudpick/jpa-demo/tree/many-to-many-bidirectional-refactor-delete-cascade.

Let us discuss one more way of defining the Mappings.

User Entity

Java
xxxxxxxxxx
1
 
1
@ManyToMany(targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH} )
2
@JoinTable(
3
        name="t_user_roles",
4
        joinColumns=
5
        @JoinColumn( name="user_id", referencedColumnName="id"),
6
        inverseJoinColumns=@JoinColumn(name="role_id", referencedColumnName="id"))
7
private List<Role> roles;


Role Entity

Java
xxxxxxxxxx
1
 
1
@ManyToMany(targetEntity = User.class, mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH})
2
private List<User> users;


Here, we are explicitly defining the mapping table. The name refers to the name of the table, which can be changed. Now, we have joinColumns, where we are defining the columns that are to be joined. Here, we are joining user_id and role_id, where user_id refers to the Id of the user table, and role_id refers to Id of the Role table. So, @JoinColumn will refer to the User table, and inverseJoinColumns will refer to the Role table.

We can also do as below.

Java
xxxxxxxxxx
1
 
1
@JoinTable(
2
        name="t_user_roles",
3
        joinColumns={
4
                @JoinColumn( name="user_email", referencedColumnName="email", nullable =true),
5
                @JoinColumn( name="user_mobile", referencedColumnName="mobile", nullable=true)
6
        },
7
        inverseJoinColumns=@JoinColumn(name="role_name", referencedColumnName="name"))


We will get a mapping table as below.Example mapping table

Please find source code at https://github.com/gudpick/jpa-demo/tree/many-to-many-bidirectional-define-mapping.

Now, my issue is regarding the get methods. If you see, the get methods give a result, as below, which we surely cannot appreciate.

JSON
xxxxxxxxxx
1
27
 
1
[
2
    {
3
        "id": 1,
4
        "firstName": "Hello Good Afternoon",
5
        "lastName": "world",
6
        "mobile": "9876435234",
7
        "email": "hi@mail.com",
8
        "roles": [
9
            {
10
                "id": 1,
11
                "name": "MANAGER",
12
                "description": "Mid Level Managers",
13
                "users": [
14
                    1
15
                ]
16
            },
17
            {
18
                "id": 2,
19
                "name": "ACCOUNTS",
20
                "description": "ACCOUNTS USERS",
21
                "users": [
22
                    1
23
                ]
24
            }
25
        ]
26
    }
27
]


Let us get it better. I am just introducing two model classes and making some changes in the code so that it gives me a better result.

UserModel

Java
xxxxxxxxxx
1
37
 
1
public class UserModel {
2
    private String firstName;
3
    private String lastName;
4
    private String mobile;
5
    private String email;
6
    private List<RoleModel> roles;
7
    public String getFirstName() {
8
        return firstName;
9
    }
10
    public void setFirstName(String firstName) {
11
        this.firstName = firstName;
12
    }
13
    public String getLastName() {
14
        return lastName;
15
    }
16
    public void setLastName(String lastName) {
17
        this.lastName = lastName;
18
    }
19
    public String getMobile() {
20
        return mobile;
21
    }
22
    public void setMobile(String mobile) {
23
        this.mobile = mobile;
24
    }
25
    public String getEmail() {
26
        return email;
27
    }
28
    public void setEmail(String email) {
29
        this.email = email;
30
    }
31
    public List<RoleModel> getRoles() {
32
        return roles;
33
    }
34
    public void setRoles(List<RoleModel> roles) {
35
        this.roles = roles;
36
    }
37
}


RoleModel

Java
xxxxxxxxxx
1
16
 
1
public class RoleModel {
2
    private String name;
3
    private String description;
4
    public String getName() {
5
        return name;
6
    }
7
    public void setName(String name) {
8
        this.name = name;
9
    }
10
    public String getDescription() {
11
        return description;
12
    }
13
    public void setDescription(String description) {
14
        this.description = description;
15
    }
16
}


Now, let us see the following methods in the UserService class.

Java
xxxxxxxxxx
1
38
 
1
public UserModel getUser(Long id) {
2
    if(userRepository.findById(id).isPresent()) {
3
        User user = userRepository.findById(id).get();
4
        UserModel userModel = new UserModel();
5
        userModel.setFirstName(user.getFirstName());
6
        userModel.setLastName(user.getLastName());
7
        userModel.setEmail(user.getEmail());
8
        userModel.setMobile(user.getMobile());
9
        userModel.setRoles( getRoleList(user));
10
        return userModel;
11
    } else return null;
12
}
13
public List<UserModel > getUsers() {
14
    List<User> userList = userRepository.findAll();
15
    if(userList.size()>0) {
16
        List<UserModel> userModels = new ArrayList<>();
17
        for (User user : userList) {
18
            UserModel model = new UserModel();
19
            model.setFirstName(user.getFirstName());
20
            model.setLastName(user.getLastName());
21
            model.setMobile(user.getMobile());
22
            model.setEmail(user.getEmail());
23
            model.setRoles(getRoleList(user));
24
            userModels.add(model);
25
        }
26
        return userModels;
27
    } else return new ArrayList<UserModel>();
28
}
29
private List<RoleModel> getRoleList(User user){
30
    List<RoleModel> roleList = new ArrayList<>();
31
    for(int i=0; i< user.getRoles().size(); i++) {
32
        RoleModel roleModel = new RoleModel();
33
        roleModel.setName(user.getRoles().get(i).getName());
34
        roleModel.setDescription(user.getRoles().get(i).getDescription());
35
        roleList.add(roleModel);
36
    }
37
    return roleList;
38
}


I am setting all the values to the model and returning the model. Have a look at the controller

UserController GET methods

Java
xxxxxxxxxx
1
10
 
1
@GetMapping("/user/details/{id}")
2
public UserModel getUser(@PathVariable Long id) {
3
    return userService.getUser(id);
4
}
5
@GetMapping("/user/all")
6
public List<UserModel> getUsers() {
7
    return userService.getUsers();
8
9
}
10
@PutMapping("/user/update/{id}")


Now, let us run the application and see all the GET methods.

POST: localhost:2003/role/create

POST: localhost:2003/user/create

Now, let us run the GET: localhost:2003/user/details/1

You get the result as below:

JSON
xxxxxxxxxx
1
12
 
1
{
2
    "firstName": "hello",
3
    "lastName": "world",
4
    "mobile": "9876435234",
5
    "email": "hello@mail.com",
6
    "roles": [
7
        {
8
            "name": "ADMIN",
9
            "description": "Administrator"
10
        }
11
    ]
12
}


GET: localhost:2003/user/all

JSON
xxxxxxxxxx
1
42
 
1
[
2
    {
3
        "firstName": "hello",
4
        "lastName": "world",
5
        "mobile": "9876435234",
6
        "email": "hello@mail.com",
7
        "roles": [
8
            {
9
                "name": "ADMIN",
10
                "description": "Administrator"
11
            }
12
        ]
13
    },
14
    {
15
        "firstName": "Hello Good Morning",
16
        "lastName": "world",
17
        "mobile": "9876435234",
18
        "email": "world@mail.com",
19
        "roles": [
20
            {
21
                "name": "ADMIN",
22
                "description": "Administrator"
23
            }
24
        ]
25
    },
26
    {
27
        "firstName": "Hello Good Afternoon",
28
        "lastName": "world",
29
        "mobile": "9876435234",
30
        "email": "hi@mail.com",
31
        "roles": [
32
            {
33
                "name": "MANAGER",
34
                "description": "Mid Level Managers"
35
            },
36
            {
37
                "name": "ACCOUNTS",
38
                "description": "ACCOUNTS USERS"
39
            }
40
        ]
41
    }
42
]


We have many options now. @JsonIgnore, @JsonManageReference, @JsonIdentityInfo, and writing code to format the JSON. You are a better judge for what suits you and what your requirements are.

Please find source code at https://github.com/gudpick/jpa-demo/tree/many-to-many-bidirectional-getters.

Please find the video tutorials at:

Relational database Database Java (programming language) Spring Data JSON Data (computing) code style

Opinions expressed by DZone contributors are their own.

Related

  • The First Annual Recap From JPA Buddy
  • CRUD Operations on Deeply Nested Comments: Scalable Spring Boot and Spring Data approach
  • Introduction to Spring Data JPA - Part 6 Bidirectional One to One Relations
  • Introduction to Spring Data JPA — Part 4 Bidirectional One-to-Many Relations

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!