Spring Boot Hibernate One-to-One Mapping Tutorial (CRUD REST APIs)

In this tutorial, we will learn how to implement One-to-One Mapping using Java, REST APIs, Spring Boot 3,  Spring Data JPA (Hibernate 6), and MySQL database. 

We will implement REST APIs for performing CRUD operations:

  • Create a user (POST)
  • Update a user (PUT)
  • Get a user by ID (GET)
  • Get all users (GET)
  • Delete a user (DELETE)

We will use DTOs (records) to transfer data between client and server and implement exception handling for better error responses.

What You Will Learn

  • How to set up a Spring Boot 3 project with Hibernate 6.
  • How to configure One-to-One mapping between entities.
  • How to use DTOs for data transfer.
  • How to implement proper exception handling.
  • How to build REST APIs for performing CRUD operations:
  • How to test APIs using Postman.

Step 1: Setting Up the Spring Boot Project

Create a Spring Boot 3 project using Spring Initializr. Select the following dependencies:

  • Spring Web (for REST API development)
  • Spring Data JPA (for ORM and Hibernate support)
  • MySQL Driver (for database connectivity)
  • Validation (for input validation)

Adding Dependencies to pom.xml

Add the required dependencies to your pom.xml file:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>9.2.0</version>
    </dependency>

    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

Step 2: Configuring the application.properties

We need to configure our MySQL database settings in src/main/resources/application.properties.

spring.datasource.url=jdbc:mysql://localhost:3306/testdb
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Replace root with your actual MySQL username and password. ddl-auto=update allows Hibernate to create and update tables automatically.

Step 3: Creating Entity Classes

We will create two entities: User and Address. Each User will have one Address, forming a One-to-One relationship.

User Entity

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;

    public User() {}

    public User(String name, String email, Address address) {
        this.name = name;
        this.email = email;
        this.address = address;
    }

    // Getters and Setters
}
  • @Entity: Marks the class as a Hibernate entity.
  • @Table(name = "users"): Maps this entity to the users table.
  • @Id: Declares the primary key.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): Auto-generates the ID.
  • @Column(nullable = false): Ensures the field is required.
  • @OneToOne: Defines a One-to-One relationship with Address.
  • @JoinColumn(name = "address_id"): Specifies the foreign key column.
  • cascade = CascadeType.ALL, orphanRemoval = true: Ensures changes in the parent affect the child entity.

Address Entity

import jakarta.persistence.*;

@Entity
@Table(name = "addresses")
public class Address {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String street;
    
    @Column(nullable = false)
    private String city;
    
    @Column(nullable = false)
    private String state;
    
    @Column(nullable = false)
    private String zipCode;

    public Address() {}

    public Address(String street, String city, String state, String zipCode) {
        this.street = street;
        this.city = city;
        this.state = state;
        this.zipCode = zipCode;
    }

    // Getters and Setters
}

Step 4: Creating the Repository Layer

We need to create repositories to handle database operations.

User Repository

import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {
}

Address Repository

import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.Address;

public interface AddressRepository extends JpaRepository<Address, Long> {
}

Step 5: Creating DTO Classes

We will use DTOs (records) to transfer data between client and server.

User DTO

public record UserDTO(Long id, String name, String email, AddressDTO address) {}

Address DTO

public record AddressDTO(Long id, String street, String city, String state, String zipCode) {}

Step 6: Creating the Service Layer

UserService Interface

The service layer acts as an intermediary between the controller and the repository. It contains business logic and ensures proper data transformation.

import java.util.List;
import com.example.demo.dto.UserDTO;

public interface UserService {
    UserDTO createUser(UserDTO userDTO);
    UserDTO updateUser(Long userId, UserDTO userDTO);
    UserDTO getUserById(Long userId);
    List<UserDTO> getAllUsers();
    void deleteUser(Long userId);
}

UserServiceImpl Implementation

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.dto.UserDTO;
import com.example.demo.dto.AddressDTO;
import com.example.demo.entity.User;
import com.example.demo.entity.Address;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.repository.UserRepository;

@Service
@Transactional
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDTO createUser(UserDTO userDTO) {
        Address address = new Address(userDTO.address().street(), userDTO.address().city(), userDTO.address().state(), userDTO.address().zipCode());
        User user = new User(userDTO.name(), userDTO.email(), address);
        user = userRepository.save(user);
        return mapToDTO(user);
    }

    @Override
    public UserDTO updateUser(Long userId, UserDTO userDTO) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with ID: " + userId));
        
        user.setName(userDTO.name());
        user.setEmail(userDTO.email());
        
        Address address = user.getAddress();
        address.setStreet(userDTO.address().street());
        address.setCity(userDTO.address().city());
        address.setState(userDTO.address().state());
        address.setZipCode(userDTO.address().zipCode());
        
        user = userRepository.save(user);
        return mapToDTO(user);
    }

    @Override
    public UserDTO getUserById(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with ID: " + userId));
        return mapToDTO(user);
    }

    @Override
    public List<UserDTO> getAllUsers() {
        return userRepository.findAll().stream().map(this::mapToDTO).collect(Collectors.toList());
    }

    @Override
    public void deleteUser(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException("User not found with ID: " + userId));
        userRepository.delete(user);
    }

    private UserDTO mapToDTO(User user) {
        AddressDTO addressDTO = new AddressDTO(user.getAddress().getId(), user.getAddress().getStreet(), user.getAddress().getCity(), user.getAddress().getState(), user.getAddress().getZipCode());
        return new UserDTO(user.getId(), user.getName(), user.getEmail(), addressDTO);
    }
}

