Business Scenario
In this lab, we will extend the Medicare backend by introducing User and Doctor Profile entities and connecting them using a @OneToOne relationship.
Pre-Lab Preparation
Phase 4 — User + Doctor Profile + @OneToOne Relationship
Module:
1) Advance Spring data JPA and production ready Rest APi
Why ?
Because the doctor is actually made of two different kinds of information:
User information
-----------------
name
phone
role
Doctor-specific information
---------------------------
specialization
experienceYears
consultationFee
photoUrl
Create Role Enum
1
package com.medicare.entity;
public enum Role {
PATIENT,
DOCTOR
}Create User Entity
2
package com.medicare.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "users")
public class User { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 3, max = 100, message = "Name must be between 3 and 100 characters")
@Column(nullable = false)
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Please provide a valid email")
@Column(nullable = false, unique = true)
private String email;
@NotBlank(message = "Phone number is required")
@Size(min = 10, max = 15, message = "Phone number must be between 10 and 15 characters")
@Column(nullable = false)
private String phone;
@NotNull(message = "Role is required")
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
public User() {
}
public User(String name, String email, String phone, Role role) {
this.name = name;
this.email = email; this.phone = phone;
this.role = role;
}
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 String getPhone() {
return phone; }
public void setPhone(String phone) { this.phone = phone; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
}Create UserRepository
3
package com.medicare.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.User;
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
}Create UserNotFoundException
4
package com.medicare.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.User;
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
}Create UserService
5
package com.medicare.service;
import java.util.List;import org.springframework.stereotype.Service;
import com.medicare.entity.User;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService( UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
return userRepository.save(user);
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserById(Long id) {
return userRepository
.findById(id)
.orElseThrow(() ->
new UserNotFoundException( "User not found with id: " + id)
);
}
}Create UserController
6
package com.medicare.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.entity.User;
import com.medicare.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService; public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser( @Valid @RequestBody User user) {
User savedUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok( userService.getAllUsers() );
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById( @PathVariable Long id) {
return ResponseEntity.ok( userService.getUserById(id) );
}
}Update DoctorProfile.java
7
Doctor.java entity into DoctorProfile.javapackage com.medicare.entity;
import java.math.BigDecimal;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "doctor_profiles")
public class DoctorProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@OneToOne
@JoinColumn(name = "user_id", nullable = false, unique = true)
private User user;
@NotBlank(message = "Specialization is required")
@Size(max = 100, message = "Specialization cannot exceed 100 characters")
@Column(nullable = false)
private String specialization;
@NotNull(message = "Experience is required")
@Min(value = 0, message = "Experience cannot be negative")
@Max(value = 60, message = "Experience cannot exceed 60 years")
@Column(name = "experience_years", nullable = false)
private Integer experienceYears;
@NotNull(message = "Consultation fee is required")
@Positive(message = "Consultation fee must be greater than zero")
@Column(name = "consultation_fee", nullable = false, precision = 10, scale = 2)
private BigDecimal consultationFee;
@Size(max = 500, message = "Photo URL cannot exceed 500 characters")
@Column(name = "photo_url")
private String photoUrl;
public DoctorProfile() {
} public Long getId() {
return id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getSpecialization() {
return specialization;
}
public void setSpecialization(String specialization) {
this.specialization = specialization;
}
public Integer getExperienceYears() {
return experienceYears;
}
public void setExperienceYears(Integer experienceYears) {
this.experienceYears = experienceYears;
}
public BigDecimal getConsultationFee() {
return consultationFee; }
public void setConsultationFee(BigDecimal consultationFee) {
this.consultationFee = consultationFee;
}
public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(String photoUrl) {
this.photoUrl = photoUrl;
}
}Rename Repository
8
package com.medicare.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.DoctorProfile;public interface DoctorProfileRepository extends JpaRepository<DoctorProfile, Long> {
boolean existsByUserId(Long userId);
}Create DoctorProfileRequest
9
com.medicare.dto
package com.medicare.dto;
import java.math.BigDecimal;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
public class DoctorProfileRequest {
@NotNull(message = "User id is required")
private Long userId;
@NotBlank(message = "Specialization is required")
@Size(max = 100)
private String specialization;
@NotNull(message = "Experience is required")
@Min(0)
@Max(60)
private Integer experienceYears;
@NotNull(message = "Consultation fee is required")
@Positive
private BigDecimal consultationFee;
@Size(max = 500)
private String photoUrl;
public Long getUserId() {
return userId;
} public void setUserId(Long userId) {
this.userId = userId;
}
public String getSpecialization() {
return specialization;
}
public void setSpecialization(
String specialization) {
this.specialization = specialization;
}
public Integer getExperienceYears() {
return experienceYears;
}
public void setExperienceYears(
Integer experienceYears) {
this.experienceYears = experienceYears;
}
public BigDecimal getConsultationFee() {
return consultationFee;
}
public void setConsultationFee(
BigDecimal consultationFee) { this.consultationFee = consultationFee;
}
public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(String photoUrl) {
this.photoUrl = photoUrl;
}
}Update DoctorService
10
package com.medicare.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medicare.dto.DoctorProfileRequest;
import com.medicare.entity.DoctorProfile;
import com.medicare.entity.Role;
import com.medicare.entity.User;import com.medicare.exception.DoctorNotFoundException;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.DoctorProfileRepository;
import com.medicare.repository.UserRepository;
@Service
public class DoctorService {
private final DoctorProfileRepository doctorRepository;
private final UserRepository userRepository;
public DoctorService(
DoctorProfileRepository doctorRepository,
UserRepository userRepository) {
this.doctorRepository = doctorRepository;
this.userRepository = userRepository;
}
public DoctorProfile createDoctor( DoctorProfileRequest request) {
User user = findUserById( request.getUserId() );
if (user.getRole() != Role.DOCTOR) {
throw new IllegalArgumentException( "Doctor profile can only be
created for a DOCTOR user" );
}
if (doctorRepository.existsByUserId(user.getId())) {
throw new IllegalArgumentException(
"Doctor profile already exists for this user"
); }
DoctorProfile doctor = new DoctorProfile();
doctor.setUser(user);
doctor.setSpecialization( request.getSpecialization() );
doctor.setExperienceYears( request.getExperienceYears() );
doctor.setConsultationFee( request.getConsultationFee() );
doctor.setPhotoUrl( request.getPhotoUrl() );
return doctorRepository.save(doctor);
}
public List<DoctorProfile> getAllDoctors() {
return doctorRepository.findAll();
}
public DoctorProfile getDoctorById(Long id) {
return findDoctorById(id);
}
public DoctorProfile updateDoctor( Long id, DoctorProfileRequest request) {
DoctorProfile doctor = findDoctorById(id);
doctor.setSpecialization(request.getSpecialization()); doctor.setExperienceYears(request.getExperienceYears());
doctor.setConsultationFee(request.getConsultationFee());
doctor.setPhotoUrl( request.getPhotoUrl());
return doctorRepository.save(doctor);
}
public void deleteDoctor(Long id) {
DoctorProfile doctor = findDoctorById(id);
doctorRepository.delete(doctor);
}
private DoctorProfile findDoctorById(Long id) {
return doctorRepository.findById(id).orElseThrow(() ->
new DoctorNotFoundException(
"Doctor not found with id: " + id
)
);
}
private User findUserById(Long id) {
return userRepository.findById(id) .orElseThrow(() ->
new UserNotFoundException( "User not found with id: " + id)
);
}
}Update DoctorService
11
package com.medicare.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.dto.DoctorProfileRequest;
import com.medicare.entity.DoctorProfile;
import com.medicare.service.DoctorService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/doctors")
public class DoctorController { private final DoctorService doctorService;
public DoctorController(DoctorService doctorService) {
this.doctorService = doctorService;
}
@PostMapping
public ResponseEntity<DoctorProfile>
createDoctor( @Valid @RequestBody DoctorProfileRequest request) {
DoctorProfile savedDoctor = doctorService.createDoctor(request);
return ResponseEntity.status(HttpStatus.CREATED).body(savedDoctor);
}
@GetMapping
public ResponseEntity<List<DoctorProfile>>
getAllDoctors() {
return ResponseEntity.ok( doctorService.getAllDoctors());
}
@GetMapping("/{id}")
public ResponseEntity<DoctorProfile>
getDoctorById( @PathVariable Long id) {
return ResponseEntity.ok(doctorService.getDoctorById(id));
}
@PutMapping("/{id}")
public ResponseEntity<DoctorProfile>
updateDoctor(@PathVariable Long id,
@Valid @RequestBodyDoctorProfileRequest request) {
return ResponseEntity.ok(doctorService.updateDoctor(id,request)); }
@DeleteMapping("/{id}")
public ResponseEntity<Void>
deleteDoctor(@PathVariable Long id) {
doctorService.deleteDoctor(id);
return ResponseEntity.noContent().build();
}
}Update Global Exception Handler
12
UserNotFoundException
IllegalArgumentException
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse>
handleUserNotFound(UserNotFoundException exception, HttpServletRequest request) {
ErrorResponse errorResponse =
new ErrorResponse( LocalDateTime.now(),HttpStatus.NOT_FOUND.value(),
exception.getMessage(),request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse>
handleIllegalArgument(IllegalArgumentException exception,
HttpServletRequest request) {
ErrorResponse errorResponse = new ErrorResponse( LocalDateTime.now(), HttpStatus.BAD_REQUEST.value(),
exception.getMessage(),
request.getRequestURI()
);
return ResponseEntity
.badRequest()
.body(errorResponse);
}Restart Application
13
users
doctor_profiles
USE medicare_db;
SHOW TABLES;
doctor_profiles
doctors
users
Test Patient User
14
POST - http://localhost:8080/api/users
{ "name": "Rahul Shah", "email": "rahul@example.com", "phone": "9876543210", "role": "PATIENT" }
Test Doctor User
15
POST - http://localhost:8080/api/users
{
"name": "Dr. Ananya Sharma",
"email": "ananya@example.com",
"phone": "9876543211",
"role": "DOCTOR"
}
POST - http://localhost:8080/api/doctors
{ "userId": 2, "specialization": "Cardiologist", "experienceYears": 8, "consultationFee": 1200, "photoUrl": "https://example.com/ananya.jpg" }
Create Doctor Profile
16
{
"id": 1,
"user": {
"id": 2,
"name": "Dr. Ananya Sharma",
"email": "ananya@example.com",
"phone": "9876543211",
"role": "DOCTOR"
},
"specialization": "Cardiologist",
"experienceYears": 8,
"consultationFee": 1200,
"photoUrl": "https://example.com/ananya.jpg"
}Test Business Rule
17
{
"userId": 1,
"specialization": "Dentist",
"experienceYears": 5,
"consultationFee": 700
}400 BAD REQUEST
{
"timestamp": "...",
"status": 400,
"message": "Doctor profile can only be created for a DOCTOR user",
"path": "/api/doctors"
}Try Duplicate Doctor Profile
18
{
"userId": 2,
"specialization": "Neurologist",
"experienceYears": 10,
"consultationFee": 2000
}Doctor profile already exists for this user
Check Database
19
SELECT * FROM users;id | name | email | role
---------------------------------------------------------
1 | Rahul Shah | rahul@example.com | PATIENT
2 | Dr. Ananya Sharma | ananya@example.com | DOCTORSELECT * FROM doctor_profiles;id | user_id | specialization | experience_years | consultation_fee
--------------------------------------------------------------------
1 | 2 | Cardiologist | 8 | 1200We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully implemented User and Doctor Profile management in the Medicare backend using the @OneToOne relationship.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) Advance Spring data JPA and production ready Rest APi