Understand Hibernate @ManyToOne, @OneToMany With Examples

Overview

@ManyToOne and @OneToMany are standard mappings in Hibernate. Understanding how to use them correctly is the key to mapping entities efficiently in Hibernate.

In this post, I will show you how to use these annotations and give examples to demonstrate use cases.

The analogy I use is schools and students. In real life, schools can have many students but at one time, a student can attend only one school. This analogy makes a perfect use case for @OneToMany and @ManyToOne mapping.

Using @ManyToOne

When using @ManyToOne, beginners probably ask: where to put this annotation? The answer is you put it on the “many” side. In this case, @ManyToOne is the annotation you set on the Student side:

@Entity
@Data
public class Student {
    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    private School school;

}

You don’t need to do anything on the School side. This annotation alone is enough to create the mapping.

@Entity
@Data
public class School {

    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Override
    public String toString() {
        return "School{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Let’s try some code that creates and saves the school and students:

        var factory = Common.getSessionFactory();

        var em = factory.openSession().getEntityManagerFactory().createEntityManager();
        var tx = em.getTransaction();
        tx.begin();
        var school = new School();
        school.setName("Some school");

        em.persist(school);

        var janeDoe = new Student();
        janeDoe.setName("Jane Doe");
        janeDoe.setSchool(school);

        em.persist(janeDoe);

        tx.commit();

Behind the scene, you can see that Hibernate executes the SQL queries as expected:

Hibernate create and save entities

If you let Hibernate create the schema for you, it will use the following queries to create the tables and relationship:

create table School (id bigint not null auto_increment, name varchar(255), primary key (id)) engine=InnoDB
create table Student (id bigint not null auto_increment, name varchar(255), school_id bigint, primary key (id)) engine=InnoDB
alter table Student add constraint FKocquk3umb3lqp8t7r406iiwa1 foreign key (school_id) references School (id)

A column named “school_id” is created on the Student table and a foreign key is setup on that column. In case you need to use a different name for the foreign key column, specify the name you desired with @JoinColumn:

    @ManyToOne
    @JoinColumn(name = "sc_id")
    private School school;

This will result in the following table schema:

Using @JoinColumn to specify join column name

Owning side of a relationship

The concepts of “mappedBy” and the “owning side” are important to understand when dealing with bidirectional relationships between entities.

When you only specify @ManyToOne on the Student entity, the relationship is “unidirectional”. That means it’s a one-way relationship.

However, when you also use @OneToMany on the School entity, the relationship is now bidirectional. That means each side is pointing to the other. In such cases, specifying the owning side is important to create the correct mapping.

The owning side is responsible for managing the relationship and the associated foreign key column in the database. In this case, that’s the Student side. The other side is called inversed side (or mappedBy side). In this case, the mappedBy side is the School side.

Using mappedBy and @OneToMany

Most of the time, you only need to use @ManyToOne on the Student side. The relationship in this case is called unidirectional (one-way). However, when you need to get the list of Students of a School (bad idea), you put @OneToMany on the School entity. Doing so creates a bidirectional relationship.

  @Entity
@Data
public class School {

    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;
    private String name;
  
    @OneToMany(mappedBy = "school")
    private List<Student> students = new ArrayList<>();

    @Override
    public String toString() {
        return "School{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

When you use @ManyToOne, make sure to include “mappedBy” on the inversed side (in this case, that’s the School side). If you don’t include the annotation mappedBy in @OneToMany side, your application is still compiled but you will face strange database schemas. Hibernate will create a joining table (instead of relying on a foreign key), which is not what you want.

SQL queries when having mappedBy:

SQL queries when having mappedBy

SQL queries when omitting (accidentally) the mappedBy:

SQL queries when omitting the mappedBy:

Cascading Operations

If you know SQL, cascading operations are probably not a new thing. For example, if you want to persist the students when persisting the school, you will set cascade to PERSIST.

These are the options you can set to cascade:

public enum CascadeType { 

    /** Cascade all operations */
    ALL, 

    /** Cascade persist operation */
    PERSIST, 

    /** Cascade merge operation */
    MERGE, 

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since 2.0
     * 
     */   
    DETACH
}

If you set the cascade to ALL, all operations on the parent side (in this case, that’s the School entity) will be applied to the children side (Student).

Let’s consider an example:

    public static void main(String[] args) {
        var factory = Common.getSessionFactory();
//
        var em = factory.openSession().getEntityManagerFactory().createEntityManager();
        var tx = em.getTransaction();
        tx.begin();
        var school = new School();
        school.setName("Some school");


        var janeDoe = new Student();
        janeDoe.setName("Jane Doe");
        janeDoe.setSchool(school);

        school.getStudents().add(janeDoe);

        em.persist(school);

        tx.commit();

    }

As you can see, I only call the persist operation on a School object. However, when showing the SQL log, you can see that the student object was inserted into the database too:

Cascade persist operation in hibernate

Let’s see the cascading of the delete operation in practice. Now, I’m going to add one more student:

    public static void main(String[] args) {
        var tx = em.getTransaction();
        tx.begin();
        var school = new School();
        school.setName("Some school");


        var janeDoe = new Student();
        janeDoe.setName("Jane Doe");
        janeDoe.setSchool(school);

        var johnDoe = new Student();
        johnDoe.setName("John Doe");
        johnDoe.setSchool(school);


        school.getStudents().add(janeDoe);
        school.getStudents().add(johnDoe);

        em.persist(school);

        tx.commit();

    }

Now, there is one School and two Students in the database, let’s call the remove operation:

        tx.begin();
        var existingSchool = em.find(School.class, 1L);
        em.remove(existingSchool);
        tx.commit();

Here is the SQL log:

sql log of deleting a parent entity

As you can see, when I called the remove operation on the existingSchool entity, the students were removed first. The School entity was removed last.

If you pay close attention to the removal of the students, there were 2 delete operations (one for each row). This is not efficient. Imagine there are thousands of students, and that would result in thousands of SQL operations.

Using @OnDelete for Efficient Deletion

To fix the inefficient deletion problem above, you can use the @OnDelete annotation to include the cascading clause in SQL.

Thus, the School entity now looks like this:

@Entity
@Data
public class School {

    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
    @OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
    private List<Student> students = new ArrayList<>();
}

When running the same code as above, there are some differences:

cascade deletion in database

There are two notable points in this log. The first one is a foreign key was created with “on delete cascade” clause. This delegates the responsibility of deleting child entities to the database.

In addition, at point #2, you can see that there is only one delete SQL query. Hibernate didn’t have to delete the students first before deleting the school entity.

Lazy fetch and LazyInitializationException

At this stage, I have the Student and School entities with the following mapping:

@Entity
@Getter
@Setter
public class Student {
    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne()
    private School school;

}


@Entity
@Getter
@Setter
public class School {

    @Id
    @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
    @OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
    private List<Student> students = new ArrayList<>();

}

Let’s try to run the following code:

    public static void main(String[] args) {
        var factory = Common.getSessionFactory();
        var em = factory.openSession().getEntityManagerFactory().createEntityManager();

        var jane = em.find(Student.class, 1L);
        var existingSchool = em.find(School.class, 1L);


        em.close();

        System.out.println(jane.getSchool().getName());
        var students = existingSchool.getStudents();
        System.out.println(students.get(0).getName());

    }

What do you think the output would be?

The answer is after printing the school name, the application will throw an exception:

Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.datmt.hibernate.mapping.models.School.students: could not initialize proxy - no Session
	at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:582)
	at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:199)
	at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:562)
	at org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:136)
	at org.hibernate.collection.spi.PersistentBag.get(PersistentBag.java:531)
	at com.datmt.hibernate.mapping.SchoolTest.main(SchoolTest.java:21)

As you can read from the exception log, the LazyInitializationException was caused by the lack of a session. The exception was thrown on line 21 in my IDE (line 13 in the code snippet above). The lack of an active session was obvious since I closed the entity manager on line 9.

However, why didn’t line 11 throw an exception?

That’s because in the Student entity, I used @ManyToOne annotation. By default the fetch type of this annotation is EAGER. That means the school was loaded when I load the student.

The case is different on the School side. The default fetch type of @OneToMany is LAZY. That means the list of students was not loaded if the getStudents method is not called.

Conclusion

In this post, I’ve shown you how to use @OneToMany and @ManyToOne relation mapping. Remember to use mappedBy if you have a bidirectional relationship. In any bidirectional relationship, there is a owning side (the child side whose table has the foreign key), and an inversed side (the parent).

Leave a Comment