Introduction to Spring Data JPA Part 8: Many-to-Many Bidirectional
Join the DZone community and get the full member experience.
Join For FreeIn 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:
You 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.
A 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
xxxxxxxxxx
name = "t_role") (
(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Role {
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private String description;
targetEntity = User.class, mappedBy = "roles", cascade = CascadeType.ALL) (
private List<User> users;
The User Entity
xxxxxxxxxx
name = "t_user") (
(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User {
strategy = GenerationType.IDENTITY) (
private Long id;
private String firstName;
private String lastName;
private String mobile;
unique = true) (
private String email;
targetEntity = Role.class,cascade = CascadeType.ALL ) (
private List<Role> roles;
One major change we can find here is the definition of relations.
xxxxxxxxxx
targetEntity = Role.class,cascade = CascadeType.ALL ) (
private List<Role> roles;
targetEntity = User.class, mappedBy = "roles", cascade = CascadeType.ALL) (
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.
xxxxxxxxxx
"/user/create") (
public ResponseEntity<Object> createUser( User user) {
return userService.createUser(user);
}
"/user/details/{id}") (
public User getUser( Long id) {
if(userRepository.findById(id).isPresent())
return userRepository.findById(id).get();
else return null;
}
"/user/all") (
public List<User> getUsers() {
return userRepository.findAll();
}
"/user/update/{id}") (
public ResponseEntity<Object> updateUser( Long id, User user) {
return userService.updateUser(user, id);
}
"user/delete/{id}") (
public ResponseEntity<Object> deleteUser( Long id) {
return userService.deleteUser(id);
}
RoleController
xxxxxxxxxx
"/role/create") (
public ResponseEntity<Object> createRole( Role role) {
return roleService.addRole(role);
}
"/role/delete/{id}") (
public ResponseEntity<Object> deleteRole( Long id) {
return roleService.deleteRole(id);
}
"/role/details/{id}") (
public Role getRole( Long id) {
if(roleRepository.findById(id).isPresent())
return roleRepository.findById(id).get();
else return null;
}
"/role/all") (
public List<Role> getRoles() {
return roleRepository.findAll();
}
"/role/update/{id}") (
public ResponseEntity<Object> updateRole( Long id, Role role) {
return roleService.updateRole(id, role);
}
Now, let us run the application. Let us create a user with two roles using the following JSON object.
POST : localhost:2003/user/create
xxxxxxxxxx
{
"firstName": "Hello Good Afternoon",
"lastName":"world",
"mobile": "9876435234",
"email":"hi@mail.com",
"roles": [
{
"name": "MANAGER",
"description": "Mid Level Managers"
},
{
"name" : "ACCOUNTS",
"description": "ACCOUNTS USERS"
}
]
}
This will give a success message. Now, let us create a Role with two users.
POST: localhost:2003/role/create
xxxxxxxxxx
{
"name": "ADMIN",
"description": "Administrator",
"users": [
{
"firstName": "hello",
"lastName":"world",
"mobile": "9876435234",
"email":"hello@mail.com"
},
{
"firstName": "Hello Good Morning",
"lastName":"world",
"mobile": "9876435234",
"email":"world@mail.com"
}
]
}
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.
xxxxxxxxxx
[
{
"id": 1,
"firstName": "Hello Good Afternoon",
"lastName": "world",
"mobile": "9876435234",
"email": "hi@mail.com",
"roles": [
{
"id": 1,
"name": "MANAGER",
"description": "Mid Level Managers",
"users": [
1
]
},
{
"id": 2,
"name": "ACCOUNTS",
"description": "ACCOUNTS USERS",
"users": [
1
]
}
]
}
]
and let us check for the user with id as 1.
GET: localhost:2003/user/details/1
The result will be:
xxxxxxxxxx
{
"id": 1,
"firstName": "Hello Good Afternoon",
"lastName": "world",
"mobile": "9876435234",
"email": "hi@mail.com",
"roles": [
{
"id": 1,
"name": "MANAGER",
"description": "Mid Level Managers",
"users": [
1
]
},
{
"id": 2,
"name": "ACCOUNTS",
"description": "ACCOUNTS USERS",
"users": [
1
]
}
]
}
Now, let us try the update.
PUT: localhost:2003/user/update/3
xxxxxxxxxx
{
"firstName": "Hello Good Evening",
"lastName":"world",
"mobile": "9876435234",
"email":"hi@mail.com",
"roles": [
{
"name": "QC",
"description": "QC Managers"
}
]
}
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.
xxxxxxxxxx
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.
xxxxxxxxxx
targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH} ) (
private List<Role> roles;
targetEntity = User.class, mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH}) (
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.
xxxxxxxxxx
public ResponseEntity<Object> deleteRole(Long id) {
if(roleRepository.findById(id).isPresent()){
roleRepository.deleteById(id);
if(roleRepository.findById(id).isPresent()){
return ResponseEntity.unprocessableEntity().body("Failed to delete the specified record");
} else return ResponseEntity.ok().body("Successfully deleted specified record");
} else
return ResponseEntity.unprocessableEntity().body("No Records Found");
}
Let us change this method as follows
xxxxxxxxxx
public ResponseEntity<Object> deleteRole(Long id) {
if(roleRepository.findById(id).isPresent()){
if(roleRepository.getOne(id).getUsers().size() == 0) {
roleRepository.deleteById(id);
if (roleRepository.findById(id).isPresent()) {
return ResponseEntity.unprocessableEntity().body("Failed to delete the specified record");
} else return ResponseEntity.ok().body("Successfully deleted specified record");
} else return ResponseEntity.unprocessableEntity().body("Failed to delete, Please delete the users associated with this role");
} else
return ResponseEntity.unprocessableEntity().body("No Records Found");
}
I have introduced a check to find whether there are any users associated with the role.
xxxxxxxxxx
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.
xxxxxxxxxx
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
xxxxxxxxxx
targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH} ) (
(
name="t_user_roles",
joinColumns=
name="user_id", referencedColumnName="id"), (
inverseJoinColumns= (name="role_id", referencedColumnName="id"))
private List<Role> roles;
Role Entity
xxxxxxxxxx
targetEntity = User.class, mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.DETACH,CascadeType.MERGE,CascadeType.REFRESH}) (
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.
xxxxxxxxxx
(
name="t_user_roles",
joinColumns={
name="user_email", referencedColumnName="email", nullable =true), (
name="user_mobile", referencedColumnName="mobile", nullable=true) (
},
inverseJoinColumns= (name="role_name", referencedColumnName="name"))
We will get a mapping table as below.
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.
xxxxxxxxxx
[
{
"id": 1,
"firstName": "Hello Good Afternoon",
"lastName": "world",
"mobile": "9876435234",
"email": "hi@mail.com",
"roles": [
{
"id": 1,
"name": "MANAGER",
"description": "Mid Level Managers",
"users": [
1
]
},
{
"id": 2,
"name": "ACCOUNTS",
"description": "ACCOUNTS USERS",
"users": [
1
]
}
]
}
]
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
xxxxxxxxxx
public class UserModel {
private String firstName;
private String lastName;
private String mobile;
private String email;
private List<RoleModel> roles;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<RoleModel> getRoles() {
return roles;
}
public void setRoles(List<RoleModel> roles) {
this.roles = roles;
}
}
RoleModel
xxxxxxxxxx
public class RoleModel {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Now, let us see the following methods in the UserService class.
xxxxxxxxxx
public UserModel getUser(Long id) {
if(userRepository.findById(id).isPresent()) {
User user = userRepository.findById(id).get();
UserModel userModel = new UserModel();
userModel.setFirstName(user.getFirstName());
userModel.setLastName(user.getLastName());
userModel.setEmail(user.getEmail());
userModel.setMobile(user.getMobile());
userModel.setRoles( getRoleList(user));
return userModel;
} else return null;
}
public List<UserModel > getUsers() {
List<User> userList = userRepository.findAll();
if(userList.size()>0) {
List<UserModel> userModels = new ArrayList<>();
for (User user : userList) {
UserModel model = new UserModel();
model.setFirstName(user.getFirstName());
model.setLastName(user.getLastName());
model.setMobile(user.getMobile());
model.setEmail(user.getEmail());
model.setRoles(getRoleList(user));
userModels.add(model);
}
return userModels;
} else return new ArrayList<UserModel>();
}
private List<RoleModel> getRoleList(User user){
List<RoleModel> roleList = new ArrayList<>();
for(int i=0; i< user.getRoles().size(); i++) {
RoleModel roleModel = new RoleModel();
roleModel.setName(user.getRoles().get(i).getName());
roleModel.setDescription(user.getRoles().get(i).getDescription());
roleList.add(roleModel);
}
return roleList;
}
I am setting all the values to the model and returning the model. Have a look at the controller
UserController GET methods
xxxxxxxxxx
"/user/details/{id}") (
public UserModel getUser( Long id) {
return userService.getUser(id);
}
"/user/all") (
public List<UserModel> getUsers() {
return userService.getUsers();
}
"/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:
xxxxxxxxxx
{
"firstName": "hello",
"lastName": "world",
"mobile": "9876435234",
"email": "hello@mail.com",
"roles": [
{
"name": "ADMIN",
"description": "Administrator"
}
]
}
GET: localhost:2003/user/all
xxxxxxxxxx
[
{
"firstName": "hello",
"lastName": "world",
"mobile": "9876435234",
"email": "hello@mail.com",
"roles": [
{
"name": "ADMIN",
"description": "Administrator"
}
]
},
{
"firstName": "Hello Good Morning",
"lastName": "world",
"mobile": "9876435234",
"email": "world@mail.com",
"roles": [
{
"name": "ADMIN",
"description": "Administrator"
}
]
},
{
"firstName": "Hello Good Afternoon",
"lastName": "world",
"mobile": "9876435234",
"email": "hi@mail.com",
"roles": [
{
"name": "MANAGER",
"description": "Mid Level Managers"
},
{
"name": "ACCOUNTS",
"description": "ACCOUNTS USERS"
}
]
}
]
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:
Opinions expressed by DZone contributors are their own.
Comments