Post

Udemy Spring boot course: Section 9 JPA / Hibernate advanced mappings

Git repo for this sections code

Course Diagram

  • alt-text

Entity Lifecycle

  • hibernate entity life cycle
  • Every Hibernate entity naturally has a lifecycle within the framework – it’s either in a transient, managed, detached or deleted state.
OperationsDescription
New / TransientAn entity instance that has been created but is not yet managed by the persistence context. It is not associated with a database record.
DetachAn entity instance that was once managed but is no longer associated with the persistence context. Changes to a detached entity are not automatically synchronized with the database.
Mergewill reattach to hibernate session
Persist / managedAn entity instance that is actively managed by the persistence context and is associated with a database record. Changes to a managed entity will be synchronized with the database.
RemovedAn entity instance marked for deletion.
RefreshReload / sync object with data from db. Prevents stale data
  • alt-text

Important Database concepts

Primary Key , Foreign Key

  • Primary Key
    • Unique identifer for a row in a table
  • Foreign Key
    • Field in one table that refers to the primary key in another table
  • Referential Integerity
    • preserves relationship between tables
    • prevents operations that would destroy relationships
    • can only contain valid reference to primary key in another table

Example below shows how to create a foreign key in MySQL

1
2
3
4
5
6
CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    quantity INT,
    product_id INT,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Cascade

  • Cascade
    • You can apply the same operation to related entities
  • alt-text
    • if we save the instructor, if were to cascade we would also save the instructor detail
    • by the same token if we were to delete an instructor we would delete the instructor detail if it is one-to-one
  • by default no operations are cascaded
Cascade TypeDescription
Persistif entity is persisted / related entity will also be persisted
Removeif entity is removed / related entity will also be removed
Refreshif entity is refreshed / related entity will also be refreshed
Detachif entity is detached (not associated w/ session) / related entity will also be detached
Mergeif entity is merged / related entity will also be merged
AllAll the above cascade types

Eager , Lazy Loading

  • When fetch data, should we retrieve all of the results or just a subset of the results
  • Eager
    • Retrieve all of the results
  • Lazy
    • Retrieve on Request

Default Fetch Types

| Mappping | Contact | | :——– | :————– | | @OneToOne | FetchType.EAGER | | @ManyToOne | FetchType.EAGER | | @OneToMany | FetchType.LAZY | | @ManyToMany | FetchType.LAZY |

More about Lazy Loading

  • lazy loading required an open Hibernate session
  • need a connection to a database to retrieve data
  • if the hibernate session is closed and you attempt to retrieve lazy data, hibernate will throw an exception

Lazy Loading method 1

  • implmenting lazy loading i quite easy
    • all you do is have to specifiy to lazy loading in the entity class
    • if you want to access the elements of a lazy loaded element you have to do it separately and associate them separately

Entity Course class

1
2
3
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
@JoinColumn("instructor_id")
private Instructor instructor;

Entity Instructor class

1
2
3
// this is the variable in the Course class that joins the two tables together
@OneToMany(mappedBy="instructor")
private List<Course> courses;

Course Repository

1
2
3
4
5
public interface CourseRepository extends JpaRepository<Course, Integer> {
    @Query(value = "SELECT * FROM course where instructor_id = ?1",
    nativeQuery = true)
    List<Course> findCoursesByInstructorId(int instructor_id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void getCourses(InstructorServiceImpl instructorService) {
  // other implmenetatoin

    // get the courses separately
    List<Course> courseList = instructorService.getCourseRepository().findCoursesByInstructorId(instructor_id);
    // associate the courses to the instructor
    instructor.get().setCourses(courseList);

    // print out all of the courses for the instructor
    for(var course : instructor.get().getCourses()) {
      System.out.println(course);
    }
  }
}

Lazy Loading method 2 (Join Fetch)

  • stack overflow fetch
  • the previous method really sucks, lets be honest, you have to fetch the data separately then associate the data with the object
  • wouldn’t it be cool if we could write a single query and it does it all for us
    • WELL WE CAN, AND THAT IS WHAT THIS METHOD WILL DO
  • STEPS
    • Add the join fetch query to our repository, this will do it all in one step with a single query
    • Just use the new method on the repository

Instructor Repository

  • notice the Query ```java public interface InstructorRepository extends JpaRepository<Instructor, Integer> { @Query(“SELECT i FROM Instructor i “ + “JOIN FETCH i.courses “ + “WHERE i.id = ?1”) public Instructor findInstructorByIdJoinFetch(int id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Use in a method
```java
// get the instructor with the courses loaded thanks to the method we defined above
Instructor instructor = instructorService.getInstructorRepository().findInstructorByIdJoinFetch(instructor_id);

if(instructor == null){
  System.out.println("no instructor with id: " + instructor_id + " was found....");
}
else{
  for(var course : instructor.getCourses()) {
    System.out.println(course);
  }
}

We have options now!!

  • If you only need the instructor with no courses loaded
    • InstructorRepository.findInstructorById(...)
  • If you need the courses along with the instructor
    • InstructorRepository.findInstructorByIdJoinFetch(...)

Lazy loading error “Hibernate could not initialize proxy – no Session”

  • error fix and explaination
  • If you setup lazy loading you could get this error when trying to access the object or the object that is setup to be lazy loaded on the object
  • Access to a lazy-loaded object outside of the context of an open Hibernate session will result in this exception

Uni-Directional , Bi-Directional

Uni-Directional

  • alt-text
    • We retrieve the instructor detail only through the instructor
    • we can’t retrieve the instructor through the instructor detail

Bi-Directional

  • alt-text
  • we can get the instructor detail through the instructor
  • we can get the instructor through the instructor detail

How to make to make a bi-directional relationship

  • we want to make it so we can access the instructor from the instructor detail
1
2
3
4
// making the relationship bi-directional
// the 'mappedBy' refers to the instructorDetail property in the instructor class
@OneToOne(mappedBy = "instructorDetail")
private Instructor instructor;
  • This code is added to the InstructorDetail class
  • mappedBy refers to the instructorDetail variable in the Instructor class

Association Mappings

  • In the database we will have multiple tables and relationships between tables
    • We will need these advanced mappings to describe this in hibernate
Annotation
One-to-One
One-to-Many , Many-to-One
Many-to-Many

One-to-One

One-to-Many

  • alt-text

Many-to-Many

  • alt-text

@JoinColumn explained

  • alt-text
    • the name in parentheseis is the name of the column name in the given table. This column name acts as the foreign key into the other table, in this case instructor

mappedBy

  • alt-text
  • @OneToMany(mapedBy="instructor") tells spring to look at instructor variable in the other class that uses @Joincolumn(name="instructor_id")

Instructor class

1
2
3
4
5
6
public class Instructor {
  // one instructor can have multiple courses
  // mapped on the instructor variable in the Course class
  @OneToMany(mappedby="instructor", cascade = {CascadeType.PERSIST, CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH})
  private List<Course> courses;
}

Course class

1
2
3
4
5
6
7
public class Course {
  // multiple courses can be mapped to a single instructor
  @ManyToOne
  // Join the instructor table on the column name "instructor_id"
  @JoinColumn(name="instructor_id")
  private Instructor instructor;
}

Examples

  • alt-text

Instructor / instructor detail examle

Code to create the database schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DROP SCHEMA IF EXISTS `hb-01-one-to-one-uni`;

CREATE SCHEMA `hb-01-one-to-one-uni`;

use `hb-01-one-to-one-uni`;

SET FOREIGN_KEY_CHECKS = 0;

CREATE TABLE `instructor_detail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `youtube_channel` varchar(128) DEFAULT NULL,
  `hobby` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;


CREATE TABLE `instructor` (
  `id` int 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 DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_DETAIL_idx` (`instructor_detail_id`),
  CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`) REFERENCES `instructor_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

Instructor Detail entity table

  • nothing to really pay attention to here, normal jpa stuff we have seen before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Entity
@Table(name = "instructor_detail")
public class InstructorDetail {
    // fields
    @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
    public InstructorDetail() {
    }
    public InstructorDetail(String youtubeChannel, String hobby) {
        this.youtubeChannel = youtubeChannel;
        this.hobby = hobby;
    }

    // tostring
    @Override
    public String toString() {
        return "InstructorDetail{" +
                "id=" + id +
                ", youtubeChannel='" + youtubeChannel + '\'' +
                ", hobby='" + hobby + '\'' +
                '}';
    }

    // getter / setter methods


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getYoutubeChannel() {
        return youtubeChannel;
    }

    public void setYoutubeChannel(String youtubeChannel) {
        this.youtubeChannel = youtubeChannel;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }
}

Instructor entity table

  • so whenever there is a foreign key relationship the code to explain that goes in the dependent entity table
  • ex. since instructor has a foreign key that points to instructor detail table we make it known in this entity table
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    
    @Entity
    @Table(name = "instructor")
    public class Instructor {
    // field
    @Id
    @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;
    
    // cannot use otherwise will get an error at runtime
    // @Column(name = "instructor_detail_id")
    
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "instructor_detail_id")
    private InstructorDetail instructorDetail;
    
    // constructor
    
