loading...

Hibernate - OneToOne, OneToMany, ManyToOne and ManyToMany

jhonifaber profile image Jonathan Faber ・6 min read

Through JPA annotations when we are using Hibernate, we are able to manage relationships between two tables as if objects they were. This makes easier to map database attributes with the application object model. Depending on the business logic and how we model, we can create unidirectional or bidirectional relationships.

@OneToOne (bidirectional)

The following image shows our database model. student_id is a foreign key (from now on FK) that points to student.

drawing

First of all, we should ask ourselves who is the owner of the relationship. This will tell us where the FK should be. A student is associated with a tuition and that tuition is associated with a unique student.

A good practice is to use cascade in the parent entity so that we can propagate the changes and apply them into children. In our example, It does not make sense tuition to exist if student does not exist, therefore, student will have the parent role.

If we have a look an the previous image, I have decided tuition to has the FK. We can think know as tuition as the owner of the relationship, owner of that FK (owning side) and student, non-owning of the relationship (non-owning side).
But, how can I create a bidirectional relationship if student would like to have tuition properties? We could think about having another FK in student side, but that would create an unnecessary duplicity in our database model. To map both entities correctly, we can use the annotations @JoinColumn and mappedBy.

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch=FetchType.LAZY)
    private Tuition tuition;

    /* Getters and setters */   
}
@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Double fee;

    //what column in Tuition table has the FK
    @JoinColumn(name = "student_id")
    @OneToOne(fetch = FetchType.LAZY)
    private Student student;

    /* Getters and setters */    
}

@JoinColumn shows the column name that we would like to point to in tuition table. With MappedBy, we can create a bidirectional relationship, even though we just have one FK, we can link both tables. In the end, the main goal of these annotations is to make sure where is the key that maps the relationships.

orphanRemoval=true means child entity should be removed automatically by the ORM if it's no longer referenced by a parent entity, eg. we have a collection of items and we remove one of them. That item does not have a reference now and it will be removed. Be careful, not to mix it up with cascadeType which are database level operations.

fetchType=LAZY, retrieves entity, only when we really need it. It is important to know that session must be open in order to invoke the getter and retrieve the entity since Hibernate uses proxy pattern (object proxying). Otherwise (when session is closed), entity would go from persistent state to detach and a LazyInitializationException will be thrown.

Let’s create a very simple test that checks the sql statement that is being triggered.

    @Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_one_bidirectional() {
        Student student = new Student();
        student.setName("Ana");

        Tuition tuition = new Tuition();
        tuition.setFee(200);
        tuition.setStudent(student);

        student.setTuition(tuition);

        entityManager.persist(student);
    }

drawing

The use of cascade in parent entity ensures that tuition will be persisted whenever student is persisted.

An alternative to previous example is to use @MapsId. As I said before, tuition makes no sense if student does not exist, only one tuition per student can be associated. With @MapsId we are saying to Hibernate that student_id is tuition PK(Primary Key) and also student FK. Both entities share now the same value and it is not necessary any more the use of @GeneratedValue for the generation of new ids in tuition.

drawing

@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    private Long id;

    private Double fee;

    @MapsId
    @OneToOne()
    private Student student;

    /* Getters and setters */
}


@OneToMany (bidirectional)

The following image shows our database model. university_id is the FK that points to University.

drawing

University can have many students so in university class we will have the @OneToMany. A student is associated with just one university that's why we use the @ManyToOne in student class. The owning side of these relationships is usually in the @ManyToOne and the mappedBy in the parent entity.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "university", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Student> students;

    /* Getters and setters */
}
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "university_id")
    private University university;

    /* Getters and setters */
}


@OneToMany (unidirectional)

drawing

In a @OneToMany unidirectional relationship, the @JoinColumn annotation points to the table of the ‘many’ (student in our example). Because of this, in the following image we see the @JoinColumn in University class. Student class will only have id and name fields.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "university_id")
    private List<Student> students;

    /* Getters and setters */
}

Let’s create the test and check the sql statements.

    @Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_many_unidirectional() {
        University university = new University();
        university.setName("Universidad de Las Palmas de Gran Canaria");

        Student student1 = new Student();
        student1.setName("Ana");

        Student student2 = new Student();
        student2.setName("Jonathan");

        university.setStudents(List.of(student1, student2));

        entityManager.persist(university);
    }

drawing

Why are there updates in the queries?
Since we didn’t map using FK in student (like in previous example), Hibernate has to run additional queries to resolve this.
It is much better(recommended) to use @ManyToOne if we want a unidirectional relationship or just create a bidirectional relationship. We will avoid running unnecessary queries.

@ManyToMany (bidirectional)

Our database model is as follows

drawing

With @ManyToMany, we should create a third table so that we can map both entities. This third table will have two FK pointing to their parent tables. Therefore, student_id points to student table and course_id points to course table.

@Entity
@Table(name="course")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Double fee;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students;

    /* Getters and setters */
}
@Entity
@Table(name="student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    })
    @JoinTable(
            name = "student_course",
            joinColumns = {@JoinColumn(name = "student_id")},
            inverseJoinColumns = {@JoinColumn(name = "course_id")}
    )
    private Set<Course> courses;

    /* Getters and setters */
}

In this example, I have decided that the owning side is student and is where we use the @JoinTable annotation. We have to specify there, the name of the table that links both tables (student_course). JoinColumns points to the owning side table (student) and InverseJoinColumns points to the inverse table of the owning side (course). I have decided to use Cascade Merge and Persist but not Cascade.Remove because if I delete a course, I don’t want to remove the students from that course.

As you can see in the example, I'm using Set rather than List in my association. This is because Hibernate deletes all the rows in student_course link to that entity, and re-insert the ones we did not want to delete. This is of course not efficient and unnecessary.
The following image shows the sql statements using List. I'm trying to delete just one course from a student that has 4 courses.

drawing

Remember the use of Set, it will avoid this kind of unwanted behaviour.

Discussion

pic
Editor guide