One-to-One Mapping

  1. A one-to-one mapping means one record in a table is linked to exactly one record in another table, such as one student and one student profile.
  2. In the database, one table holds a foreign key (here profile_id in students) that points to the other table, and a UNIQUE constraint makes sure it is never shared.
  3. In JPA you declare it with @OneToOne, and @JoinColumn says which column holds the foreign key.
  4. It is used to split a big record into two tables (core details and optional details), so the main table stays small and the extra details load only when needed.

Features

  • unique = true on the join column enforces “exactly one”.
  • Cascade: saving or deleting a student also saves or deletes the profile.
  • Orphan removal: removing the profile from the student deletes the profile row.
  • It can be navigated from both sides (student.getProfile() and profile.getStudent()).
  • A student can exist without a profile (the foreign key is just null).
  • Searches can follow the link (findByProfileCity).

Hands-on Experiment: Creating a spring boot application to understand One-to-One Mapping

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.yml

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: relationship
  • Name: relationship
  • Package name: com.example.relationship
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring JPA, MySQL Driver.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-profile-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <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>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/student_onetoone_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

server:
  port: 8082

Step 5: Create Entity Class

package com.example.onetoone.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;

@Entity
@Table(name = "students")
public class Student {

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

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true, length = 150)
    private String email;

    // ONE student has exactly ONE profile.
    // @JoinColumn  -> creates the foreign key column profile_id in the students table (Student OWNS the relationship)
    // unique = true -> no two students can share the same profile (this is what makes it one-to-one)
    // cascade = ALL -> saving or deleting a student also saves or deletes its profile
    // orphanRemoval -> a profile removed from the student is deleted from the database
    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "profile_id", unique = true)
    @JsonIgnoreProperties("student")          // avoids the loop student -> profile -> student
    private StudentProfile profile;

    public Student() {}

    public Student(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public StudentProfile getProfile() { return profile; }

    // Sets BOTH sides so they always agree in memory
    public void setProfile(StudentProfile profile) {
        this.profile = profile;
        if (profile != null) {
            profile.setStudent(this);
        }
    }
}
package com.example.onetoone.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;

@Entity
@Table(name = "student_profiles")
public class StudentProfile {

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

    @Column(length = 15)
    private String phone;

    @Column(length = 200)
    private String address;

    @Column(length = 50)
    private String city;

    // The other direction (bidirectional). mappedBy = "profile" means:
    // "the foreign key is already stored by Student.profile, do not create another one here".
    @OneToOne(mappedBy = "profile")
    @JsonIgnoreProperties("profile")
    private Student student;

    public StudentProfile() {}

