Wednesday 25 April 2012

Hibernate and Multiple Bags

Certain associations an Entity can have can be deemed 'bags' by Hibernate. A bag is an unordered collection that permits duplicate elements and when persisting these collections, the order of elements cannot be guaranteed. For these reasons, bags are an efficient way of representing a collection as you can add to a bag collection without the collection having been loaded. An example of a bag would be an ArrayList.

When designing the mapping between the objects and the database ie building entities, thought must be given to the type of associations between the entities and the fetching strategy for each association.

A common problem encountered when modelling entities using Hibernate is that of multiple bags. ie when an Entity contains two or more associations which are bags that have a fetch strategy of EAGER. This could be an Entity which has two member collections like the Company class below,  or an Entity that contains collections within its object graph, for example, the Company class may contain a collection of Buildings which in turn has a collection of MeetingRooms.

(Note: BaseEntity contains Id and Version fields.)


@Entity
public class Company extends BaseEntity {

    private List<Department> departments;
    private List<String> shareholders;

    @OneToMany(fetch=FetchType.EAGER)
    @Cascade(value ={CascadeType.SAVE_UPDATE, CascadeType.DELETE_ORPHAN})
    @JoinColumn(name="CO_ID") 
    public List<Department> getDepartments() {  
        return this.departments; 
    }

    public void setDepartments(List<Department> departments) {      
        this.departments = departments; 
    }  

    @CollectionOfElements(fetch=FetchType.EAGER,targetElement=java.lang.String.class) 
    @JoinTable(name="CO_SHR",joinColumns=@JoinColumn(name="CO_ID")) 
    @Column(name="CO_SHR") 
    public List<String> getShareholders() {  
        return this.shareholders; 
    }

    public void setShareholders(List<String> shareholders) {  
        this.shareholders = shareholders; 
    }

}


The Hibernate exception when identifying more than one bag to fetch is shown below:

Caused by: org.hibernate.HibernateException: cannot simultaneously fetch multiple bags
 at org.hibernate.loader.BasicLoader.postInstantiate(BasicLoader.java:66)
 at org.hibernate.loader.entity.EntityLoader.<init>(EntityLoader.java:75)

There are a few ways to resolve this problem once the offending association(s) have been identified. One option is to make all or all but one collection have a fetch strategy of LAZY. This is the default strategy for Hibernate. This would mean that LAZY associations would not be loaded until requested but if requested outside of a session, this would raise a LazyInitializationException. One way to get around this is to load the collection when in a session. For example, if the Company -> Departments association is changed to be @OneToMany, the collection can be loaded as described below using Hibernate.initialize:


    public Company findById(String id) {
        HibernateTemplate ht = getHibernateTemplate();
        Company company = (Company) ht.get(Company.class, id);
        Hibernate.initialize(company.getDepartments());
        return company;
    }


Another option is to revise the type of collection being used. Does it have to be java.util.List? An alternative could be to use java.util.Set which isn't treated by Hibernate as a bag. An example of this would be to change the Company -> Shareholders association to be a SortedSet as shown below:


    private SortedSet<String> shareholders;

    @CollectionOfElements(fetch=FetchType.EAGER,targetElement=java.lang.String.class)
    @JoinTable(name="CO_SHR",joinColumns=@JoinColumn(name="CO_ID"))
    @Column(name="CO_SHR")
    @Sort(type=SortType.NATURAL)
    public SortedSet<String> getShareholders() {
        return this.shareholders;
    }


(Note that if you use HashSet, the retrieved collection may not be in the same order as it was persisted.)


If it must be java.util.List, then another solution is to use @IndexColumn which would mean the Collection semantic is that of List and not Bag. As well as a name, the Index can have a base number which if not specified will default to zero.


    @CollectionOfElements(fetch=FetchType.EAGER,targetElement=java.lang.String.class)
    @JoinTable(name="CO_SHR",joinColumns=@JoinColumn(name="CO_ID"))
    @Column(name="CO_SHR")
    @IndexColumn(name="IDX",base=1)
    public List<String> getShareholders() {
        return this.shareholders;
    }


This post highlights the need to think carefully about all associations and pose questions like what needs to be EAGERly loaded, what is the effect on memory usage, etc...