Search code examples
javahibernatejpaormhibernate-mapping

Can a JPA entity have multiple OneToMany associations?


Adding two OneToMany associations to my entity class seems not to work. It works fine if I remove one of them.

@Entity
@Table(name = "school")
public class School {

    private List<Teacher> teachers;
    private List<Student> students;

    @OneToMany(cascade=CascadeType.ALL, mappedBy = "school", fetch = FetchType.EAGER)
    public List<Teacher> getTeachers()
        return this.teachers;
    }
    public void setTeachers(List<Teacher> teachers) {
         this.teachers = teachers;
    }

    @OneToMany(cascade=CascadeType.ALL, mappedBy = "school", fetch = FetchType.EAGER)
    public List<Student> getStudents()
        return this.students;
    }
    public void setStudents(List<Student> teachers) {
         this.students = students;
    }
}

Then in Teacher and Student I have the correct ManyToOne annotations

@Entity
@Table(name = "teacher")
public class Teacher {
    private School school;

    @ManyToOne
    @JoinColumn(name = "school_id")
    public School getSchool() {
        return this.school;
    }
    public void setSchool(School school) {
        this.school = school;
    }
}

@Entity
@Table(name = "student")
public class Student {
    private School school;

    @ManyToOne
    @JoinColumn(name = "school_id")
    public School getSchool() {
        return this.school;
    }
    public void setSchool(School school) {
        this.school = school;
    }
}

I also have id fields with the correct annotations (@Id, @GeneratedValue)

So to me it seems like I cannot have more than one @OneToMany in the same class. Is this correct?


Solution

  • Why FetchType.EAGER is a bad idea

    You can have multiple one-to-many associations, as long as only one is EAGER.

    But, even if you can use Set instead of List to bypass this MultipleBagFetchException, it's not a good thing to do because you'll end up with a Cartesian Product.

    Avoiding the Cartesian Product

    The best thing to do is to NOT use EAGER at all. You should use LAZY for all associations and eager fetch multiple many-to-one and one-to-one associations and at most one one-to-many association using the JOIN FETCH JPQL directive.

    If you want to fetch multiple one-to-many associations, you can use the Hibernate.initialize(proxy) method for the 2nd or 3rd association.

    Fetching multiple parents along with multiple child collections

    However, if you have a collection of parent entities that need to load multiple child associations, then you can use JOIN FETCH for the first collection since, if you try to eagerly fetch multiple child collections, then you'll end up with a Cartesian Product.

    SO, assuming you wanted to fetch multiple Post parent entities along with their comments and tags collections, you can fetch the comments in the first query:

    List<Post> _posts = entityManager
    .createQuery(
        "select distinct p " +
        "from Post p " +
        "left join fetch p.comments " +
        "where p.id between :minId and :maxId ", Post.class)
    .setParameter("minId", 1L)
    .setParameter("maxId", 50L)
    .setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
    .getResultList();
    

    And, for the second tags collection, you can issue the following secondary query:

    _posts = entityManager
    .createQuery(
        "select distinct p " +
        "from Post p " +
        "left join fetch p.tags t " +
        "where p in :posts ", Post.class)
    .setParameter("posts", _posts)
    .setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
    .getResultList();
    

    How does this work?

    The first query fetches the Post entities which are attached to the current EntityManager. The comments collection is fetched for each Post entity, but the tags collection is represented by a Proxy.

    The second query will fetch the tags, but since Hibernate already cached the Post entities, it will just replace the tags Proxy collections with the actual tags entries fetched from the database.