Search This Blog

Thursday 24 November 2011

Bi Directional Associations

In earlier posts I created associations between Parent and Entity from both sides in an unidirectional manner. Either I created a reference to parent from within the child element or I added a set of children inside parent.
Here I shall use a bidirectional relationship.
The java entities would be as below:
Shelf.java
public class Shelf {
    private Integer id;
    private String code;
    private Set<Book> books = new HashSet<Book>();
        //setter - getter methods

    public void addBook(Book book) {
        if (null != book.getShelf()) {
            Shelf otherShelf = book.getShelf();
            otherShelf.getBooks().remove(book);        
            }
        book.setShelf(this);
        books.add(book);        
    }
}
As seen, the code for addBook() has been enhanced to reflect the bi-directional association.
Book.java
public class Book {
    private String name;
    private Integer id;
    private Shelf shelf;
//setter - getter methods
}
I merged the relations from the two unidirectional examples in the bidirectional mapping below. The mapping files for the two is as below:
<?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.bidirectional">
    <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="books" cascade="save-update,delete">
            <key column="SHELF_ID" foreign-key="BOOK_FK_1" not-null="true"/>
            <one-to-many class="Book" /> 
        </set>
    </class>
</hibernate-mapping> 

<?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.bidirectional">
    <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_FK_1"
            cascade="save-update,delete">
            <column name="SHELF_ID" not-null="true"></column>
        </many-to-one>
    </class>
</hibernate-mapping>
I have simply copied my two unidirectional association from the earlier posts and used as is.
The Shelf entity has a collection of Books. Each Book includes a reference to a Shelf Element. Additional cascade settings have been added to enable deletion/updating of books through updates to the Shelf object.
On start-up the application logs are as below:
1063 [main] DEBUG org.hibernate.cfg.Configuration  - resolving reference to clas
s: com.collection.bidirectional.Shelf
Exception in thread "main" org.hibernate.MappingException: Repeated column in ma
pping for entity: com.collection.bidirectional.Book column: SHELF_ID (should be 
mapped with insert="false" update="false")
    at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass
.java:652)
    at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(Persist
entClass.java:674)
The problem is that Hibernate and JPA are inherently unidirectional. For Hibernate the relation between Shelf and Book is different to the relation between Book and Shelf. (In true bidirectional relationship setting book.setShelf(Shelf) should have automatically resulted in Shelf.getBooks.add(book). But this is not the case. We need to write code manually. See the addBook() method )
The advantage we have is Hibernate does not interfere in the actual code and manage the relations between entities. This ensures a loose coupling between our entities and the Hibernate framework thus allowing that the classes can be used elsewhere.
Coming back to the issue at hand: we have mapped the foreign key BOOK_FK_1 as a part of two separate (unidirectional) relationships.To add a book to shelf we execute two lines of code:
book.setShelf(shelf);
shelf.getBooks().add(book);
In this situation Hibernate is aware of two changes made to the same database column (BOOK.SHELF_ID). As a result it throws the column duplication error. This can be fixed by removing the "not-null" constraint from one of the mapping.
<set name="books" cascade="save-update,delete" >
      <key column="SHELF_ID" foreign-key="BOOK_FK_1" />
      <one-to-many class="Book" /> 
</set>
The code now works fine.On trying to create a shelf and books just like the previous posts,
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 logs indicate the following SQL statements were fired
2594 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        SHELF
        (CODE) 
    values
        (?)
...
2625 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
...
2641 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
...
3266 [main] DEBUG org.hibernate.SQL  - 
    update
        BOOK 
    set
        SHELF_ID=? 
    where
        ID=?
...
3281 [main] DEBUG org.hibernate.SQL  - 
    update
        BOOK 
    set
        SHELF_ID=? 
    where
        ID=?
The Chocolate Lover with name SH01 was created with id 1
Book1 saved with id 1 and Book2 saved with id 2
As seen additional (and redundant) update queries are fired just as before. The next step would be to fix the same. The same queries were observed if I depended on Books cascade behavior to save the relation.
Transaction t = session.beginTransaction();
//session.save(shelf1);//CASCADE settings take care of the same
session.save(book1); 
session.save(book2);
t.commit();
How does cascade work ?
When you save an object everything that the object went and associated with itself gets saved.
For example
Transaction t = session.beginTransaction();  
//shelf1.getBooks().add(book1); This was needed
//shelf1.getBooks().add(book2);
book1.setShelf(shelf1);
book2.setShelf(shelf1);
session.save(shelf1);//CASCADE settings take care of the same ? ?
//session.save(book1); 
//session.save(book2);
t.commit();
On saving shelf only objects that have been associated via shelf will be saved. Here the books have not been added to shelf. Hence there is no impact of the cascade save setting and no insert book query will be fired. Only an insert shelf query will be executed.

No comments:

Post a Comment