Skip to content

Latest commit

 

History

History
2091 lines (1608 loc) · 54.3 KB

File metadata and controls

2091 lines (1608 loc) · 54.3 KB

JPA/Hibernate Advanced Mappings - Complete Guide

Table of Contents

  1. Overview
  2. Database Concepts
  3. Mapping Types
  4. Entity Lifecycle
  5. Cascade Types
  6. Fetch Types
  7. Complete Examples

Overview

This guide covers advanced JPA/Hibernate mappings for modeling complex database relationships in Spring Boot applications.

Mapping Types Covered

  • @OneToOne - Single entity relates to single entity
  • @OneToMany / @ManyToOne - Single entity relates to multiple entities
  • @ManyToMany - Multiple entities relate to multiple entities

Database Concepts

Primary Key

  • Uniquely identifies each row in a table
  • Example: id column in instructor table

Foreign Key

  • Links tables together
  • References primary key in another table
  • Example: instructor_detail_id in instructor table references id in instructor_detail table

Cascade Operations

  • Apply same operation to related entities
  • Example: Deleting instructor also deletes their instructor_detail

Mapping Types

1. @OneToOne Mapping

Uni-Directional Example

Use Case: Instructor has one InstructorDetail (like a profile)

Database Schema:

-- instructor_detail table
CREATE TABLE `instructor_detail` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `youtube_channel` varchar(128) DEFAULT NULL,
    `hobby` varchar(45) DEFAULT NULL,
    PRIMARY KEY (`id`)
);

-- instructor table
CREATE TABLE `instructor` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `first_name` varchar(45) DEFAULT NULL,
    `last_name` varchar(45) DEFAULT NULL,
    `email` varchar(45) DEFAULT NULL,
    `instructor_detail_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`)
    REFERENCES `instructor_detail` (`id`)
);

Entity Classes:

// InstructorDetail.java
@Entity
@Table(name="instructor_detail")
public class InstructorDetail {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="youtube_channel")
    private String youtubeChannel;
    
    @Column(name="hobby")
    private String hobby;
    
    // constructors, getters, setters
    public InstructorDetail() {}
    
    public InstructorDetail(String youtubeChannel, String hobby) {
        this.youtubeChannel = youtubeChannel;
        this.hobby = hobby;
    }
    
    // getters and setters...
}
// Instructor.java
@Entity
@Table(name="instructor")
public class Instructor {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="instructor_detail_id")
    private InstructorDetail instructorDetail;
    
    // constructors, getters, setters
    public Instructor() {}
    
    public Instructor(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
    
    // getters and setters...
}

DAO Layer:

// AppDAO.java
public interface AppDAO {
    void save(Instructor theInstructor);
    Instructor findInstructorById(int theId);
    void deleteInstructorById(int theId);
}
// AppDAOImpl.java
@Repository
public class AppDAOImpl implements AppDAO {
    
    private EntityManager entityManager;
    
    @Autowired
    public AppDAOImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    @Override
    @Transactional
    public void save(Instructor theInstructor) {
        // This will ALSO save the instructorDetail object
        // because of CascadeType.ALL
        entityManager.persist(theInstructor);
    }
    
    @Override
    public Instructor findInstructorById(int theId) {
        // This will ALSO retrieve the instructorDetail object
        // because of default @OneToOne fetch type (eager)
        return entityManager.find(Instructor.class, theId);
    }
    
    @Override
    @Transactional
    public void deleteInstructorById(int theId) {
        Instructor tempInstructor = entityManager.find(Instructor.class, theId);
        
        // This will ALSO delete the instructorDetail object
        // because of CascadeType.ALL
        entityManager.remove(tempInstructor);
    }
}

Main Application:

@SpringBootApplication
public class MainApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner commandLineRunner(AppDAO appDAO) {
        return runner -> {
            createInstructor(appDAO);
            // findInstructor(appDAO);
            // deleteInstructor(appDAO);
        };
    }
    
    private void createInstructor(AppDAO appDAO) {
        // Create the instructor
        Instructor tempInstructor = 
            new Instructor("Chad", "Darby", "darby@myapp.com");
        
        // Create the instructor detail
        InstructorDetail tempInstructorDetail = 
            new InstructorDetail("http://www.myapp.com/youtube", "Luv 2 code!!!");
        
        // Associate the objects
        tempInstructor.setInstructorDetail(tempInstructorDetail);
        
        // Save the instructor
        System.out.println("Saving instructor: " + tempInstructor);
        appDAO.save(tempInstructor);
        // NOTE: This will ALSO save the instructorDetail because of CascadeType.ALL
        System.out.println("Done!");
    }
    
    private void findInstructor(AppDAO appDAO) {
        int theId = 1;
        System.out.println("Finding instructor id: " + theId);
        
        Instructor tempInstructor = appDAO.findInstructorById(theId);
        
        System.out.println("tempInstructor: " + tempInstructor);
        System.out.println("the associated instructorDetail: " + 
                          tempInstructor.getInstructorDetail());
    }
    
    private void deleteInstructor(AppDAO appDAO) {
        int theId = 1;
        System.out.println("Deleting instructor id: " + theId);
        
        appDAO.deleteInstructorById(theId);
        // NOTE: This will ALSO delete the instructorDetail because of CascadeType.ALL
        
        System.out.println("Done!");
    }
}

Bi-Directional Example

Use Case: Navigate from InstructorDetail back to Instructor

Updated InstructorDetail.java:

@Entity
@Table(name="instructor_detail")
public class InstructorDetail {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="youtube_channel")
    private String youtubeChannel;
    
    @Column(name="hobby")
    private String hobby;
    
    // Add bi-directional mapping
    @OneToOne(mappedBy="instructorDetail", cascade=CascadeType.ALL)
    private Instructor instructor;
    
    // constructors, getters, setters
    public InstructorDetail() {}
    
    public InstructorDetail(String youtubeChannel, String hobby) {
        this.youtubeChannel = youtubeChannel;
        this.hobby = hobby;
    }
    
    public Instructor getInstructor() {
        return instructor;
    }
    
    public void setInstructor(Instructor instructor) {
        this.instructor = instructor;
    }
    
    // other getters and setters...
}

