JPA/Hibernate One To One unidirectional mapping in Spring Boot

In this tutorial, I will show you how to implement Spring JPA One-To-One unidirectional mapping with Hibernate in a Spring Boot example using @OneToOne annotation. You’ll know:

  • How to configure Spring Data, JPA, Hibernate to work with Database
  • How to define Data Models and Repository interfaces for JPA One-To-One relationship
  • Way to use Spring JPA to interact with Database for One-To-One association
  • Way to create Spring Rest Controller to process HTTP requests

Related Posts:
JPA One To Many example with Hibernate and Spring Boot
JPA Many to Many example with Hibernate in Spring Boot
Spring Boot Token based Authentication with Spring Security & JWT
Spring JPA + H2 example
Spring JPA + MySQL example
Spring JPA + PostgreSQL example
Spring JPA + Oracle example
Spring JPA + SQL Server example

Appropriate way to implement JPA/Hibernate One To One mapping

In a relational database, a One-to-One relationship between table A and table B indicates that one row in table A links to only one row in table B, and vice versa.

For example, you need to design data model for a Tutorial Blog in which One Tutorial has corresponding Details (the Date Time it was created on, the author it was created by). So this is a One-to-One association.

You can use a Join Table (with @JoinTable annotation). It stores the primary key values from both entities in a NEW table.

jpa-hibernate-one-to-one-join-table

Another way is to use Shared Primary Key with the Foreign Key is located in the tutorial_details table. Tutorial entity is the parent, and the Tutorial Details is the child.

jpa-hibernate-one-to-one

You can map the child entity with the parent using JPA/Hibernate @OneToOne annotation. And in this case, only the children-side defines the relationship. We call it unidirectional One-to-One association.

Now look at the tutorial_details table that contains a Primary Key column (id) and a Foreign Key column (tutorial_id). You can see that we really need only one tutorial_details (child) row associated with a tutorial (parent) row, and the child data is hardly used in other relationships. So we can omit the child’s id column:

jpa-hibernate-one-to-one-example

JPA One To One example

We’re gonna create a Spring project from scratch, then we implement JPA/Hibernate One to One Unidirectional Mapping with tutorials and tutorial_details table as following:

jpa-hibernate-one-to-one-example

We also write Rest Apis to perform CRUD operations on the Details entities.

These are APIs that we need to provide:

Methods Urls Actions
POST /api/tutorials/:id/details create new Details for a Tutorial
GET /api/details/:id retrieve Details by :id
GET /api/tutorials/:id/details retrieve Details of a Tutorial
PUT /api/details/:id update Details by :id
PUT /api/tutorials/:id/details update Details of a Tutorial
DELETE /api/details/:id delete Details by :id
DELETE /api/tutorials/:id/details delete Details of a Tutorial
DELETE /api/tutorials/:id delete a Tutorial (and its Details)

Assume that we’ve had tutorials table like this:

jpa-hibernate-one-to-one-parent-table

Here are the example requests:

– Create new Details entity: POST /api/tutorials/[:id]/details

jpa-hibernate-one-to-one-create

tutorial_details table after that:

jpa-hibernate-one-to-one-table

– Retrieve Details of specific Tutorial: GET /api/tutorials/[:id]/details or /api/details/[:id]

jpa-hibernate-one-to-one-retrieve

– Update Details of specific Tutorial: PUT /api/tutorials/[:id]/details or /api/details/[:id]

jpa-hibernate-one-to-one-update

– Delete Details of specific Tutorial: DELETE /api/tutorials/[:id]/details or /api/details/[:id]

jpa-hibernate-one-to-one-delete

Check the tutorial_details table, row with tutorial_id=1 were deleted:

jpa-hibernate-one-to-one-delete-db

– Delete a Tutorial: DELETE /api/tutorials/[:id]

jpa-hibernate-one-to-one-delete-parent

Tutorial (id=3) and its Details were deleted:

jpa-hibernate-one-to-one-delete-parent-table

Let’s build our Spring Boot Data JPA One to One example.

Spring Boot One to One example

Technology

  • Java 8
  • Spring Boot 2.7.2 (with Spring Web MVC, Spring Data JPA)
  • H2/MySQL/PostgreSQL
  • Maven 3.8.1

Project Structure

spring-data-jpa-one-to-one-unidirectional

Let me explain it briefly.

