- 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.
- In the database, one table holds a foreign key (here
profile_idinstudents) that points to the other table, and a UNIQUE constraint makes sure it is never shared. - In JPA you declare it with
@OneToOne, and@JoinColumnsays which column holds the foreign key. - 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 = trueon 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()andprofile.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