DAO Methods:

// AppDAO.java
public interface AppDAO {
    InstructorDetail findInstructorDetailById(int theId);
}

// AppDAOImpl.java
@Repository
public class AppDAOImpl implements AppDAO {
    
    @Override
    public InstructorDetail findInstructorDetailById(int theId) {
        // This will ALSO retrieve the instructor object
        // because of default @OneToOne fetch type (eager)
        return entityManager.find(InstructorDetail.class, theId);
    }
}

Usage Example:

private void findInstructorDetail(AppDAO appDAO) {
    int theId = 1;
    System.out.println("Finding instructor detail id: " + theId);
    
    InstructorDetail tempInstructorDetail = appDAO.findInstructorDetailById(theId);
    
    System.out.println("tempInstructorDetail: " + tempInstructorDetail);
    System.out.println("the associated instructor: " + 
                      tempInstructorDetail.getInstructor());
}

Key Points:

  • mappedBy="instructorDetail" tells Hibernate to look at the instructorDetail property in the Instructor class
  • Uses information from Instructor class's @JoinColumn to find the association
  • No database changes needed for bi-directional mapping - only Java code updates

2. @OneToMany / @ManyToOne Mapping

Bi-Directional Example

Use Case: Instructor has many Courses, each Course belongs to one Instructor

⚠️ Important Requirement: If you delete an instructor, DO NOT delete courses. If you delete a course, DO NOT delete the instructor.

Database Schema:

-- course table
CREATE TABLE `course` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `title` varchar(128) DEFAULT NULL,
    `instructor_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `TITLE_UNIQUE` (`title`),
    KEY `FK_INSTRUCTOR_idx` (`instructor_id`),
    CONSTRAINT `FK_INSTRUCTOR` 
    FOREIGN KEY (`instructor_id`) 
    REFERENCES `instructor` (`id`)
);

Entity Classes:

// Course.java
@Entity
@Table(name="course")
public class Course {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="title")
    private String title;
    
    // Many courses can belong to one instructor
    @ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE,
                        CascadeType.DETACH, CascadeType.REFRESH})
    @JoinColumn(name="instructor_id")
    private Instructor instructor;
    
    // constructors, getters, setters
    public Course() {}
    
    public Course(String title) {
        this.title = title;
    }
    
    // getters and setters...
}
// Updated Instructor.java
@Entity
@Table(name="instructor")
public class Instructor {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="instructor_detail_id")
    private InstructorDetail instructorDetail;
    
    // One instructor can have many courses
    @OneToMany(mappedBy="instructor",
               cascade={CascadeType.PERSIST, CascadeType.MERGE,
                       CascadeType.DETACH, CascadeType.REFRESH})
    private List<Course> courses;
    
    // constructors
    public Instructor() {}
    
    public Instructor(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
    
    // Convenience method for bi-directional relationship
    public void add(Course tempCourse) {
        if (courses == null) {
            courses = new ArrayList<>();
        }
        courses.add(tempCourse);
        tempCourse.setInstructor(this);
    }
    
    // getters and setters...
}

DAO Methods:

// AppDAO.java
public interface AppDAO {
    void save(Instructor theInstructor);
    Instructor findInstructorById(int theId);
    void update(Instructor tempInstructor);
    void deleteInstructorById(int theId);
    
    Course findCourseById(int theId);
    void update(Course tempCourse);
    void deleteCourseById(int theId);
}

// AppDAOImpl.java
@Repository
public class AppDAOImpl implements AppDAO {
    
    private EntityManager entityManager;
    
    @Autowired
    public AppDAOImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    @Override
    @Transactional
    public void update(Instructor tempInstructor) {
        entityManager.merge(tempInstructor);
    }
    
    @Override
    @Transactional
    public void deleteInstructorById(int theId) {
        // Retrieve the instructor
        Instructor tempInstructor = entityManager.find(Instructor.class, theId);
        
        // Get the courses
        List<Course> courses = tempInstructor.getCourses();
        
        // Break associations of all courses for instructor
        for (Course tempCourse : courses) {
            tempCourse.setInstructor(null);
        }
        
        // Delete the instructor
        // NOTE: We only delete the instructor, NOT the courses
        entityManager.remove(tempInstructor);
    }
    
    @Override
    public Course findCourseById(int theId) {
        return entityManager.find(Course.class, theId);
    }
    
    @Override
    @Transactional
    public void update(Course tempCourse) {
        entityManager.merge(tempCourse);
    }
    
    @Override
    @Transactional
    public void deleteCourseById(int theId) {
        // Retrieve the course
        Course tempCourse = entityManager.find(Course.class, theId);
        
        // Delete the course
        // NOTE: We only delete the course, NOT the instructor
        entityManager.remove(tempCourse);
    }
}

Usage Examples:

@SpringBootApplication
public class MainApplication {
    
    @Bean
    public CommandLineRunner commandLineRunner(AppDAO appDAO) {
        return runner -> {
            // createInstructorWithCourses(appDAO);
            // updateInstructor(appDAO);
            // updateCourse(appDAO);
            // deleteInstructor(appDAO);
            // deleteCourse(appDAO);
        };
    }
    
    private void createInstructorWithCourses(AppDAO appDAO) {
        // Create instructor
        Instructor tempInstructor = 
            new Instructor("Susan", "Public", "susan@myapp.com");
        
        // Create instructor detail
        InstructorDetail tempInstructorDetail = 
            new InstructorDetail("http://www.youtube.com", "Video Games");
        
        // Associate the objects
        tempInstructor.setInstructorDetail(tempInstructorDetail);
        
        // Create courses
        Course tempCourse1 = new Course("Air Guitar - The Ultimate Guide");
        Course tempCourse2 = new Course("The Pinball Masterclass");
        
        // Add courses to instructor (using convenience method)
        tempInstructor.add(tempCourse1);
        tempInstructor.add(tempCourse2);
        
        // Save the instructor
        System.out.println("Saving instructor: " + tempInstructor);
        System.out.println("The courses: " + tempInstructor.getCourses());
        appDAO.save(tempInstructor);
        System.out.println("Done!");
    }
    
    private void updateInstructor(AppDAO appDAO) {
        int theId = 1;
        
        // Find instructor
        System.out.println("Finding instructor id: " + theId);
        Instructor tempInstructor = appDAO.findInstructorById(theId);
        
        // Update instructor
        System.out.println("Updating instructor id: " + theId);
        tempInstructor.setLastName("TESTER");
        
        appDAO.update(tempInstructor);
        System.out.println("Done!");
    }
    
    private void updateCourse(AppDAO appDAO) {
        int theId = 10;
        
        // Find course
        System.out.println("Finding course id: " + theId);
        Course tempCourse = appDAO.findCourseById(theId);
        
        // Update course
        System.out.println("Updating course id: " + theId);
        tempCourse.setTitle("Enjoy the Simple Things");
        
        appDAO.update(tempCourse);
        System.out.println("Done!");
    }
    
    private void deleteInstructor(AppDAO appDAO) {
        int theId = 1;
        System.out.println("Deleting instructor id: " + theId);
        
        appDAO.deleteInstructorById(theId);
        // NOTE: Courses are NOT deleted, only instructor
        
        System.out.println("Done!");
    }
    
    private void deleteCourse(AppDAO appDAO) {
        int theId = 10;
        System.out.println("Deleting course id: " + theId);
        
        appDAO.deleteCourseById(theId);
        // NOTE: Instructor is NOT deleted, only course
        
        System.out.println("Done!");
    }
}

Uni-Directional Example

Use Case: Course has many Reviews (one direction only)

⚠️ Important Requirement: If you delete a course, also delete reviews (reviews without a course have no meaning).

Database Schema:

-- review table
CREATE TABLE `review` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `comment` varchar(256) DEFAULT NULL,
    `course_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `FK_COURSE_ID_idx` (`course_id`),
    CONSTRAINT `FK_COURSE` 
    FOREIGN KEY (`course_id`) 
    REFERENCES `course` (`id`)
);