Tutorial, TutorialDetails data model class correspond to entity and table tutorials, tutorial_details.
TutorialRepository, TutorialDetailsRepository are interfaces that extends JpaRepository for CRUD methods and custom finder methods. It will be autowired in TutorialController, TutorialDetailsController.
TutorialController, TutorialDetailsController are RestControllers which has request mapping methods for RESTful CRUD API requests.
– Configuration for Spring Datasource, JPA & Hibernate in application.properties.
pom.xml contains dependencies for Spring Boot and MySQL/PostgreSQL/H2 database.

– About exception package, to keep this post straightforward, I won’t explain it. For more details, you can read following tutorial:
@RestControllerAdvice example in Spring Boot

Create & Setup Spring Boot project

Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.

Then open pom.xml and add these dependencies:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

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

We also need to add one more dependency.
– If you want to use MySQL:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>

– or PostgreSQL:

<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<scope>runtime</scope>
</dependency>

– or H2 (embedded database):

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

Configure Spring Datasource, JPA, Hibernate

Under src/main/resources folder, open application.properties and write these lines.

– For MySQL:

spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.password= 123456

spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update

– For PostgreSQL:

spring.datasource.url= jdbc:postgresql://localhost:5432/testdb
spring.datasource.username= postgres
spring.datasource.password= 123

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update
  • spring.datasource.username & spring.datasource.password properties are the same as your database installation.
  • Spring Boot uses Hibernate for JPA implementation, we configure MySQL5InnoDBDialect for MySQL or PostgreSQLDialect for PostgreSQL
  • spring.jpa.hibernate.ddl-auto is used for database initialization. We set the value to update value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should be validate.

– For H2 database:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
 
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update

spring.h2.console.enabled=true
# default path: h2-console
spring.h2.console.path=/h2-ui
  • spring.datasource.url: jdbc:h2:mem:[database-name] for In-memory database and jdbc:h2:file:[path/database-name] for disk-based database.
  • We configure H2Dialect for H2 Database
  • spring.h2.console.enabled=true tells the Spring to start H2 Database administration tool and you can access this tool on the browser: http://localhost:8080/h2-console.
  • spring.h2.console.path=/h2-ui is for H2 console’s url, so the default url http://localhost:8080/h2-console will change to http://localhost:8080/h2-ui.

Define Data Model for JPA One to One mapping

In model package, we define Tutorial and TutorialDetails class.

Tutorial has four fields: id, title, description, published.

model/Tutorial.java

package com.bezkoder.spring.jpa.onetoone.model;

import javax.persistence.*;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties({"hibernateLazyInitializer"})
@Entity
@Table(name = "tutorials")
public class Tutorial {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;

  @Column(name = "title")
  private String title;

  @Column(name = "description")
  private String description;

  @Column(name = "published")
  private boolean published;

  public Tutorial() {

  }

  public Tutorial(String title, String description, boolean published) {
    this.title = title;
    this.description = description;
    this.published = published;
  }

  // getters and setters
}

@Entity annotation indicates that the class is a persistent Java class.
@Table annotation provides the table that maps this entity.

@Id annotation is for the primary key.
@GeneratedValue annotation is used to define generation strategy for the primary key.

@Column annotation is used to define the column in database that maps annotated field.

– How about @JsonIgnoreProperties({"hibernateLazyInitializer"})?
When we fetch data from the database using a JPA Repository for the field which is being lazy-loaded from the parent entity, Hibernate returns an object that has all the fields of the class which are mapped to table along with hibernateLazyInitializer.

Then when we serialize this entity in JSON String format, all the fields and hibernateLazyInitializer will be serialized.
So, to avoid this unnecessary serialization, we use @JsonIgnoreProperties.

The TutorialDetails class has the @OneToOne annotation for one-to-one relationship with the Tutorial entity and @MapsId annotation that makes the id field serve as both Primary Key and Foreign Key (shared primary key).

We set the shared primary key column name by using @JoinColumn annotation.

model/TutorialDetails.java

package com.bezkoder.spring.jpa.onetoone.model;

import java.util.Date;

import javax.persistence.*;

@Entity
@Table(name = "tutorial_details")
public class TutorialDetails {
  @Id
  private Long id;

  @Column
  private Date createdOn;

  @Column
  private String createdBy;

  @OneToOne(fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "tutorial_id")
  private Tutorial tutorial;

  public TutorialDetails() {
  }

