- Overview
- Database Concepts
- Mapping Types
- Entity Lifecycle
- Cascade Types
- Fetch Types
- Complete Examples
This guide covers advanced JPA/Hibernate mappings for modeling complex database relationships in Spring Boot applications.
- @OneToOne - Single entity relates to single entity
- @OneToMany / @ManyToOne - Single entity relates to multiple entities
- @ManyToMany - Multiple entities relate to multiple entities
- Uniquely identifies each row in a table
- Example:
idcolumn ininstructortable
- Links tables together
- References primary key in another table
- Example:
instructor_detail_idininstructortable referencesidininstructor_detailtable
- Apply same operation to related entities
- Example: Deleting instructor also deletes their instructor_detail
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!");
}
}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 theinstructorDetailproperty in theInstructorclass- Uses information from
Instructorclass's@JoinColumnto find the association - No database changes needed for bi-directional mapping - only Java code updates
Use Case: Instructor has many Courses, each Course belongs to one 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!");
}
}Use Case: Course has many Reviews (one direction only)
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!");
}Use Case: Course has many Students, Student has many 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!");
}
}| 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 |
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 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 |
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;| 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 |
| Mapping Type | Default Fetch Type |
|---|---|
@OneToOne |
EAGER |
@OneToMany |
LAZY |
@ManyToOne |
EAGER |
@ManyToMany |
LAZY |
// 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;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;
}| 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") |
| 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") |
| Mapping Type | Foreign Key Location | Owning Side | Inverse Side |
|---|---|---|---|
| Bidirectional | course_student join table | Course with @ManyToMany + @JoinTable |
Student with @ManyToMany(mappedBy="students") |
- 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
@JoinColumnor@JoinTableinformation - Example:
@OneToMany(mappedBy="instructor")looks at theinstructorproperty in theCourseclass
- 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 theinstructor_idcolumn
- 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")
)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
}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)
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`)// 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);// 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// 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);// 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);// 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);# 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<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>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
# 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)# 1. Find instructor detail
✓ InstructorDetail retrieved
✓ Instructor retrieved (eager)
# 2. Navigate both directions
✓ instructor.getInstructorDetail() works
✓ instructorDetail.getInstructor() works# 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 ✓# 1. Create course with reviews
✓ Course saved
✓ Reviews saved (cascade all)
# 2. Delete course
✓ Course deleted
✓ Reviews deleted (cascade remove) ✓# 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 automaticallyProblem:
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);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);Problem:
// Trying to delete instructor without handling courses
entityManager.remove(instructor); // Exception: FK constraintSolution:
// Break associations first
List<Course> courses = instructor.getCourses();
for (Course course : courses) {
course.setInstructor(null);
}
entityManager.remove(instructor);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;-
There is NO single "right" mapping - JPA/Hibernate supports multiple approaches. Adapt to your requirements.
-
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?
-
Lazy loading is default for collections - Use JOIN FETCH when you know you'll need the data.
-
mappedBy goes on the inverse side - The side WITHOUT the foreign key.
-
@JoinColumn goes on the owning side - The side WITH the foreign key.
-
Convenience methods maintain consistency - Always update both sides of bi-directional relationships.
-
Test your cascade configurations - Make sure delete operations behave as expected.
-
Use transactions for write operations - Always annotate save/update/delete methods with
@Transactional.
// @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;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);// ❌ 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);@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// ❌ 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;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
@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();// 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();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);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<>();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 - 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@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());
}
}-
Choose Bidirectional Only When Needed: Unidirectional relationships are simpler
-
Always Implement Helper Methods:
public void addCourse(Course course) { courses.add(course); course.setInstructor(this); }
-
Use Set for @ManyToMany: Prevents duplicates
@ManyToMany private Set<Course> courses = new HashSet<>();
-
Specify Fetch Strategy: Avoid LazyInitializationException
@OneToMany(fetch = FetchType.EAGER) // Or use JOIN FETCH
-
Use orphanRemoval Carefully:
@OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
-
Test Mapping Thoroughly: Complex mappings need comprehensive tests
-
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! 🚀