As of release 3.2, Hibernate can perform per-item lazy initialization of collections. The reference documentation mentions it
here. When the association mapping (OneToMany or ManyToMany) is set to
lazy="extra", the collection implementation for the mapping takes on "smart" collection behavior, i.e. some collection operations such as
size(),
contains(),
get(), etc. do not trigger collection initialization. This is only sensible for very large collections, but it's quite handy nonetheless.
In the following example, I have two domain objects, Winery and Wine. A Winery has a OneToMany relationship to Wine. I made the relationship use
list semantics by adding the IndexColumn annotation. If you omit this and just use a List interface type for the collection, you will end up with
bag semantics and Hibernate will not optimize the get(int index) call. You can easily check what semantics Hibernate is using by getting a hold of the mapped collection implementation object and checking its class. If the class is
org.hibernate.collection.PersistentList, you're using list semantics and you will lazy initialize each individual item of the List if you access it by index (
get(int index) method invocation). If the class is
org.hibernate.collection.PersistentBag, you're using bag semantics and now the collection will eager load
all the items the first time you touch
any item in the List.
Here is my Winery class using Hibernate and JPA annotations:
1 import org.hibernate.annotations.*;
2
3 import javax.persistence.*;
4 import javax.persistence.Entity;
5 import javax.persistence.Table;
6 import java.util.ArrayList;
7 import java.util.List;
8
9 @Entity
10 @Table(name = "winery")
11 @Proxy
12 public class Winery {
13
14 @Id
15 @GeneratedValue(strategy = GenerationType.AUTO)
16 @Column(name = "id")
17 protected Long id;
18
19 @Version
20 @Column(name = "version")
21 protected Long version;
22
23 @Basic
24 @Column(name = "name", nullable = false, length = 80)
25 private String name;
26
27 @OneToMany(mappedBy = "winery", fetch = FetchType.LAZY)
28 @IndexColumn(name = "index", base = 1)
29 @Cascade(org.hibernate.annotations.CascadeType.ALL)
30 @LazyCollection(LazyCollectionOption.EXTRA)
31 private List<Wine> wines = new ArrayList<Wine>();
32
33 public Long getId() {
34 return id;
35 }
36
37 public void setId(Long id) {
38 this.id = id;
39 }
40
41 public Long getVersion() {
42 return version;
43 }
44
45 public void setVersion(Long version) {
46 this.version = version;
47 }
48
49 public String getName() {
50 return name;
51 }
52
53 public void setName(String name) {
54 this.name = name;
55 }
56
57 public List<Wine> getWines() {
58 return wines;
59 }
60
61 public void setWines(List<Wine> wines) {
62 this.wines = wines;
63 }
64
65
66 public boolean equals(Object o) {
67 if (this == o) return true;
68 if (o == null || getClass() != o.getClass()) return false;
69
70 Winery winery = (Winery) o;
71
72 if (id != null ? !id.equals(winery.id) : winery.id != null) return false;
73 if (name != null ? !name.equals(winery.name) : winery.name != null) return false;
74 if (version != null ? !version.equals(winery.version) : winery.version != null) return false;
75
76 return true;
77 }
78
79 public int hashCode() {
80 int result;
81 result = (name != null ? name.hashCode() : 0);
82 result = 31 * result + (id != null ? id.hashCode() : 0);
83 result = 31 * result + (version != null ? version.hashCode() : 0);
84 return result;
85 }
86
87 public void addWine(Wine wine) {
88 this.wines.add(wine);
89 wine.setWinery(this);
90 }
91 }
92
Now the integration test case. I used DbUnit to load the HSQLDB with one Winery and three associated Wines. Here's the relevant test:
1 @Test
2 public void extraLazyInitializationOfWines() {
3 final Winery winery = (Winery) session.get(Winery.class, -100L);
4 final List<Wine> wines = winery.getWines();
5 // Assert that we're using list semantics
6 assertEquals(PersistentList.class, wines.getClass());
7 int size = wines.size();
8 assertEquals(3, size);
9 final boolean isEmpty = wines.isEmpty();
10 assertTrue(!isEmpty);
11 final Wine firstIndexedWine = wines.get(0);
12 assertNotNull(firstIndexedWine);
13 assertEquals(-202L, firstIndexedWine.getId().longValue());
14 final Wine secondIndexedWine = wines.get(1);
15 assertNotNull(secondIndexedWine);
16 assertEquals(-201L, secondIndexedWine.getId().longValue());
17 final Wine thirdIndexedWine = wines.get(2);
18 assertNotNull(thirdIndexedWine);
19 assertEquals(-200L, thirdIndexedWine.getId().longValue());
20 }
Finally, the formatted SQL generated by Hibernate during the execution of this test case:
Hibernate: (The fetch of the initial Winery object)
select
winery0_.id as id3_0_,
winery0_.name as name3_0_,
winery0_.version as version3_0_
from
winery winery0_
where
winery0_.id=?
Hibernate: (An optimized SQL call to get the size() of the Wine list. Also used for isEmpty())
select
max(index) + 1
from
wine
where
winery_id =?
Hibernate: (First optimized per-item SQL call to retrieve the first Wine, by index = 0)
select
wine0_.id as id2_0_,
wine0_.name as name2_0_,
wine0_.version as version2_0_,
wine0_.wine_type as wine4_2_0_,
wine0_.winery_id as winery6_2_0_,
wine0_.year_bottled as year5_2_0_
from
wine wine0_
where
wine0_.winery_id=?
and wine0_.index=?
Hibernate: (Second optimized per-item SQL call to retrieve the second Wine, by index = 1)
select
wine0_.id as id2_0_,
wine0_.name as name2_0_,
wine0_.version as version2_0_,
wine0_.wine_type as wine4_2_0_,
wine0_.winery_id as winery6_2_0_,
wine0_.year_bottled as year5_2_0_
from
wine wine0_
where
wine0_.winery_id=?
and wine0_.index=?
Hibernate: (Third optimized per-item SQL call to retrieve the third Wine, by index = 2)
select
wine0_.id as id2_0_,
wine0_.name as name2_0_,
wine0_.version as version2_0_,
wine0_.wine_type as wine4_2_0_,
wine0_.winery_id as winery6_2_0_,
wine0_.year_bottled as year5_2_0_
from
wine wine0_
where
wine0_.winery_id=?
and wine0_.index=?
Notice that there are three calls to get Wine state from the database, each time constraining the query by winery_id foreign key and the current index for the Wine list. Removing the @IndexColumn drastically changes the generated SQL:
Hibernate: (The fetch of the initial Winery object)
select
winery0_.id as id3_0_,
winery0_.name as name3_0_,
winery0_.version as version3_0_
from
winery winery0_
where
winery0_.id=?
Hibernate: (An optimized SQL call to get the size() of the Wine list. Also used for isEmpty(). Note this is different than the list semantics query for size and isEmpty.)
select
count(id)
from
wine
where
winery_id =?
Hibernate: (An unoptimized SQL call to get all the Wine objects for the list. Due to the use of bag semantics, we do not have an optimized get(int index) method available on the Hibernate collection implementation, org.hibernate.collection.PersistentBag)
select
wines0_.winery_id as winery6_1_,
wines0_.id as id1_,
wines0_.id as id2_0_,
wines0_.name as name2_0_,
wines0_.version as version2_0_,
wines0_.wine_type as wine4_2_0_,
wines0_.winery_id as winery6_2_0_,
wines0_.year_bottled as year5_2_0_
from
wine wines0_
where
wines0_.winery_id=?
A question came up recently about inserting new elements into a extra-lazy mapped collection. Hibernate does not materialize the collection elements when accessing the collection and then inserting into the collection (at least in my example of Winery and Wine OneToMany relationship mapping):
120 @Test
121 public void addingWinesToWinery() {
122 final Winery winery = (Winery) session.get(Winery.class, -100L);
123
124 // Create a new wine
125 Wine wine = new Wine();
126 wine.setName("Vintner's Reserve");
127 wine.setWineType(WineType.CABERNET_SAUVIGNON);
128 wine.setYearBottled(2004);
129
130 // Set up the bi-directional relationship between winery and wine.
131 winery.getWines().add(wine);
132 wine.setWinery(winery);
133
134 // Save the winery, cascading the save to the new wine.
135 session.save(winery);
136 session.flush();
137 session.clear();
138
139 // Verify database state.
140 assertNotNull(wine.getId());
141 int rowsAffected = jdbcTemplate.queryForInt("select count(*) from wine where id = ?",
142 new Object[]{wine.getId()});
143 assertEquals(1, rowsAffected);
144
145 int afterCount = jdbcTemplate.queryForInt("select count(*) from wine where winery_id = ?",
146 new Object[] {winery.getId()});
147 assertEquals(4, afterCount);
148 }
This test case produces the following SQL statements from Hibernate:
Hibernate: (Fetch of the winery from Hibernate session.)
select
winery0_.id as id3_0_,
winery0_.name as name3_0_,
winery0_.version as version3_0_
from
winery winery0_
where
winery0_.id=?
Hibernate: (Cascaded save of the new wine.)
insert
into
wine
(id, list_index, name, version, wine_type, winery_id, year_bottled)
values
(null, ?, ?, ?, ?, ?, ?)
Hibernate: (Primary key generation for the new wine record.)
call identity()
Hibernate: (Update of the winery version number.)
update
winery
set
name=?,
version=?
where
id=?
and version=?
At no time do we materialize all of the wines. Again, this is only a demonstration of the OneToMany mapping with extra-lazy loading of the relationship collection.