  public TutorialDetails(String createdBy) {
    this.createdOn = new Date();
    this.createdBy = createdBy;
  }

  // getters and setters
}

Create Repository Interfaces for One To One mapping

Let’s create a repository to interact with database.
In repository package, create TutorialRepository and TutorialDetailsRepository interfaces that extend JpaRepository.

repository/TutorialRepository.java

package com.bezkoder.spring.jpa.onetoone.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.jpa.onetoone.model.Tutorial;

@Repository
public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
  List<Tutorial> findByPublished(boolean published);

  List<Tutorial> findByTitleContaining(String title);
}

repository/TutorialDetailsRepository.java

package com.bezkoder.spring.jpa.onetoone.repository;

import javax.transaction.Transactional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.jpa.onetoone.model.TutorialDetails;

@Repository
public interface TutorialDetailsRepository extends JpaRepository<TutorialDetails, Long> {
  @Transactional
  void deleteById(long id);
  
  @Transactional
  void deleteByTutorialId(long tutorialId);
}

Now we can use JpaRepository’s methods: save(), findOne(), findById(), findAll(), count(), delete(), deleteById()… without implementing these methods.

We also define custom finder methods:

  • findByPublished(): returns all Tutorials with published having value as input published.
  • findByTitleContaining(): returns all Tutorials which title contains input title.
  • deleteById(): deletes Details specified by id.
  • deleteByTutorialId(): deletes Details of a Tutorial specified by tutorialId.

The implementation is plugged in by Spring Data JPA automatically.

More Derived queries at:
JPA Repository query example in Spring Boot

Custom query with @Query annotation:
Spring JPA @Query example: Custom query in Spring Boot

You also find way to write Unit Test for this JPA Repository at:
Spring Boot Unit Test for JPA Repositiory with @DataJpaTest

Create Spring Rest APIs Controller

Finally, we create controller that provides APIs for CRUD operations: creating, retrieving, updating, deleting and finding Tutorials and Details.

controller/TutorialController.java

package com.bezkoder.spring.jpa.onetoone.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.bezkoder.spring.jpa.onetoone.exception.ResourceNotFoundException;
import com.bezkoder.spring.jpa.onetoone.model.Tutorial;
import com.bezkoder.spring.jpa.onetoone.repository.TutorialDetailsRepository;
import com.bezkoder.spring.jpa.onetoone.repository.TutorialRepository;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class TutorialController {

  @Autowired
  TutorialRepository tutorialRepository;
  
  @Autowired
  private TutorialDetailsRepository detailsRepository;

  @GetMapping("/tutorials")
  public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
    List<Tutorial> tutorials = new ArrayList<Tutorial>();

    if (title == null)
      tutorialRepository.findAll().forEach(tutorials::add);
    else
      tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);

    if (tutorials.isEmpty()) {
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    return new ResponseEntity<>(tutorials, HttpStatus.OK);
  }

  @GetMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
    Tutorial tutorial = tutorialRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));

    return new ResponseEntity<>(tutorial, HttpStatus.OK);
  }

  @PostMapping("/tutorials")
  public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
    Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), true));
    return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
  }

  @PutMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
    Tutorial _tutorial = tutorialRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));

    _tutorial.setTitle(tutorial.getTitle());
    _tutorial.setDescription(tutorial.getDescription());
    _tutorial.setPublished(tutorial.isPublished());
    
    return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
  }

  @DeleteMapping("/tutorials/{id}")
  public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
    if (detailsRepository.existsById(id)) {
      detailsRepository.deleteById(id);
    }
    
    tutorialRepository.deleteById(id);
    
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }

  @DeleteMapping("/tutorials")
  public ResponseEntity<HttpStatus> deleteAllTutorials() {
    tutorialRepository.deleteAll();
    
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }

  @GetMapping("/tutorials/published")
  public ResponseEntity<List<Tutorial>> findByPublished() {
    List<Tutorial> tutorials = tutorialRepository.findByPublished(true);

    if (tutorials.isEmpty()) {
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
    
    return new ResponseEntity<>(tutorials, HttpStatus.OK);
  }
}

controller/TutorialDetailsController.java