Explanation of annotations and methods:

  • @Service: Marks the class as a Spring service component.
  • @Transactional: Ensures that database operations within the service methods are executed within a transaction.
  • createUser: Converts DTO to an entity, saves the user, and returns a DTO.
  • updateUser: Finds the user, updates details, and returns the modified DTO.
  • getUserById: Fetches a user from the database using their ID.
  • getAllUsers: Retrieves all users and maps them to DTOs.
  • deleteUser: Deletes a user by ID after checking existence.

Step 7: Creating the Controller

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import com.example.demo.dto.UserDTO;
import com.example.demo.service.UserService;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO userDTO) {
        UserDTO createdUser = userService.createUser(userDTO);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO) {
        UserDTO updatedUser = userService.updateUser(id, userDTO);
        return ResponseEntity.ok(updatedUser);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        UserDTO userDTO = userService.getUserById(id);
        return ResponseEntity.ok(userDTO);
    }

    @GetMapping
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        return ResponseEntity.ok(userService.getAllUsers());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

Explanation of Controller Methods

  • @RestController: Marks the class as a REST API controller.
  • @RequestMapping("/api/users"): Sets the base path for all endpoints in this controller.
  • @PostMapping: Handles HTTP POST requests.
  • @PutMapping: Handles HTTP PUT requests.
  • @GetMapping: Handles HTTP GET requests.
  • @DeleteMapping: Handles HTTP DELETE requests.
  • ResponseEntity: Used to send proper HTTP status codes.

Step 8: Testing REST APIs with Sample Data (Using Postman)

Now that we have implemented the REST APIs. Next, let's test them with sample data using Postman.

1. Creating a User (POST Request)

Endpoint: POST http://localhost:8080/api/users

Request Body:

{
  "name": "Rajesh Kumar",
  "email": "rajesh.kumar@example.com",
  "address": {
    "street": "MG Road",
    "city": "Mumbai",
    "state": "Maharashtra",
    "zipCode": "400001"
  }
}

Response: 201 Created

{
  "id": 1,
  "name": "Rajesh Kumar",
  "email": "rajesh.kumar@example.com",
  "address": {
    "id": 1,
    "street": "MG Road",
    "city": "Mumbai",
    "state": "Maharashtra",
    "zipCode": "400001"
  }
}

2. Updating a User (PUT Request)

Endpoint: PUT http://localhost:8080/api/users/1

Request Body:

{
  "name": "Rajesh Sharma",
  "email": "rajesh.sharma@example.com",
  "address": {
    "street": "Brigade Road",
    "city": "Bangalore",
    "state": "Karnataka",
    "zipCode": "560001"
  }
}

Response: 200 OK

{
  "id": 1,
  "name": "Rajesh Sharma",
  "email": "rajesh.sharma@example.com",
  "address": {
    "id": 1,
    "street": "Brigade Road",
    "city": "Bangalore",
    "state": "Karnataka",
    "zipCode": "560001"
  }
}

3. Getting a User by ID (GET Request)

Endpoint: GET http://localhost:8080/api/users/1

Response: 200 OK

{
  "id": 1,
  "name": "Rajesh Sharma",
  "email": "rajesh.sharma@example.com",
  "address": {
    "id": 1,
    "street": "Brigade Road",
    "city": "Bangalore",
    "state": "Karnataka",
    "zipCode": "560001"
  }
}

4. Getting All Users (GET Request)

Endpoint: GET http://localhost:8080/api/users

Response: 200 OK

[
  {
    "id": 1,
    "name": "Rajesh Sharma",
    "email": "rajesh.sharma@example.com",
    "address": {
      "id": 1,
      "street": "Brigade Road",
      "city": "Bangalore",
      "state": "Karnataka",
      "zipCode": "560001"
    }
  },
  {
    "id": 2,
    "name": "Priya Singh",
    "email": "priya.singh@example.com",
    "address": {
      "id": 2,
      "street": "Connaught Place",
      "city": "Delhi",
      "state": "Delhi",
      "zipCode": "110001"
    }
  }
]

5. Deleting a User (DELETE Request)

Endpoint: DELETE http://localhost:8080/api/users/1

Response: 204 No Content

Conclusion

We have successfully implemented and tested a One-to-One Mapping REST API using Spring Boot 3, Hibernate 6, MySQL, and DTOs. The REST APIs include CRUD operations and proper error handling, and we tested them with sample data using Indian names.

This concludes the tutorial. Happy coding!

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare