Problems with Hibernate One-To-Many (And Their Solutions)
This blog intends to be a reference of Hibernate mapping related problems and their solutions, and is not intended as an educational read. This is to prevent us from wasting time on finding solutions to the same problems again and again.
If you’re using JPA annotations, you may have come across the @OneToMany
@JoinColumn
and similar annotations. There are various nuances which come with these annotations.
Let’s begin by taking an example of what is OneToMany mapping, and what is the syntax in Hibernate.
Assume you have two tables — courses
and lessons
, where a Course can have multiple objects of type Lesson associated to it. In the database, it would be represented by a foreign key constraint.
For example, it could be something like this —
courses
----------------
| id | name |
| 1 | Intro to Java |
| 2 | Introduction to Docker|lessons
| id | course_id | name |
| 1 | 1 | Basic Syntax |
| 2 | 1 | Classes in Java |
In Java, you could use JPA annotations to create an association between these 2 objects.
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Size(min=5, max=60)
private String name;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "course_id", referencedColumnName = "id")
private List<Lesson> lessons;
This allows us to tell Hibernate that when it creates the Course object, it should load all lessons that have a foreign key constraint on that course.
Sounds amazing, doesn’t it?
Well, it is amazing, till the point where you start facing issues that are extremely painful and difficult to solve.
Let’s go into a little more details.
What is FetchType?
The join between Course and Lesson class could make the object quite heavy, and the query quite slow, so Hibernate gives us 2 options — Fetch the lessons eagerly or lazily.
If we choose Lazy, it will create a proxy object that will load the object only when it is queried.
Problems with FetchType.Eager
Problem: The obvious problem with FetchType.EAGER is that it can have a performance impact.
However, the more serious problem lies in a bad decision that Hibernate wants to persist with…..
Problem : When you do a OneToMany annotation, it does a Left Outer Join. For example, in the above example, if you load all records from the courses table and do an outer join on lessons, you will get 3 records. Unfortunately, when you map this to Java, Hibernate returns you 3 Course objects, not 2. That is apparently by design. You can read more in the Hibernate FAQ, or this StackOverflow question.
Solution: Various solutions are given in the Hibernate FAQ and the StackOverflow answer, including suggestions to choose a data-structure like Set instead of list to remove duplicates. Personally, I don’t like most suggestions, because they rely on the coder to solve the problem in code, rather than the framework to return only distinct records.
The solution I find most convenient is to use the FetchMode select —
@OneToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
This is a solution that will work, but has a major performance impact. This tells Hibernate to first load the Course objects, then go individually into each object, and do one query each to load the corresponding lessons. This makes the load really slow and bad on performance, but more accurate.
Because of these issues, you might be tempted to try FetchType Lazy
Problems with FetchType.Lazy
If you thought FetchType.Eager had problems, wait till you get into the details of the pain FetchType Lazy can cause.
Problem: Fetch Type Lazy will work only when the referenced object is queried within a Hibernate Session. If the session closes, an attempt to reference the object throws an exception.
This can be really annoying when you try to work with an object, only to realise that the items cannot be accessed because Hibernate had already closed the session.
Solution: If you annotate a Spring Method with @Transactional
annotation, it does not close the Hibernate session till the transaction is complete.
Problems with the Transactional Annotation
The Transaction annotation seems like a magic pill to solve all our problems, just like Hibernate, OneToMany, FetchType Lazy, but that also has its own problems.
Problem: The @Transactional
annotation will not work if the method annotated as @Transactional
is referenced from another method of the same class. For example, if you have the following code —
public class MyClass {
public void loadEverything(){
loadCourse(); }
@Transactional
public Course loadCourse(){...}}
The Transactional annotation will not make a difference. This is because when a method annotated as @Transactional
is called from outside, it is wrapped inside a proxy object that takes care of transactions. When the method is called from within the class, the method is called directly, and the transaction handling does not happen.
Solution: The solution is to use a TransactionTemplate to dynamically create a transaction and execute the method within that Transaction.
public class MyClass {
@Autowired TransactionTemplate template
public void loadEverything(){
template.execute(status -> loadCourse());
}
public Course loadCourse(){...}}
Problem: Saving data using 2 different repositories, and loading it within the object will not work within the same session.
course = courseRepository.save(new Course(null, courseName, "desc1", null, live, price));
lesson1 = lessonService.save(course.getId(), lesson1)
savedCourse = courseRepository.findByid(course.getId())
If the above code is within a single transaction, the savedCourse object will not contain the lesson object, even if it is associated to the course. This is because the session is still ongoing.
Solution: Keep saving and retrieving transactions separate. This is difficult to do, because you might want to save a complex object in a step by step manner without using the cascading provided by Hibernate, but use Hibernate to retrieve it. However, this will not work.
Summary
In some ways, working with Hibernate reminds me of SOAP. It feels organised and structured, but requires over-engineering, and can become painful and slow. If you have a project with a fairly large amount of complexity, you might want to choose a framework that allows you to write your own queries such as Mybatis or Norm.