Entity Classes:

// Review.java
@Entity
@Table(name="review")
public class Review {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="comment")
    private String comment;
    
    // constructors, getters, setters
    public Review() {}
    
    public Review(String comment) {
        this.comment = comment;
    }
    
    // getters and setters...
}
// Updated Course.java
@Entity
@Table(name="course")
public class Course {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="title")
    private String title;
    
    @ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE,
                        CascadeType.DETACH, CascadeType.REFRESH})
    @JoinColumn(name="instructor_id")
    private Instructor instructor;
    
    // One course can have many reviews
    @OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
    @JoinColumn(name="course_id")
    private List<Review> reviews;
    
    // constructors
    public Course() {}
    
    public Course(String title) {
        this.title = title;
    }
    
    // Convenience method for adding review
    public void add(Review tempReview) {
        if (reviews == null) {
            reviews = new ArrayList<>();
        }
        reviews.add(tempReview);
    }
    
    // getters and setters...
}

Usage Example:

private void createCourseAndReviews(AppDAO appDAO) {
    // Create course
    Course tempCourse = new Course("Pacman - How To Score One Million Points");
    
    // Add reviews
    tempCourse.add(new Review("Great course ... loved it!"));
    tempCourse.add(new Review("Cool course, job well done."));
    tempCourse.add(new Review("What a dumb course, you are an idiot!"));
    
    // Save the course
    System.out.println("Saving the course");
    System.out.println(tempCourse);
    System.out.println(tempCourse.getReviews());
    
    appDAO.save(tempCourse);
    // NOTE: This will ALSO save the reviews because of CascadeType.ALL
    
    System.out.println("Done!");
}

3. @ManyToMany Mapping

Bi-Directional Example

Use Case: Course has many Students, Student has many Courses

⚠️ Important Requirement: DO NOT use cascade delete! Deleting a course should NOT delete students. Deleting a student should NOT delete courses.

Database Schema:

-- student table
CREATE TABLE `student` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `first_name` varchar(45) DEFAULT NULL,
    `last_name` varchar(45) DEFAULT NULL,
    `email` varchar(45) DEFAULT NULL,
    PRIMARY KEY (`id`)
);

-- join table
CREATE TABLE `course_student` (
    `course_id` int(11) NOT NULL,
    `student_id` int(11) NOT NULL,
    PRIMARY KEY (`course_id`, `student_id`),
    
    KEY `FK_STUDENT_idx` (`student_id`),
    
    CONSTRAINT `FK_COURSE_05` 
    FOREIGN KEY (`course_id`) 
    REFERENCES `course` (`id`),
    
    CONSTRAINT `FK_STUDENT` 
    FOREIGN KEY (`student_id`) 
    REFERENCES `student` (`id`)
);

Entity Classes:

// Student.java
@Entity
@Table(name="student")
public class Student {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    // Many students can be in many courses
    @ManyToMany(fetch=FetchType.LAZY,
                cascade={CascadeType.PERSIST, CascadeType.MERGE,
                        CascadeType.DETACH, CascadeType.REFRESH})
    @JoinTable(
        name="course_student",
        joinColumns=@JoinColumn(name="student_id"),
        inverseJoinColumns=@JoinColumn(name="course_id")
    )
    private List<Course> courses;
    
    // constructors
    public Student() {}
    
    public Student(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
    
    // Convenience method
    public void add(Course tempCourse) {
        if (courses == null) {
            courses = new ArrayList<>();
        }
        courses.add(tempCourse);
    }
    
    // getters and setters...
}
// Updated Course.java
@Entity
@Table(name="course")
public class Course {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="title")
    private String title;
    
