User Management & Doctor Profile Relationship

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​

  • Important design change - we will rename our existing Doctor entity to DoctorProfile now

Why ?

Because the doctor is actually made of two different kinds of information:

User information

-----------------

name

email

phone

role

Doctor-specific information

---------------------------

specialization

experienceYears

consultationFee

photoUrl

Create Role Enum

1

  • Inside: entity/ Package create: Role.java
package com.medicare.entity;
public enum Role {
    PATIENT,
    DOCTOR
}

Create User Entity

2

  • Inside: entity/ Package create: User.java
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

  • Inside: repository/ Package create: UserRepository.java
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

  • Inside: repository/ Package create: UserRepository.java
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

  • Inside: service/ Package create: UserService.java
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

  • Inside: controller/ Package create: UserController.java
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

  • At the beginning of this lab, we refactored the existing Doctor.java entity into DoctorProfile.java
  • Update the existing code and change it to :
package 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

  • Rename DoctorRepository.java to DoctorProfileRepository.java
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

  • Create package:

com.medicare.dto

  • Inside the package create: DoctorProfileRequest.java
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

  • Now our Doctor service works with DoctorProfile.
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

  • Add handling for:

UserNotFoundException

IllegalArgumentException

  • Inside the existing GlobalExceptionHandler, add:
@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);
}
  • And :
@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

  • Hibernate should now create:

  

users

doctor_profiles

  • Check:
USE medicare_db;

SHOW TABLES;
  • ​Expected outcome:

doctor_profiles

doctors

users

  • You might still see the old: doctors table from Phasebecause: spring.jpa.hibernate.ddl-auto=update doesn't automatically remove old tables.

Test Patient User

14

  • First create a patient.

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

  • Body:
{
    "name": "Rahul Shah",
    "email": "rahul@example.com",
    "phone": "9876543210",
    "role": "PATIENT"
}

Test Doctor User

15

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

  • Body:
{
    "name": "Dr. Ananya Sharma",
    "email": "ananya@example.com",
    "phone": "9876543211",
    "role": "DOCTOR"
}
  • Expected response : 201 CREATED

POST - http://localhost:8080/api/doctors

  • Body:
{
    "userId": 2,
    "specialization": "Cardiologist",
    "experienceYears": 8,
    "consultationFee": 1200,
    "photoUrl": "https://example.com/ananya.jpg"
}

Create Doctor Profile

16

  • Response approximately:
{
    "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

  • Try creating a doctor profile using: userId = 1
  • But user 1 is: PATIENT
{
    "userId": 1,
    "specialization": "Dentist",
    "experienceYears": 5,
    "consultationFee": 700
}
  • Send :
  • Expected:

400 BAD REQUEST

  • Response :
{
    "timestamp": "...",
    "status": 400,
    "message": "Doctor profile can only be created for a DOCTOR user",
    "path": "/api/doctors"
}

Try Duplicate Doctor Profile

18

  • Try creating another profile with:
{
    "userId": 2,
    "specialization": "Neurologist",
    "experienceYears": 10,
    "consultationFee": 2000
}
  • But User 2 already has a DoctorProfile.
  • Expected: 400 BAD REQUEST with :

Doctor profile already exists for this user

Check Database

19

  • Run:
SELECT * FROM users;
  • This reinforces the meaning of: @OneToOne
id | name               | email               | role
---------------------------------------------------------
1  | Rahul Shah         | rahul@example.com   | PATIENT
2  | Dr. Ananya Sharma  | ananya@example.com  | DOCTOR
  • You might see:
  • Now: 
SELECT * FROM doctor_profiles;
  • Something like:
id | user_id | specialization | experience_years | consultation_fee
--------------------------------------------------------------------
1  | 2       | Cardiologist   | 8                | 1200

We 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 branchName

Next-Lab Preparation

Module:

1) Advance Spring data JPA and production ready Rest APi​