package com.bezkoder.spring.jpa.onetoone.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.bezkoder.spring.jpa.onetoone.exception.ResourceNotFoundException;
import com.bezkoder.spring.jpa.onetoone.model.TutorialDetails;
import com.bezkoder.spring.jpa.onetoone.model.Tutorial;
import com.bezkoder.spring.jpa.onetoone.repository.TutorialDetailsRepository;
import com.bezkoder.spring.jpa.onetoone.repository.TutorialRepository;

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class TutorialDetailsController {
  @Autowired
  private TutorialDetailsRepository detailsRepository;

  @Autowired
  private TutorialRepository tutorialRepository;

  @GetMapping({ "/details/{id}", "/tutorials/{id}/details" })
  public ResponseEntity<TutorialDetails> getDetailsById(@PathVariable(value = "id") Long id) {
    TutorialDetails details = detailsRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial Details with id = " + id));

    return new ResponseEntity<>(details, HttpStatus.OK);
  }

  @PostMapping("/tutorials/{tutorialId}/details")
  public ResponseEntity<TutorialDetails> createDetails(@PathVariable(value = "tutorialId") Long tutorialId,
      @RequestBody TutorialDetails detailsRequest) {
    Tutorial tutorial = tutorialRepository.findById(tutorialId)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId));

    detailsRequest.setCreatedOn(new java.util.Date());
    detailsRequest.setTutorial(tutorial);
    TutorialDetails details = detailsRepository.save(detailsRequest);

    return new ResponseEntity<>(details, HttpStatus.CREATED);
  }

  @PutMapping("/details/{id}")
  public ResponseEntity<TutorialDetails> updateDetails(@PathVariable("id") long id,
      @RequestBody TutorialDetails detailsRequest) {
    TutorialDetails details = detailsRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Id " + id + " not found"));

    details.setCreatedBy(detailsRequest.getCreatedBy());

    return new ResponseEntity<>(detailsRepository.save(details), HttpStatus.OK);
  }

  @DeleteMapping("/details/{id}")
  public ResponseEntity<HttpStatus> deleteDetails(@PathVariable("id") long id) {
    detailsRepository.deleteById(id);

    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }

  @DeleteMapping("/tutorials/{tutorialId}/details")
  public ResponseEntity<TutorialDetails> deleteDetailsOfTutorial(@PathVariable(value = "tutorialId") Long tutorialId) {
    if (!tutorialRepository.existsById(tutorialId)) {
      throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId);
    }

    detailsRepository.deleteByTutorialId(tutorialId);
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }
}

Conclusion

Today we’ve built a Spring Boot example using Spring Data JPA, Hibernate One to One relationship with MySQL/PostgreSQL/embedded database (H2).

We also see that @OneToOne & @MapsId annotation is the appropriate way for implementing JPA One to One Unidirectional Mapping, and JpaRepository supports a great way to make CRUD operations, custom finder methods without need of boilerplate code.

Custom query with @Query annotation:
Spring JPA @Query example: Custom query in Spring Boot

If you want to add Pagination to this Spring project, you can find the instruction at:
Spring Boot Pagination & Filter example | Spring JPA, Pageable

To sort/order by multiple fields:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot

Handle Exception for this Rest APIs is necessary:
Spring Boot @ControllerAdvice & @ExceptionHandler example
@RestControllerAdvice example in Spring Boot

Or way to write Unit Test for the JPA Repository:
Spring Boot Unit Test for JPA Repositiory with @DataJpaTest

You can also know:
– how to deploy this Spring Boot App on AWS (for free) with this tutorial.
– dockerize with Docker Compose: Spring Boot and MySQL example
– way to upload an Excel file and store the data in MySQL database with this post
– upload CSV file and store the data in MySQL with this post.

Happy learning! See you again.

Further Reading

Fullstack CRUD App:
Vue + Spring Boot example
Angular 8 + Spring Boot example
Angular 10 + Spring Boot example
Angular 11 + Spring Boot example
Angular 12 + Spring Boot example
Angular 13 + Spring Boot example
Angular 14 + Spring Boot example
React + Spring Boot example

Source Code

You can find the complete source code for this tutorial on Github.

One-to-Many: JPA One To Many example with Hibernate and Spring Boot
Many-to-Many: JPA Many to Many example with Hibernate in Spring Boot

You can apply this implementation in following tutorials:
Spring JPA + H2 example
Spring JPA + MySQL example
Spring JPA + PostgreSQL example
Spring JPA + Oracle example
Spring JPA + SQL Server example

More Derived queries at:
JPA Repository query example in Spring Boot