    @ManyToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE,
                        CascadeType.DETACH, CascadeType.REFRESH})
    @JoinColumn(name="instructor_id")
    private Instructor instructor;
    
    @OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
    @JoinColumn(name="course_id")
    private List<Review> reviews;
    
    // Many courses can have many students
    @ManyToMany(fetch=FetchType.LAZY,
                cascade={CascadeType.PERSIST, CascadeType.MERGE,
                        CascadeType.DETACH, CascadeType.REFRESH})
    @JoinTable(
        name="course_student",
        joinColumns=@JoinColumn(name="course_id"),
        inverseJoinColumns=@JoinColumn(name="student_id")
    )
    private List<Student> students;
    
    // constructors
    public Course() {}
    
    public Course(String title) {
        this.title = title;
    }
    
    // Convenience method
    public void add(Student tempStudent) {
        if (students == null) {
            students = new ArrayList<>();
        }
        students.add(tempStudent);
    }
    
    // getters and setters...
}

DAO Methods:

// AppDAO.java
public interface AppDAO {
    void save(Course theCourse);
    Course findCourseAndStudentsByCourseId(int theId);
    void update(Course tempCourse);
    void deleteCourseById(int theId);
    
    Student findStudentAndCoursesByStudentId(int theId);
    void update(Student tempStudent);
    void deleteStudentById(int theId);
}

// AppDAOImpl.java
@Repository
public class AppDAOImpl implements AppDAO {
    
    private EntityManager entityManager;
    
    @Autowired
    public AppDAOImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    @Override
    @Transactional
    public void save(Course theCourse) {
        entityManager.persist(theCourse);
    }
    
    @Override
    public Course findCourseAndStudentsByCourseId(int theId) {
        // Create query with JOIN FETCH to retrieve course and students
        TypedQuery<Course> query = entityManager.createQuery(
            "select c from Course c "
            + "JOIN FETCH c.students "
            + "where c.id = :data", Course.class);
        
        query.setParameter("data", theId);
        
        return query.getSingleResult();
    }
    
    @Override
    @Transactional
    public void update(Course tempCourse) {
        entityManager.merge(tempCourse);
    }
    
    @Override
    @Transactional
    public void deleteCourseById(int theId) {
        // Retrieve the course
        Course tempCourse = entityManager.find(Course.class, theId);
        
        // Delete the course
        // NOTE: Students are NOT deleted due to cascade configuration
        entityManager.remove(tempCourse);
    }
    
    @Override
    public Student findStudentAndCoursesByStudentId(int theId) {
        // Create query with JOIN FETCH to retrieve student and courses
        TypedQuery<Student> query = entityManager.createQuery(
            "select s from Student s "
            + "JOIN FETCH s.courses "
            + "where s.id = :data", Student.class);
        
        query.setParameter("data", theId);
        
        return query.getSingleResult();
    }
    
    @Override
    @Transactional
    public void update(Student tempStudent) {
        entityManager.merge(tempStudent);
    }
    
    @Override
    @Transactional
    public void deleteStudentById(int theId) {
        // Retrieve the student
        Student tempStudent = entityManager.find(Student.class, theId);
        
        // Delete the student
        // NOTE: Courses are NOT deleted due to cascade configuration
        entityManager.remove(tempStudent);
    }
}

Usage Examples:

@SpringBootApplication
public class MainApplication {
    
    @Bean
    public CommandLineRunner commandLineRunner(AppDAO appDAO) {
        return runner -> {
            // createCourseAndStudents(appDAO);
            // findCourseAndStudents(appDAO);
            // findStudentAndCourses(appDAO);
            // addMoreCoursesForStudent(appDAO);
            // deleteCourse(appDAO);
            // deleteStudent(appDAO);
        };
    }
    
    private void createCourseAndStudents(AppDAO appDAO) {
        // Create course
        Course tempCourse = new Course("Pacman - How To Score One Million Points");
        
        // Create students
        Student tempStudent1 = new Student("John", "Doe", "john@myapp.com");
        Student tempStudent2 = new Student("Mary", "Public", "mary@myapp.com");
        
        // Add students to course
        tempCourse.add(tempStudent1);
        tempCourse.add(tempStudent2);
        
        // Save the course and associated students
        System.out.println("Saving the course: " + tempCourse);
        System.out.println("Associated students: " + tempCourse.getStudents());
        
        appDAO.save(tempCourse);
        
        System.out.println("Done!");
    }
    
    private void findCourseAndStudents(AppDAO appDAO) {
        int theId = 10;
        
        System.out.println("Finding course id: " + theId);
        
        Course tempCourse = appDAO.findCourseAndStudentsByCourseId(theId);
        
        System.out.println("Course: " + tempCourse);
        System.out.println("Students: " + tempCourse.getStudents());
        
        System.out.println("Done!");
    }
    
    private void findStudentAndCourses(AppDAO appDAO) {
        int theId = 1;
        
        System.out.println("Finding student id: " + theId);
        
        Student tempStudent = appDAO.findStudentAndCoursesByStudentId(theId);
        
        System.out.println("Student: " + tempStudent);
        System.out.println("Courses: " + tempStudent.getCourses());
        
        System.out.println("Done!");
    }
    