    public Instructor() {
    }
    
    public Instructor(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
    
    // tostring
    @Override
    public String toString() {
        return "Instructor{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", instructorDetail=" + instructorDetail +
                '}';
    }
    
    // getters and setters
    public int getId() {
        return id;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public InstructorDetail getInstructorDetailId() {
        return instructorDetail;
    }
    
    public void setInstructorDetailId(InstructorDetail instructorDetailId) {
        this.instructorDetail = instructorDetailId;
    }
    }
    

Instructor Course example

  • alt-text

Adding custom method when extending JpaRepository

  • stack overflow
  • You want to add a custom method when extending the JpaRepository
  • Steps
    • place the method signature in the interface when extending JpaRepository
    • create a class with the @Component annotation. name must be the interface + Impl
    • define the method in the class..

Extending from the JpaRepository and defining the custom method

1
2
3
4
5
@Repository
public interface InstructorDetailRepository extends JpaRepository<InstructorDetail, Integer> {
    // custom method
    public void RemoveDetailKeepInstructor(int id);
}

Class with @Annotation and the method defined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class InstructorDetailRepositoryImpl {
    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings("unused")
    @Transactional
    public void RemoveDetailKeepInstructor(int id) {
        // finds the instructor detail by the id
        InstructorDetail instructorDetail = entityManager.find(InstructorDetail.class, id);

        // break the bi-directional link
        instructorDetail.getInstructor().setInstructorDetailId(null);

        // remove the instructor detail
        entityManager.remove(instructorDetail);
    }
}

Many to Many implementation

  • In our example we have a course, student, and course_student table
  • the course_student is our join table
  • more on the inverse
    • alt-text
  • this is how we would handle it in our code

Student Entity class

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Student {
  // ...

  // field declaration for the courses associated with a student
  @ManyToMany
  @JoinTable(
          name = "course_student", // name of the join table
          joinColumns = @JoinColumn(name = "student_id"), // name of the column referring to this table
          inverseJoinColumns = @JoinColumn(name="course_id") // name of the other column referring to the courses table
  )
  private List<Course> courses;
}

Course Entity class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Course {
  // ...
  // field declaration for the students associated with a course
  @ManyToMany
  @JoinTable(
          name = "course_student",
          joinColumns = @JoinColumn(name = "course_id"),
          inverseJoinColumns = @JoinColumn(name = "student_id")
  )
  private List<Student> students;

  
  // method for convenience to add students to the list
  public void addStudent(Student student) {
    if(this.students == null) {
        this.students = new ArrayList<>();
    }
    this.students.add(student);
  }
}

Repository for course

1
2
3
4
5
6
public interface CourseRepository extends JpaRepository<Course, Integer> {
    // other implementation

    @Query(value = "SELECT c FROM Course c LEFT JOIN FETCH c.students WHERE c.id=?1")
    Course findCoursesLoadStudents(int courseId);
}

Method implementing this

  • Read the comment above course.addStudent(student);
  • this method is adding a new student to an existing course
  • this is achieved by adding a new student to the students list in the Course class.
    • saving will cascade to the Student causing a new entry into the Student table as well as an entry into the join table course_student
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void addNewStudent(InstructorServiceImpl instructorService) {
  // get the course id to add the student to
  System.out.print("course id: ");
  int courseId = scanner.nextInt();scanner.nextLine();

  // get the course from the db, loads the students into the students list
  Course course = instructorService.getCourseRepository().findCoursesLoadStudents(courseId);

  if(course == null) {
    System.out.println("unable to find course with id: " + courseId);
    return;
  }

  // assemble the new student
  System.out.print("first name: ");
  String firstName = scanner.nextLine();
  System.out.print("last name: ");
  String lastName = scanner.nextLine();
  String email = "%s.%s@gmail.com".formatted(firstName, lastName);
  Student student = new Student(firstName, lastName, email);

  // WE ONLY NEED TO ADD ONE WAY. MEANING WE DONT HAVE TO ADD THE COURSE TO THE STUDENT OBJECT.
  // THIS IS BECAUSE WHEN WE ADD TO ONE SIDE, IN THIS CASE ADDING A STUDENT TO THE COURSE ASSOCIATION. IT WILL 
  // ... WRITE INTO THE JOIN TABLE
  // .. IF YOU TRIED TO ADD THE OTHER WAY THAT WOULD CAUSE DUPLICATE ENTRIES INTO THE JOIN TABLE WHICH IS BAD!!
  // student.addCourse(course);

  // add student
  course.addStudent(student);

  // persists in the db
  instructorService.getCourseRepository().save(course);

}

This post is licensed under CC BY 4.0 by the author.