Introduction to Spring Data JPA, Part 3: Unidirectional One to Many Relations
In this tutorial, we take a look at unidirectional one-to-many realations and how to enable the removal of user records without removing role records.
Join the DZone community and get the full member experience.
Join For FreeWe will look into unidirectional one-to-many relations. We will use Cascade Types, a constructor-based dependency injection by removing the @Autowired
annotation. We will accomplish this with a small implementation where we will handle the DELETE operation, understand @PathVariable
annotation, use Orphan removal, and the @Transaction
annotation.
Let us consider an organization. Here there will be many employees, each taking up different tasks. Let us call the employees users and classify their tasks into roles they play. Some may be performing managerial task, so we'll give them the role MANAGER. Some may be performing system administration tasks, so we'll give them the role ADMIN.
So let us look at in this manner. There might be many MANAGER and many ADMIN roles in the same organization. Therefore, you can say they each have many users.
This relation can be called a One to Many relation. Let's look at the conditions.
- Every user will have a role.
- There can be many users with the same role.
- When you add an user you should associate a role with the user.
- When you delete a user the corresponding role need not be deleted since there might be other users associated with the role.
- When you delete a role all users associated with the role should be deleted.
Modeling the Entities
When we look at the definition of the entity we can see that the role entity has a list of users and in the user entity, you do not have any definition of role. That is the reason why we call this relation unidirectional. If we had a definition of role in the user entity, we will call this relation bidirectional.
Now let's talk only about unidirectional one-to-many relations. Once you have an entity definition like this, Hibernate by default will create the tables that look like these below.
Let us go with the default creation and write code for both the role and user entities.
User Entity
package com.notyfyd.entity;
import javax.persistence.*;
name = "t_user") (
public class User {
strategy = GenerationType.IDENTITY) (
private Long id;
private String firstName;
private String lastName;
private String mobile;
unique = true) (
private String email;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
Role Entity
x
package com.notyfyd.entity;
import javax.persistence.*;
import java.util.List;
name = "t_role") (
public class Role {
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private String description;
targetEntity = User.class) (
private List<User> users;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
What we notice here is the definition of users in the role entity.
@OneToMany(targetEntity = User.class)
private List<User> users;
whereas we do not find any definition for role in the user entity.
Role Repository
xxxxxxxxxx
package com.notyfyd.repository;
import com.notyfyd.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
User Repository
xxxxxxxxxx
package com.notyfyd.repository;
import com.notyfyd.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Role Service
xxxxxxxxxx
package com.notyfyd.service;
import com.notyfyd.entity.Role;
import com.notyfyd.repository.RoleRepository;
import com.notyfyd.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
public class RoleService {
private RoleRepository roleRepository;
private UserRepository userRepository;
public RoleService(RoleRepository roleRepository, UserRepository userRepository) {
this.roleRepository = roleRepository;
this.userRepository = userRepository;
}
/**
* Create a new role along with users
*/
public ResponseEntity<Object> addRole(Role role) {
Role newRole = new Role();
newRole.setName(role.getName());
newRole.setDescription(role.getDescription());
newRole.setUsers(role.getUsers());
Role savedRole = roleRepository.save(newRole);
if (roleRepository.findById(savedRole.getId()).isPresent()) {
return ResponseEntity.accepted().body("Successfully Created Role and Users");
} else
return ResponseEntity.unprocessableEntity().body("Failed to Create specified Role");
}
/**
* Delete a specified role given the id
*/
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");
}
}
Now, in the addRole
method, which takes the Role type, we will create a new Role as newRole
. We will set the name, description, and list of users to the newRole
from role. The method is saved and the role returned is assigned to savedRole. The existence of the saveRole
object is checked and returns success if present and return failure if it is not present.
The deleteRole
method takes the Long ID. The findById
method is used to check whether the role is present. If it is present it is deleted and returns "Successfully Deleted" after cross checking once more with the finder method; otherwise, it returns "No Records found."
RoleController
xxxxxxxxxx
package com.notyfyd.controller;
import com.notyfyd.entity.Role;
import com.notyfyd.service.RoleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
public class RoleController {
private RoleService roleService;
public RoleController(RoleService roleService) {
this.roleService = roleService;
}
"/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);
}
}
For the DELETE
operation we are using the @DeleteMapping
annotation and in the URI we are using flower brackets for the ID to be passed in. The @PathVariable
annotation is used for the ID passed in the URI. It call the deleteRole
method in the UserService. The CREATE
operation uses the @PostMapping
as before and calls the addRole
method in the UserService. Now you can see that I am not using the @Autowired
annotation; instead I am using a constructor. You can call it a constructor-based dependency injection.
application.properties
xxxxxxxxxx
server.port=2003
spring.datasource.driver-class-name= org.postgresql.Driver
spring.datasource.url= jdbc:postgresql://192.168.64.6:30432/jpa-test
spring.datasource.username = postgres
spring.datasource.password = root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
Let us run the application and open Postman.
JSON Object
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"
}
]
}
You can see the way I have written the users. I am using the square brackets("[]") since it is an array of object and each user object is enclosed in flower brackets ("{}") separated by a comma.
Having done all this, when you send the request, you will get an error.
xxxxxxxxxx
Object references an unsaved transient instance - save the transient instance before flushing: com.notyfyd.entity.User
This is because the user entity is not saved in the addRole
method before we save the role. Let us look at two solutions to correct it.
Please find the source code here.
Error Solutions
1. Let us write few lines of code to save the user before the role entity is saved. I will make changes to the addRole method.
xxxxxxxxxx
public ResponseEntity<Object> addRole(Role role) {
Role newRole = new Role();
newRole.setName(role.getName());
newRole.setDescription(role.getDescription());
for(int i=0; i< role.getUsers().size(); i++){
User savedUser = userRepository.save(role.getUsers().get(i));
if(!userRepository.findById(savedUser.getId()).isPresent())
return ResponseEntity.unprocessableEntity().body("Failed creating user and roles");
}
newRole.setUsers(role.getUsers());
Role savedRole = roleRepository.save(newRole);
if (roleRepository.findById(savedRole.getId()).isPresent()) {
return ResponseEntity.accepted().body("Successfully Created Role and Users");
} else
return ResponseEntity.unprocessableEntity().body("Failed to Create specified Role");
}
xxxxxxxxxx
for(int i=0; i< role.getUsers().size(); i++){
User savedUser = userRepository.save(role.getUsers().get(i));
if(!userRepository.findById(savedUser.getId()).isPresent())
return ResponseEntity.unprocessableEntity().body("Failed creating user and roles");
}
This is code I have changed in the addRole
method. Every user is fetched in the for loop. It is saved and if it fails, the application returns an error.
Now let us run the application. You will get a message saying "Successfully Created Role and Users."
If you check the database you can see both the t_user and t_role tables are populated correctly and in the mapping table you will find as below.
You can see that the role_id and user_id are mapped.
Please find the full source code here.
Now what will happen if the addRole
method throws an exception after saving the user but before saving the role?
Let us make it throw an exception.
if(true) throw new RuntimeException();
Add this line of code after the For loop. Once you run the application and send the request, you should get an exception:
java.lang.RuntimeException: null
But if you check the database you will find both the users are created, but no role is created and the mapping table will be empty.
The solution for this is to use the @Transactional
annotation before the method.
xxxxxxxxxx
import org.springframework.transaction.annotation.Transactional;
xxxxxxxxxx
public ResponseEntity<Object> addRole(Role role)
The usage of this annotation @Transactional
causes the entire transaction to roll back if anything happens in between. Now if you run the application you will find that the users are not inserted and the the roles are not created.
Please find the full source code here.
The second way of rectifying this is to use cascading.
- Cascading: When we perform an action on the target entity, the same action will be applied to associated entities.
- CascadeType.PERSIST: Propagates the persist operation from a parent to child entity. When the role is saved the user will also be saved along with it.
- CascadeType.MERGE: If the source entity is merged, the merge is cascaded to the target of the association.
- CascadeType.REFRESH: If the source entity is refreshed, the refresh is cascaded to the target of the association.
- CascadeType.REMOVE: Cascade type remove removes all related entities when the source entity is deleted.
- CascadeType.DETACH: Detached entity objects are objects in a special state in which they are not managed by any EntityManager but still represent objects in the database. Changes to detached entity objects are not stored in the database unless modified detached objects are merged back.
- CascadeType.ALL: Propagates all operations from parent to child entity.
Let us use the Cascade - CascadeType.PERSIST
xxxxxxxxxx
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private String description;
targetEntity = User.class, cascade = CascadeType.PERSIST) (
private List<User> users;
Now I am removing the code written to save the User in the addRole
method. The addRole
method will be as follows.
xxxxxxxxxx
public ResponseEntity<Object> addRole(Role role) {
Role newRole = new Role();
newRole.setName(role.getName());
newRole.setDescription(role.getDescription());
newRole.setUsers(role.getUsers());
Role savedRole = roleRepository.save(newRole);
if (roleRepository.findById(savedRole.getId()).isPresent()) {
return ResponseEntity.accepted().body("Successfully Created Role and Users");
} else
return ResponseEntity.unprocessableEntity().body("Failed to Create specified Role");
}
Now let us run the application and send the request in Postman. You will find the users and roles are successfully created.
Delete
Now time to run the delete request.
Let us go to Postman and run localhost:2003/role/delete/1. Here "1" is the ID of the role to be deleted.
Now when we send the request, you will receive the message that it has successfully deleted the record. When you check the database the role is deleted. The relations in the mapping tables are removed but you will find the two users still in the database. This is not what you want. What you want is when the role is removed all users associated with the role also should be removed.
Now there is something called orphanRemoval
which is false by default. You can set it to true and send the request again. Orphan Removal causes the child entity to be removed when it's no longer referenced from the parent entity.
xxxxxxxxxx
targetEntity = User.class, cascade = CascadeType.PERSIST, orphanRemoval = true) (
private List<User> users;
You will now see that all roles and user are deleted.
Please find the source code here.
Now another way of doing it is to set CascadeType to ALL.
xxxxxxxxxx
targetEntity = User.class, cascade = CascadeType.ALL) (
private List<User> users;
Now send in the delete request you will see that all the users and roles are deleted.
Please find the source code here.
Please find the video tutorials below:
Opinions expressed by DZone contributors are their own.
Comments