About a year and an half ago I wrote an entry
about the problem that rises when mapping an entity with multiple bags using eager
fetching. At the end of the entry I suggested three different solutions: (a) to
use lazy fetching, (b) to use sets instead of bags, and (c) to use the
@IndexColumn annotation. Few months later I elaborated
on the usage of @IndexColumn, this time another way – using the @CollectionId
annotation.
Environment
·
Hibernate Entity Manager –
3.3.1.GA
·
Hibernate core – 3.2.5.GA
·
Hibernate annotations- 3.3.0.GA
·
Database – PostgreSQL 8.1
First one, first…
Just before we start a warning – the
@CollectionId annotation is a Hibernate specific annotation – not a part of the
specification. And it doesn't work on one-to-many associations (but it can be
used in conjunction with one-to-many associations). After putting that behind
of us lets see the problem.
The Problem
Assume
the following entities relation, a parent entity has two collections of child
entities. Both collections should be eagerly loaded.
First try will be to just to
map it as is (Child1 has a many-to-many association; Child2 has a one-to-many):
@ManyToMany( fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(name = "PARENT_CHILD1",
joinColumns = @JoinColumn(name = "PARENT_ID", referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD1_ID", referencedColumnName = "ID"))
List<Child1> child1s = new LinkedList<Child1>();
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
List<Child2> child2s = new LinkedList<Child2>();
But
when loading a persistence unit with the above configuration the
"org.hibernate.HibernateException: cannot simultaneously fetch multiple
bags" will be thrown.
Using
an idbag
The
reason is that when we add the @CollectionId to a List or a Collection its
semantics changes from "just a bag" to "a bag with primary
key" (a.k.a, idbag). It means that a surrogate key is assigned to each row
on the collection
When
transforming the association to Child1 into an idbag
(using the @CollectionId annotation) the problem is solved. The reason is that when
we switch the association semantics from "a simple bag" to "a
bag with primary key" (a.k.a, idbag) it means that a surrogate key is
assigned to each row on the collection.
@Entity
public class Parent {
………
@ManyToMany( fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(name = "PARENT_CHILD1",
joinColumns = @JoinColumn(name = "PARENT_ID", referencedColumnName = "ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD1_ID", referencedColumnName = "ID"))
@GenericGenerator(name="uuid-gen", strategy = "uuid")
@CollectionId(columns = @Column(name = "COL_ID"), type = @Type(type = "string"), generator = "uuid-gen")List<Child1> child1s = new LinkedList<Child1>();
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
List<Child2> child2s = new LinkedList<Child2>();
}
The
problem with bags is that when Hibernate eagerly loads multiple collections it
issues an outer join select to the RDBMS which can cause multiple entries on
some of the collections. But when using an idbag each row in the collections is
uniquely identified by the surrogate key, therefore Hibernate can identify it
within the collection, even if the SQL statement returns an entry more than
once.
Notice
the usage of a generator assigned to the collection id, the generator is
responsible for creating the surrogate keys of the collection rows. I decided
to use the UUID strategy. You can, of course, use JPA standard generators (such
as Sequence or Table generators). The @CollectionId references the COL_ID
column on PARENT_CHILD1 table (the join table). The value of the collection id
is invisible to the application (it is not being mapped to a property).
So
why not on one-to-many
The
Hibernate annotations documentation says that to announce idbag
semantics you should assign the @CollectionId to a @ManyToMany,
@CollectionOfElements, or @OneToMany (look at the table on this
section), but the Hibernate core documentation it says "Hibernate
provides a feature that allows you to map many to many associations and collections
of values to a table with a surrogate key." (here).
I've tried it and indeed when annotating a @OneToMany collection with the
@CollectionId an exception with the message "one-to-many collections with
identifiers are not supported " is thrown by Hibernate.
Idbag - Not Just For Eager Fetching
Don't
forge that you can use idbag for reasons other than solving multiple eager
associations. For example it can give a major performance boost over simple
bags for mutative operations. If entries in a collection have surrogate keys
Hibernate will be able to locate the matching rows in the database using these
keys (each row in the association table becomes unique) – there is no need for
the fetch -> delete-all -> insert-all cycle when updating the collection.