    public StudentProfile(String phone, String address, String city) {
        this.phone = phone;
        this.address = address;
        this.city = city;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    public String getAddress() { return address; }
    public void setAddress(String address) { this.address = address; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    public Student getStudent() { return student; }
    public void setStudent(Student student) { this.student = student; }
}

Step 6: Create below Repository Interface

package com.example.onetoone.repository;

import com.example.onetoone.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface StudentRepository extends JpaRepository<Student, Long> {

    boolean existsByEmail(String email);

    // Walks the relationship: student.profile.city = ?
    List<Student> findByProfileCity(String city);
}
package com.example.onetoone.repository;

import com.example.onetoone.entity.StudentProfile;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentProfileRepository extends JpaRepository<StudentProfile, Long> {
}

Step 7: Create below Service class

package com.example.onetoone.service;

import com.example.onetoone.entity.Student;
import com.example.onetoone.entity.StudentProfile;
import com.example.onetoone.repository.StudentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
public class StudentService {

    private final StudentRepository repository;

    public StudentService(StudentRepository repository) {
        this.repository = repository;
    }

    // The student JSON may contain a "profile": both rows are saved (cascade)
    public Optional<Student> create(Student student) {
        if (repository.existsByEmail(student.getEmail())) {
            return Optional.empty();
        }
        student.setId(null);
        return Optional.of(repository.save(student));
    }

    public List<Student> findAll() {
        return repository.findAll();
    }

    public Optional<Student> findById(Long id) {
        return repository.findById(id);
    }

    public List<Student> findByCity(String city) {
        return repository.findByProfileCity(city);
    }

    // Adds a profile to a student, or REPLACES the existing one (the old row is deleted by orphanRemoval)
    @Transactional
    public Optional<Student> saveProfile(Long studentId, StudentProfile profile) {
        return repository.findById(studentId).map(student -> {
            profile.setId(null);
            student.setProfile(profile);
            return repository.save(student);
        });
    }

    // Removes only the profile, the student stays
    @Transactional
    public boolean removeProfile(Long studentId) {
        Optional<Student> student = repository.findById(studentId);
        if (student.isEmpty() || student.get().getProfile() == null) {
            return false;
        }
        student.get().setProfile(null);       // orphanRemoval deletes the profile row
        return true;
    }

    // Deletes the student AND its profile (cascade)
    public boolean delete(Long id) {
        if (!repository.existsById(id)) {
            return false;
        }
        repository.deleteById(id);
        return true;
    }
}
package com.example.onetoone.service;

import com.example.onetoone.entity.StudentProfile;
import com.example.onetoone.repository.StudentProfileRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class StudentProfileService {

    private final StudentProfileRepository repository;

    public StudentProfileService(StudentProfileRepository repository) {
        this.repository = repository;
    }

    public List<StudentProfile> findAll() {
        return repository.findAll();
    }

    public Optional<StudentProfile> findById(Long id) {
        return repository.findById(id);
    }
}

Step 8: Create Controller class

package com.example.onetoone.controller;

import com.example.onetoone.entity.Student;
import com.example.onetoone.entity.StudentProfile;
import com.example.onetoone.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService service;

    public StudentController(StudentService service) {
        this.service = service;
    }

    @PostMapping
    public ResponseEntity<Object> create(@RequestBody Student student) {
        return service.create(student)
                .<ResponseEntity<Object>>map(saved -> ResponseEntity.status(HttpStatus.CREATED).body(saved))
                .orElseGet(() -> ResponseEntity.status(HttpStatus.CONFLICT)
                        .body(Map.of("error", "Email already exists")));
    }

    @GetMapping
    public List<Student> getAll() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Student> getById(@PathVariable Long id) {
        return service.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // Students whose profile is in the given city
    @GetMapping("/city/{city}")
    public List<Student> getByCity(@PathVariable String city) {
        return service.findByCity(city);
    }

    // ----- the profile of one student -----
    @GetMapping("/{id}/profile")
    public ResponseEntity<StudentProfile> getProfile(@PathVariable Long id) {
        return service.findById(id)
                .map(Student::getProfile)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // Create or replace the profile
    @PutMapping("/{id}/profile")
    public ResponseEntity<Student> saveProfile(@PathVariable Long id, @RequestBody StudentProfile profile) {
        return service.saveProfile(id, profile)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}/profile")
    public ResponseEntity<Void> deleteProfile(@PathVariable Long id) {
        return service.removeProfile(id) ? ResponseEntity.noContent().build()
                                         : ResponseEntity.notFound().build();
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        return service.delete(id) ? ResponseEntity.noContent().build()
                                  : ResponseEntity.notFound().build();
    }
}
package com.example.onetoone.controller;

import com.example.onetoone.entity.StudentProfile;
import com.example.onetoone.service.StudentProfileService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

// Reads profiles from the other side: each profile shows the student it belongs to
@RestController
@RequestMapping("/profiles")
public class StudentProfileController {

    private final StudentProfileService service;

    public StudentProfileController(StudentProfileService service) {
        this.service = service;
    }

    @GetMapping
    public List<StudentProfile> getAll() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<StudentProfile> getById(@PathVariable Long id) {
        return service.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}

Step 9: Main Application

package com.example.onetoone;

import com.example.onetoone.entity.Student;
import com.example.onetoone.entity.StudentProfile;
import com.example.onetoone.repository.StudentRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class OneToOneApplication {

    public static void main(String[] args) {
        SpringApplication.run(OneToOneApplication.class, args);
    }

    // Adds sample data once. Saving a Student also saves its profile (cascade).
    @Bean
    CommandLineRunner seedData(StudentRepository repository) {
        return args -> {
            if (repository.count() == 0) {
                Student ravi = new Student("Ravi Kumar", "ravi@example.com");
                ravi.setProfile(new StudentProfile("9876543210", "12 MG Road", "Hyderabad"));

                Student sneha = new Student("Sneha Reddy", "sneha@example.com");
                sneha.setProfile(new StudentProfile("9123456780", "45 Fort Street", "Warangal"));

                Student anil = new Student("Anil Verma", "anil@example.com");   // no profile yet

                repository.save(ravi);
                repository.save(sneha);
                repository.save(anil);
            }
        };
    }
}

Step 10: Run the OneToOneApplication

Scroll to Top