Search This Blog

Monday 28 November 2011

Collections and The Inverse Attribute

In a previous post I successfully created a bidirectional relationship between Shelf and Books. We also saw that hibernate treats these as two individual unidirectional relationships in the application.
On saving the Shelf with two Books, we observed that -
  • A single insert was fired to create the Shelf record
  • Two inserts were fired to create the Book records
  • Two updates were fired to associate the Books with the Shelf.
As seen before these update scripts were redundant as the insert scripts ensured that the  foreign key values were updated.As we found that Hibernate is not able to detect that the same column is being updated via two relationships, we need to manually instruct Hibernate that the books collection in  Shelf is same as Shelf property in Book. This is done with the inverse attribute.
<set name="books" cascade="save-update,delete" inverse ="true">
    <key column="SHELF_ID" foreign-key="BOOK_FK_1" />
    <one-to-many class="Book" />
</set>
By setting inverse = "true", we have informed Hibernate to be concerned with changes made to Book via the Shelf property only.
Thus calling shelf.getBooks().add(Book) will not cause a Book object to be persisted in the system. the book will be made persistent via the call to code book.setShelf(shelf)
I executed the below code to save the shelf and books.
static void create() {
    Shelf shelf1 = new Shelf();
    shelf1.setCode("SH01");
    
    Book book1 = new Book();
    book1.setName("Lord Of The Rings");
    
    Book book2 = new Book();
    book2.setName("Simply Fly");

    shelf1.addBook(book1);
    shelf1.addBook(book2);
    
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    session.save(shelf1);
//    session.save(book1); //CASCADE settings take care of the same
//    session.save(book2);
    t.commit();
    System.out.println("The Chocolate Lover with name " + shelf1.getCode()
        + " was created with id " + shelf1.getId());
    System.out.println("Book1 saved with id " + book1.getId()
        + " and Book2 saved with id " + book2.getId());
} 
The queries generated are as below:
Hibernate: 
    insert 
    into
        SHELF
        (CODE) 
    values
        (?)
Hibernate: 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
The Chocolate Lover with name SH01 was created with id 1
Book1 saved with id 2 and Book2 saved with id 1
The output indicates that:
  1. When the session tried to save shelf, its cascade settings kicked into picture.
  2. Saving the shelf thus resulted in all associated books getting saved too.
  3. As inverse was set to true, the below line in shelf.addBook() method caused the association between shelf and book objects.
    public void addBook(Book book) {
        if (null != book.getShelf()) {
            Shelf otherShelf = book.getShelf();
            otherShelf.getBooks().remove(book);
        } else {
            book.setShelf(this);
        }
        books.add(book);
    }
    
  4. If in the above code, I were to comment the line
    //book.setShelf(this);
    
    The call to save shelf would now save the shelf object. As books have been added to the shelf collection, it will try to save the books. However because of inverse being true and above line being commented, It will try to create books with null Shelf attribute.
  5. Thus while cascade decides what objects are to be saved, inverse helps decide which side of relation must decide the value of the foreign key. With inverse = true, the child's setParent property decides the value of the foreign key (in this case book.setShelf()). Instead if inverse = false, then the action of adding a child to a parent's collection decides the value of the foreign key (i.e. shelf.getBooks().add())
All this intricacies will be known to the person creating the mappings and the entity class. Hence it now makes perfect sense to include an addBooks() method in Shelf class which hides the inner details from the other users.
NOTE: Just to add (what is actually implied from the above discussion) the inverse property comes into picture only for bidirectional relations and nowhere else. A collection is needed to apply the inverse attribute. If the many to one mapping is not present on the other side (as is the case for unidirectional) then inverse=true cannot work. And inverse=false will allow the code to work fine, but the extra queries will still be there. Basically inverse=false is redundant in the above case.
There is also the (improbable I agree) case wherein you would want the updates to be reflected via the collection end. In this case the mapping would have to be modified so as to reflect the new changes:
Shelf.hbm.xml
<set name="books" cascade="save-update,delete" inverse ="false">
    <key column="SHELF_ID" foreign-key="BOOK_FK_1" not-null ="true"/>
    <one-to-many class="Book" />
</set>
Book.hbm.xml
<many-to-one name="shelf" class="Shelf" foreign-key="BOOK_FK_1"
    update ="false" insert ="false">
    <column name="SHELF_ID"></column>
</many-to-one>
Now a call to add Book to the collection in the shelf class, will create the Book entity. However the extra update scripts will be fired again.

No comments:

Post a Comment