Introduction to Spring Data JPA — Part 4 Bidirectional One-to-Many Relations
In this article, explore a continued introduction to Spring Data JPA and look at bidirectional one-to-many relations.
Join the DZone community and get the full member experience.
Join For FreeIn this article, we will discuss the following:
- Bi-directional one-to-many relation
-
@Join
column annotation -
@JsonIgnore
annotation -
@Transient
annotation
Let's look at the same use case of User and Role.
You can see that User is referring to the Role entity and the Role entity is referring to the User entity, so we call it bidirectional. Navigation is possible from both User and the Role.
Let's see how Hibernate creates the table for you by default.
You can see a foreign key reference to the ID of the t_role table in the t_user table. How does it translate to code? Please see below.
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;
private Role role;
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;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
private Role role;
The above is the change that has been brought in. Now, there are definitions on both sides of the relation.
Role Entity
xxxxxxxxxx
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;
}
}
RoleRepository
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);
}
UserRepository
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);
}
RoleService
We are using the same code as in the previous article (Part 2)
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;
import org.springframework.transaction.annotation.Transactional;
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");
}
}
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);
}
}
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
Now let's run the application.
Open Postman and send in a Post request create two users with the Role as ADMIN. Please find the JSON Object below.
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 should get the following error:
save the transient instance before flushing
Please find source code at https://github.com/gudpick/jpa-demo/tree/one-to-many-bidirectional-starter
Let us change the cascade type as following.
xxxxxxxxxx
targetEntity = User.class, cascade = CascadeType.ALL) (
private List<User> users;
Please find source code at https://github.com/gudpick/jpa-demo/tree/add-cascade-type-all
Now, when you run the application and send in the request, you will see that both the tables are populated, but unfortunately, you find that the role_id is not getting updated. Now let's explicitly mention role_id using the @JoinColumn annotation. The @JoinColumn annotation helps us specify the column we'll use for joining an entity association or element collection.
xxxxxxxxxx
targetEntity = User.class, cascade = CascadeType.ALL) (
name = "role_id") (
private List<User> users;
Now run the application and send the request. You will see all the data gets populated correctly.
Please find source code at https://github.com/gudpick/jpa-demo/tree/one-to-many-bidirectional-starter
The READ operation:
Let us use @GetMapping to read the roles and users from the database.
- Get User by Id
- Read all Users
- Get Role by Id
- Get all Roles
Now let's update our controllers.
UserController
xxxxxxxxxx
package com.notyfyd.controller;
import com.notyfyd.entity.User;
import com.notyfyd.repository.UserRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
public class UserController {
private UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
"/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();
}
We are using the @GetMapping with the getUser() method which will fetch the user by it's Id, getUsers() method will get all the users. It uses the findAll method to accomplish the task.
RoleController
x
package com.notyfyd.controller;
import com.notyfyd.entity.Role;
import com.notyfyd.repository.RoleRepository;
import com.notyfyd.service.RoleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class RoleController {
private RoleService roleService;
private RoleRepository roleRepository;
public RoleController(RoleService roleService, RoleRepository roleRepository) {
this.roleService = roleService;
this.roleRepository = roleRepository;
}
"/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();
}
}
Now let us run the application.
Create the Role with users as before using Postman.
Now let us try the Get requests.
Now you will get the stack overflow error as below
java.lang.StackOverflowError: null
Please find source code at https://github.com/gudpick/jpa-demo/tree/controllers-updated
This is because of circular reference. The user picks up the role and the role picks up the user. We can overcome this in many ways. We'll use @JsonIgnore to overcome the error. @JsonIgnore ignores a field both when reading JSON into Java objects and when writing Java objects into JSON.
So all we have to do is to add this annotation as below in the User entity. Using @JsonIgnore in the role entity will create errors while reading in the data. I encourage you to try it.
xxxxxxxxxx
private Role role;
Please find source code at https://github.com/gudpick/jpa-demo/tree/json-ignore
Now run the application and send the get request. You will see that everything is working. But let's not be satisfied with just this. When you see the JSON Object returned by the get user, you will find the following:
xxxxxxxxxx
{
"id": 1,
"firstName": "hello",
"lastName": "world",
"mobile": "9876435234",
"email": "hello@mail.com"
}
Don't you think you deserve at least the name of the role? Now we will introduce the @Transient annotation. @Transient annotation in JPA or Hibernate is used to indicate that a field is not to be persisted or ignore fields to be saved in the database. Let's use this effectively. Define a field roleName and write getters and setters as below in the User entity. Annotate the roleName with @Transient.
xxxxxxxxxx
private String roleName;
public String getRoleName() {
return getRole().getName();
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
Now run the application. Send in the post request to create the Role with Users. Now let's send in the get request. You will get the below result:
xxxxxxxxxx
{
"id": 1,
"firstName": "hello",
"lastName": "world",
"mobile": "9876435234",
"email": "hello@mail.com",
"roleName": "ADMIN"
}
Please find the source code at https://github.com/gudpick/jpa-demo/tree/transient-annotation
Please find the video tutorials at:
Opinions expressed by DZone contributors are their own.
Comments