    private void addMoreCoursesForStudent(AppDAO appDAO) {
        int theId = 2;
        
        // Find student
        Student tempStudent = appDAO.findStudentAndCoursesByStudentId(theId);
        
        // Create more courses
        Course tempCourse1 = new Course("Rubik's Cube - How to Speed Cube");
        Course tempCourse2 = new Course("Atari 2600 - Game Development");
        
        // Add courses to student
        tempStudent.add(tempCourse1);
        tempStudent.add(tempCourse2);
        
        System.out.println("Saving student");
        ```java
        System.out.println("Saving student: " + tempStudent);
        System.out.println("Associated courses: " + tempStudent.getCourses());
        
        appDAO.update(tempStudent);
        
        System.out.println("Done!");
    }
    
    private void deleteCourse(AppDAO appDAO) {
        int theId = 10;
        
        System.out.println("Deleting course id: " + theId);
        
        appDAO.deleteCourseById(theId);
        // NOTE: Students are NOT deleted, only the course
        // The join table entries are automatically removed
        
        System.out.println("Done!");
    }
    
    private void deleteStudent(AppDAO appDAO) {
        int theId = 1;
        
        System.out.println("Deleting student id: " + theId);
        
        appDAO.deleteStudentById(theId);
        // NOTE: Courses are NOT deleted, only the student
        // The join table entries are automatically removed
        
        System.out.println("Done!");
    }
}

Entity Lifecycle

Lifecycle Operations

Operation Description
Detach Entity is not associated with a Hibernate session
Merge Reattach detached entity to session
Persist Transitions new instance to managed state. Next flush/commit saves to DB
Remove Transitions managed entity to be removed. Next flush/commit deletes from DB
Refresh Reload/sync object with data from DB. Prevents stale data

Lifecycle State Transitions

New/Transient State
        ↓ (save/persist)
Persistent/Managed State
        ↓ (commit/rollback/close)
Detached State
        ↓ (merge)
Back to Persistent/Managed State

Persistent/Managed State
        ↓ (delete/remove)
Removed State

Cascade Types

Available Cascade Types

Cascade Type Description
PERSIST If entity is persisted/saved, related entity will also be persisted
REMOVE If entity is removed/deleted, related entity will also be deleted
REFRESH If entity is refreshed, related entity will also be refreshed
DETACH If entity is detached, related entity will also be detached
MERGE If entity is merged, related entity will also be merged
ALL All of the above cascade types

Configuration Examples

Single Cascade Type:

@OneToOne(cascade=CascadeType.ALL)
@JoinColumn(name="instructor_detail_id")
private InstructorDetail instructorDetail;

Multiple Cascade Types:

@OneToMany(mappedBy="instructor",
           cascade={CascadeType.PERSIST, 
                   CascadeType.MERGE,
                   CascadeType.DETACH, 
                   CascadeType.REFRESH})
private List<Course> courses;

⚠️ Important Note: By default, NO operations are cascaded. You must explicitly configure cascade types.


Fetch Types

Eager vs Lazy Loading

Fetch Type Description When to Use
EAGER Retrieves everything immediately Small datasets, frequently accessed data
LAZY Retrieves data on request Large datasets, infrequently accessed data

Default Fetch Types

Mapping Type Default Fetch Type
@OneToOne EAGER
@OneToMany LAZY
@ManyToOne EAGER
@ManyToMany LAZY

Configuration Examples

// Lazy loading for OneToMany
@OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
@JoinColumn(name="course_id")
private List<Review> reviews;

// Lazy loading for ManyToMany
@ManyToMany(fetch=FetchType.LAZY,
            cascade={CascadeType.PERSIST, CascadeType.MERGE,
                    CascadeType.DETACH, CascadeType.REFRESH})
@JoinTable(name="course_student",
           joinColumns=@JoinColumn(name="course_id"),
           inverseJoinColumns=@JoinColumn(name="student_id"))
private List<Student> students;

Working with Lazy Loading

Problem: LazyInitializationException when accessing lazy-loaded data after session closes

Solution 1: JOIN FETCH in JPQL

@Override
public Instructor findInstructorByIdJoinFetch(int theId) {
    TypedQuery<Instructor> query = entityManager.createQuery(
        "select i from Instructor i "
        + "JOIN FETCH i.courses "
        + "where i.id = :data", Instructor.class);
    
    query.setParameter("data", theId);
    
    return query.getSingleResult();
}

Solution 2: Access within transaction

@Override
@Transactional
public Instructor findInstructorById(int theId) {
    Instructor instructor = entityManager.find(Instructor.class, theId);
    
    // Access lazy-loaded data within transaction
    instructor.getCourses().size(); // Force initialization
    
    return instructor;
}

Complete Mapping Summary

@OneToOne Mappings

Mapping Type Foreign Key Location Owning Side Inverse Side
Unidirectional instructor table Instructor with @OneToOne + @JoinColumn -
Bidirectional instructor table Instructor with @OneToOne + @JoinColumn InstructorDetail with @OneToOne(mappedBy="instructorDetail")

@OneToMany / @ManyToOne Mappings

Mapping Type Foreign Key Location Owning Side Inverse Side
OneToMany Unidirectional review table Course with @OneToMany + @JoinColumn -
OneToMany/ManyToOne Bidirectional course table Course with @ManyToOne + @JoinColumn Instructor with @OneToMany(mappedBy="instructor")

@ManyToMany Mappings

Mapping Type Foreign Key Location Owning Side Inverse Side
Bidirectional course_student join table Course with @ManyToMany + @JoinTable Student with @ManyToMany(mappedBy="students")

Key Concepts and Best Practices

1. mappedBy Attribute

  • Used on the inverse (non-owning) side of a relationship
  • Tells Hibernate to look at the specified property in the owning entity
  • Uses the owning side's @JoinColumn or @JoinTable information
  • Example: @OneToMany(mappedBy="instructor") looks at the instructor property in the Course class

2. @JoinColumn

  • Used on the owning side to specify the foreign key column
  • Defines the physical mapping in the database
  • Example: @JoinColumn(name="instructor_id") maps to the instructor_id column

3. @JoinTable (for ManyToMany)

  • Specifies the join table name and column mappings
  • joinColumns: Maps the owning entity
  • inverseJoinColumns: Maps the inverse (other) entity
@JoinTable(
    name="course_student",
    joinColumns=@JoinColumn(name="course_id"),
    inverseJoinColumns=@JoinColumn(name="student_id")
)

4. Bi-Directional Convenience Methods

Always create convenience methods to keep both sides of relationship in sync:

public void add(Course tempCourse) {
    if (courses == null) {
        courses = new ArrayList<>();
    }
    courses.add(tempCourse);
    tempCourse.setInstructor(this); // Keep both sides in sync
}

5. Cascade Delete Guidelines

DO use cascade delete:

  • @OneToOne: Instructor → InstructorDetail (detail is meaningless without instructor)
  • @OneToMany (uni): Course → Review (reviews are meaningless without course)

DON'T use cascade delete:

  • @OneToMany/@ManyToOne (bi): Instructor ↔ Course (courses exist independently)
  • @ManyToMany: Course ↔ Student (both exist independently)

6. Database Schema Best Practices

Primary Keys:

PRIMARY KEY (`id`)

Foreign Keys with Constraints:

CONSTRAINT `FK_DETAIL` 
FOREIGN KEY (`instructor_detail_id`)
REFERENCES `instructor_detail` (`id`)

Unique Constraints:

UNIQUE KEY `TITLE_UNIQUE` (`title`)

Join Table Primary Keys:

PRIMARY KEY (`course_id`, `student_id`)

Common Patterns and Examples

Pattern 1: Create and Save Related Entities

// Create parent
Instructor instructor = new Instructor("John", "Doe", "john@example.com");

// Create child
InstructorDetail detail = new InstructorDetail("http://youtube.com", "Guitar");

// Associate
instructor.setInstructorDetail(detail);

// Save (cascade will save detail too)
appDAO.save(instructor);

Pattern 2: Load and Access Related Entities

// Eager loading (default for @OneToOne, @ManyToOne)
Instructor instructor = appDAO.findInstructorById(1);
System.out.println(instructor.getInstructorDetail()); // Already loaded

// Lazy loading with JOIN FETCH
Instructor instructor = appDAO.findInstructorByIdJoinFetch(1);
System.out.println(instructor.getCourses()); // Loaded via JOIN FETCH

Pattern 3: Update Relationships

// Load parent
Instructor instructor = appDAO.findInstructorById(1);

// Modify
instructor.setLastName("Smith");

// Add new child
Course newCourse = new Course("New Course");
instructor.add(newCourse);

// Update
appDAO.update(instructor);

Pattern 4: Delete with Relationship Management

// Delete parent WITHOUT deleting children
Instructor instructor = appDAO.findInstructorById(1);
List<Course> courses = instructor.getCourses();

// Break associations
for (Course course : courses) {
    course.setInstructor(null);
}

// Now delete parent (children remain)
entityManager.remove(instructor);

Pattern 5: Many-to-Many Relationships

// Load course with students
Course course = appDAO.findCourseAndStudentsByCourseId(10);

// Create new student
Student newStudent = new Student("Jane", "Smith", "jane@example.com");

// Add student to course
course.add(newStudent);

// Update (saves join table entry and student if new)
appDAO.update(course);

Application Configuration

application.properties

# Database connection
spring.datasource.url=jdbc:mysql://localhost:3306/hb-01-one-to-one-uni
spring.datasource.username=springstudent
spring.datasource.password=springstudent

# Turn off Spring Boot banner
spring.main.banner-mode=off

# Reduce logging level
logging.level.root=warn

# Show JPA/Hibernate logging messages
logging.level.org.hibernate.SQL=trace
logging.level.org.hibernate.orm.jdbc.bind=trace

# Configure JPA/Hibernate to auto create tables
# Create, create-drop, validate, update
spring.jpa.hibernate.ddl-auto=create

pom.xml Dependencies

<dependencies>
    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- MySQL Driver -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>

Project Structure

src/main/java/com/myapp/cruddemo/
├── CruddemoApplication.java          # Main Spring Boot application
├── dao/
│   ├── AppDAO.java                   # DAO interface
│   └── AppDAOImpl.java               # DAO implementation
└── entity/
    ├── Instructor.java               # Instructor entity
    ├── InstructorDetail.java         # InstructorDetail entity
    ├── Course.java                   # Course entity
    ├── Review.java                   # Review entity
    └── Student.java                  # Student entity

src/main/resources/
├── application.properties            # Configuration
└── sql/
    └── create-db.sql                # Database creation scripts

Testing Scenarios

Scenario 1: @OneToOne Uni-Directional

# 1. Create instructor with details
✓ Instructor saved
✓ InstructorDetail saved (cascade)

# 2. Find instructor
✓ Instructor retrieved
✓ InstructorDetail retrieved (eager)

# 3. Delete instructor
✓ Instructor deleted
✓ InstructorDetail deleted (cascade)

Scenario 2: @OneToOne Bi-Directional

# 1. Find instructor detail
✓ InstructorDetail retrieved
✓ Instructor retrieved (eager)

# 2. Navigate both directionsinstructor.getInstructorDetail() works
✓ instructorDetail.getInstructor() works

Scenario 3: @OneToMany/@ManyToOne

# 1. Create instructor with courses
✓ Instructor saved
✓ Courses saved (cascade persist)

# 2. Delete instructor
✓ Course.instructor set to null (manual)
✓ Instructor deleted
✓ Courses NOT deleted ✓

# 3. Delete course
✓ Course deleted
✓ Instructor NOT deleted ✓

Scenario 4: @OneToMany Uni-Directional

# 1. Create course with reviews
✓ Course saved
✓ Reviews saved (cascade all)

# 2. Delete course
✓ Course deleted
✓ Reviews deleted (cascade remove) ✓

Scenario 5: @ManyToMany

# 1. Create course with students
✓ Course saved
✓ Students saved (cascade persist)
✓ Join table entries created

# 2. Delete course
✓ Course deleted
✓ Students NOT deleted ✓
✓ Join table entries removed automatically

# 3. Delete student
✓ Student deleted
✓ Courses NOT deleted ✓
✓ Join table entries removed automatically

Troubleshooting Common Issues

Issue 1: LazyInitializationException

Problem:

Instructor instructor = appDAO.findInstructorById(1);
// Session closed here
List<Course> courses = instructor.getCourses(); // Exception!

Solution:

// Use JOIN FETCH
TypedQuery<Instructor> query = entityManager.createQuery(
    "select i from Instructor i "
    + "JOIN FETCH i.courses "
    + "where i.id = :data", Instructor.class);

Issue 2: Detached Entity Passed to Persist

Problem:

Course course = new Course("Title");
course.setId(10); // Setting ID makes it detached
entityManager.persist(course); // Exception!

Solution:

// Use merge for updates
entityManager.merge(course);

Issue 3: Foreign Key Constraint Violation

Problem:

// Trying to delete instructor without handling courses
entityManager.remove(instructor); // Exception: FK constraint

Solution:

// Break associations first
List<Course> courses = instructor.getCourses();
for (Course course : courses) {
    course.setInstructor(null);
}
entityManager.remove(instructor);

Issue 4: Cascade Delete Removing Too Much Data

Problem:

// Accidentally using CascadeType.ALL
@ManyToMany(cascade=CascadeType.ALL) // WRONG!
private List<Student> students;

// Deleting course deletes all students!

Solution:

// Use specific cascade types
@ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE,
                    CascadeType.DETACH, CascadeType.REFRESH})
private List<Student> students;

Important Reminders

  1. There is NO single "right" mapping - JPA/Hibernate supports multiple approaches. Adapt to your requirements.

  2. Cascade deletes depend on use case - Always consider:

    • Does the child entity have meaning without the parent?
    • Could the child be shared by multiple parents?
  3. Lazy loading is default for collections - Use JOIN FETCH when you know you'll need the data.

  4. mappedBy goes on the inverse side - The side WITHOUT the foreign key.

  5. @JoinColumn goes on the owning side - The side WITH the foreign key.

  6. Convenience methods maintain consistency - Always update both sides of bi-directional relationships.

  7. Test your cascade configurations - Make sure delete operations behave as expected.

  8. Use transactions for write operations - Always annotate save/update/delete methods with @Transactional.


Quick Reference Card

// @OneToOne Uni-Directional
@OneToOne(cascade=CascadeType.ALL)
@JoinColumn(name="detail_id")
private Detail detail;

// @OneToOne Bi-Directional (Inverse)
@OneToOne(mappedBy="detail", cascade=CascadeType.ALL)
private Owner owner;

// @ManyToOne (Owning Side)
@ManyToOne(cascade={...})
@JoinColumn(name="parent_id")
private Parent parent;

// @OneToMany (Inverse Side)
@OneToMany(mappedBy="parent", cascade={...})
private List<Child> children;

// @OneToMany Uni-Directional
@OneToMany(cascade=CascadeType.ALL)
@JoinColumn(name="parent_id")
private List<Child> children;

// @ManyToMany (Owning Side)
@ManyToMany(cascade={...})
@JoinTable(
    name="join_table",
    joinColumns=@JoinColumn(name="this_id"),
    inverseJoinColumns=@JoinColumn(name="other_id")
)
private List<Other> others;

// @ManyToMany (Inverse Side)
@ManyToMany(mappedBy="others", cascade={...})
private List<This> thisList;

⚠️ Lazy Loading & Proxy Issues

LazyInitializationException Problem

Problem: Accessing lazy-loaded collection outside transaction

// ❌ WRONG - Causes LazyInitializationException
@GetMapping("/instructor/{id}")
public String getInstructor(@PathVariable int id, Model model) {
    Instructor instructor = instructorRepository.findById(id).orElse(null);
    // Session closes here - courses collection becomes inaccessible
    instructor.getCourses().size();  // LazyInitializationException!
    return "instructor";
}

// ✅ CORRECT - Keep transaction open
@GetMapping("/instructor/{id}")
@Transactional  // Keeps session open
public String getInstructor(@PathVariable int id, Model model) {
    Instructor instructor = instructorRepository.findById(id).orElse(null);
    instructor.getCourses().size();  // Works fine
    model.addAttribute("instructor", instructor);
    return "instructor";
}

// ✅ CORRECT - Use eager loading
@ManyToOne(fetch = FetchType.EAGER)
private Instructor instructor;

// ✅ CORRECT - Use JOIN FETCH in query
@Query("SELECT i FROM Instructor i LEFT JOIN FETCH i.courses WHERE i.id = :id")
Instructor findByIdWithCourses(@Param("id") int id);

Proxy Object Issues

// ❌ WRONG - Proxy check fails
if (instructor instanceof Instructor) {  // May fail due to proxy
    // ...
}

// ✅ CORRECT - Use Hibernate utilities
Instructor realInstructor = HibernateProxyHelper.getRealClass(instructor);

// ✅ CORRECT - Use class equality
if (instructor.getClass().equals(Instructor.class)) {
    // ...
}

// ✅ CORRECT - Use reflection
Instructor proxied = (Instructor) instructor;
Instructor real = (Instructor) Hibernate.unproxy(proxied);

🗑️ Orphan Deletion & Cascade

Orphan Removal Strategy

@Entity
public class Instructor {
    
    @OneToMany(
        mappedBy = "instructor",
        cascade = CascadeType.ALL,
        orphanRemoval = true  // Automatically delete orphaned courses
    )
    private List<Course> courses = new ArrayList<>();
    
    public void addCourse(Course course) {
        courses.add(course);
        course.setInstructor(this);
    }
    
    public void removeCourse(Course course) {
        courses.remove(course);
        course.setInstructor(null);  // Orphan removed automatically
    }
}

// Usage
instructor.removeCourse(course);  // Course deleted from DB automatically

Cascade Strategies

// ❌ REMOVE ALL CASCADE TYPES - Child data survives parent deletion
@OneToMany(mappedBy = "parent")
private List<Child> children;

// ✅ PERSIST - Child automatically saved when parent saved
@OneToMany(cascade = CascadeType.PERSIST)
private List<Child> children;

// ✅ MERGE - Child merged when parent merged
@OneToMany(cascade = CascadeType.MERGE)
private List<Child> children;

// ✅ ALL - All operations cascade
@OneToMany(cascade = CascadeType.ALL)
private List<Child> children;

// ✅ BEST PRACTICE - Explicit cascades + orphan removal
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE},
           orphanRemoval = true)
private List<Child> children;

🔍 Query Optimization

N+1 Query Problem

Problem: One query + N additional queries for related data

// ❌ CAUSES N+1 PROBLEM
List<Instructor> instructors = instructorRepository.findAll();
for (Instructor instr : instructors) {
    System.out.println(instr.getCourses().size());  // N additional queries!
}
// Total: 1 + N queries

// ✅ SOLUTION - Fetch Join
@Query("SELECT DISTINCT i FROM Instructor i LEFT JOIN FETCH i.courses")
List<Instructor> findAllWithCourses();

// Result: 1 query with all data

Entity Graph for Complex Queries

@Entity
@NamedEntityGraph(
    name = "Instructor.withCoursesAndStudents",
    attributeNodes = {
        @NamedAttributeNode(value = "courses", subgraph = "courses-subgraph")
    },
    subgraphs = {
        @NamedSubgraph(
            name = "courses-subgraph",
            attributeNodes = @NamedAttributeNode("students")
        )
    }
)
public class Instructor {
    // ...
}

// Usage
@Query("SELECT i FROM Instructor i")
@EntityGraph("Instructor.withCoursesAndStudents")
List<Instructor> findAllOptimized();

Projection for DTO Results

// DTO without lazy loading issues
public class InstructorDTO {
    private Integer id;
    private String firstName;
    private String lastName;
    private Integer courseCount;
    
    public InstructorDTO(Integer id, String firstName, String lastName, 
                         Integer courseCount) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.courseCount = courseCount;
    }
}

// Query using projection
@Query("SELECT new com.example.dto.InstructorDTO(i.id, i.firstName, i.lastName, SIZE(i.courses)) " +
       "FROM Instructor i")
List<InstructorDTO> findAllAsDTO();

⚠️ Common Mapping Issues & Solutions

Issue: Foreign Key Constraint Violation

Problem: Cannot delete parent with children

ERROR: Cannot delete or update a parent row: a foreign key constraint fails

Solution: Cascade delete or remove children first

// Option 1: Cascade delete
@OneToMany(cascade = CascadeType.REMOVE)
private List<Child> children;

// Option 2: Orphan removal
@OneToMany(orphanRemoval = true)
private List<Child> children;

// Option 3: Delete children first
instructorService.deleteChildren(instructorId);
instructorRepository.deleteById(instructorId);

Issue: Duplicate Entries in @ManyToMany

Problem: Getting duplicate results with JOINs

// ❌ WRONG - Returns duplicates
List<Student> students = studentRepository.findAll();
for (Student s : students) {
    List<Course> courses = s.getCourses();  // May have duplicates
}

// ✅ CORRECT - Use DISTINCT
@Query("SELECT DISTINCT s FROM Student s LEFT JOIN FETCH s.courses")
List<Student> findAllWithCourses();

// ✅ CORRECT - Use Set instead of List
@ManyToMany
private Set<Course> courses = new HashSet<>();

Issue: Cannot Update Parent-Child Relationship

Problem: Changes to relationship not persisting

// ❌ WRONG - Doesn't persist changes
instructor.getCourses().add(newCourse);

// ✅ CORRECT - Use helper methods and transactions
@Transactional
public void addCourseToInstructor(int instructorId, Course course) {
    Instructor instructor = instructorRepository.findById(instructorId)
        .orElseThrow();
    instructor.addCourse(course);
    instructorRepository.save(instructor);
}

// Helper method in entity
public void addCourse(Course course) {
    if (courses == null) {
        courses = new ArrayList<>();
    }
    courses.add(course);
    course.setInstructor(this);
}

📊 Bidirectional vs Unidirectional

Bidirectional Benefits & Costs

// ✅ BIDIRECTIONAL - Query from both sides, but more complex
@Entity
public class Instructor {
    @OneToMany(mappedBy = "instructor", cascade = CascadeType.ALL)
    private List<Course> courses;
}

@Entity
public class Course {
    @ManyToOne
    private Instructor instructor;
}

// Usage: Can query from both directions
instructor.getCourses();  // Works
course.getInstructor();   // Works

// ❌ UNIDIRECTIONAL - Simple but limited
@Entity
public class Instructor {
    @OneToMany(cascade = CascadeType.ALL)
    private List<Course> courses;
}

@Entity
public class Course {
    // No reference to instructor
}

// Can only query: Instructor -> Courses
// Cannot query: Course -> Instructor directly

🧪 Testing Complex Mappings

@SpringBootTest
@Transactional
public class AdvancedMappingTest {
    
    @Autowired
    private InstructorRepository instructorRepository;
    
    @Test
    public void testOneToOneMapping() {
        Instructor instructor = new Instructor("John", "Doe", "john@example.com");
        InstructorDetail detail = new InstructorDetail("java_guru", "coding");
        
        instructor.setInstructorDetail(detail);
        instructorRepository.save(instructor);
        
        Instructor retrieved = instructorRepository.findById(instructor.getId()).orElse(null);
        assertNotNull(retrieved.getInstructorDetail());
        assertEquals("java_guru", retrieved.getInstructorDetail().getYoutubeChannel());
    }
    
    @Test
    public void testOneToManyMapping() {
        Instructor instructor = new Instructor("Jane", "Smith", "jane@example.com");
        Course course1 = new Course("Java Basics");
        Course course2 = new Course("Advanced Java");
        
        instructor.addCourse(course1);
        instructor.addCourse(course2);
        
        instructorRepository.save(instructor);
        
        Instructor retrieved = instructorRepository.findById(instructor.getId()).orElse(null);
        assertEquals(2, retrieved.getCourses().size());
    }
    
    @Test
    public void testManyToManyMapping() {
        Course course = new Course("Spring Framework");
        Student student1 = new Student("Bob", "Wilson");
        Student student2 = new Student("Alice", "Johnson");
        
        course.addStudent(student1);
        course.addStudent(student2);
        
        courseRepository.save(course);
        
        Course retrieved = courseRepository.findById(course.getId()).orElse(null);
        assertEquals(2, retrieved.getStudents().size());
    }
}

💡 Best Practices

  1. Choose Bidirectional Only When Needed: Unidirectional relationships are simpler

  2. Always Implement Helper Methods:

    public void addCourse(Course course) {
        courses.add(course);
        course.setInstructor(this);
    }
  3. Use Set for @ManyToMany: Prevents duplicates

    @ManyToMany
    private Set<Course> courses = new HashSet<>();
  4. Specify Fetch Strategy: Avoid LazyInitializationException

    @OneToMany(fetch = FetchType.EAGER)  // Or use JOIN FETCH
  5. Use orphanRemoval Carefully:

    @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
  6. Test Mapping Thoroughly: Complex mappings need comprehensive tests

  7. Monitor SQL Queries: Use logging to detect N+1 problems

    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.format_sql=true
    logging.level.org.hibernate.SQL=DEBUG

Happy Coding! 🚀