Search This Blog

Tuesday 24 July 2012

lazy="extra"

Hibernate uses proxies to ensure that the data is fetched only on a need basis. So if we have a Shelf class that has a set of books, the loading of a shelf object will not load the Book set. Instead a collection proxy will be assigned to the field. It is only when we access the methods of the collection, that the actual fetch occurs.
public class Book {
    private String name;
    private Integer id;
    private Shelf shelf;
        //setter -getter methods
}
And the shelf class is as below:
public class Shelf {
    private Integer id;
    private String code;
    private Set<Book> allBooks = new HashSet<Book>();
        //setter -getter methods
}
The hbms for the two would be as below:
Book.hbm.xml:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.collection.smart">
    <class name="Book" table="BOOK">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="name" type="string">
            <column name="Name" length="50" not-null="true" />
        </property>
        <many-to-one name="shelf" class="Shelf" foreign-key="BOOK_FK1">
            <column name="shelf_id"></column>
        </many-to-one>
    </class>
</hibernate-mapping>
Shelf.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.collection.smart">
    <class name="Shelf" table="SHELF">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="code" type="string">
            <column name="CODE" length="50" not-null="true" />
        </property>
        <set name="allBooks" cascade="save-update" inverse="true">
            <key column="SHELF_ID" not-null="true" />
            <one-to-many class="Book" />
        </set>
    </class>
</hibernate-mapping>
I wrote the below code to simply load a Book object.
public static void testLoad() {
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Shelf shelf = (Shelf) session.load(Shelf.class, shelfId);
    System.out.println("Number of books on " + shelf.getCode() + " is "
            + shelf.getAllBooks().size());
    transaction.commit();
    session.close();
}
If we were to execute the code, we can see that our HashSet class has been replaced with a run time generated class:
The set is loaded only when we call the getAllBooks() method. (click on the image to enlarge it)
/* load com.collection.smart.Shelf */  
    select
        shelf0_.ID as ID1_0_,
        shelf0_.CODE as CODE1_0_ 
    from
        SHELF shelf0_ 
    where
        shelf0_.ID=?
Hibernate: 
    /* load one-to-many com.collection.smart.Shelf.allBooks */  
    select
        allbooks0_.SHELF_ID as SHELF3_1_,
        allbooks0_.ID as ID1_,
        allbooks0_.ID as ID0_0_,
        allbooks0_.Name as Name0_0_,
        allbooks0_.shelf_id as shelf3_0_0_ 
    from
        BOOK allbooks0_ 
    where
        allbooks0_.SHELF_ID=?
Number of books on SH001 is 2
There could be scenarios wherein you simply need to test the size of the set or check if a particular book is present on the set. In such cases the records will be fetched from the books table, although we do not need them as we are not performing any operations on the set.
Hibernate provides an additional lazy setting for collections - lazy="extra". The collection is now a smarter collection. It does not fetch all records for a size method call.
<set name="allBooks" cascade="save-update" inverse="true" lazy="extra">
    <key column="SHELF_ID" not-null="true" />
    <one-to-many class="Book" />
</set>
Consider the below code
public static void testSmartLoad() {
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Shelf shelf = (Shelf) session.load(Shelf.class, shelfId);
    Set<Book> allBooks = shelf.getAllBooks();
    System.out.println("Are books there on " + shelf.getCode() + " ? "
                + !allBooks.isEmpty());
        
    System.out.println("Number of books on " + shelf.getCode() + " is "
        + allBooks.size());
    Book searchBook  = new Book();
    searchBook.setId(4);
    System.out.println("Is book there ? " + allBooks.contains(searchBook));

    transaction.commit();
    session.close();
}
The logs indicate the fired SQL queries:
Hibernate: 
    /* load com.collection.smart.Shelf */  
    select
        shelf0_.ID as ID1_0_,
        shelf0_.CODE as CODE1_0_ 
    from
        SHELF shelf0_ 
    where
        shelf0_.ID=?
Hibernate: 
    select
        count(ID) 
    from
        BOOK 
    where
        SHELF_ID =?
Are books there on SH001 ? true
Number of books on SH001 is 2
Hibernate: 
    select
        1 
    from
        BOOK 
    where
        SHELF_ID =? 
        and ID =?
Is book there ? false
As can be seen the isEmpty/ size method both did not result in any load calls. instead simple select calls were executed to get the size of the collection.
Similarly the contains method also did not result in any data loading.
The same rules would apply for a Map (containsKey and get method) and List (get method)
This is a good option to use with very large collections.

6 comments:

  1. This is a nice feature, thanks for posting this.

    ReplyDelete
  2. Thanks for your effort.

    ReplyDelete
  3. yes, great example
    now i can do it by myself
    thanks for sharing mate

    ReplyDelete
  4. Thanks Robin.
    Now i got the concept

    ReplyDelete
  5. But didn't work if you are using HQL to fetch All records

    ReplyDelete
  6. nice article..thanks for posting it..

    ReplyDelete