Hibernate: Extra-lazy collection fetching

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.

Comments