Search This Blog

Tuesday 15 November 2011

Simplest UniDirectional Association in Hibernate

I decided to implement an unidirectional many-to-one association.
A Shelf contains many books. Each book can exist on only one shelf. No book can exist without being assigned a shelf.
The above would be represented in database by two tables - Shelf and a Book.
The Book would have a foreign-key reference to Shelf. In the Hibernate modelling the same could be represented by an Entity shelf that would have a collection of components (books.) This would however make it difficult to use the books with any other entities.
For example if we decided to give the book to a particular person to read there is no book identifier that could be bound to the person. In more technical terms the object book has valid justifications to have an independent life cycle and hence is represented as an Entity.
The association between the book and the shelf here is represented as a many-to-one 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.unidirectional2">
    <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>
    </class>
</hibernate-mapping> 
The shelf class here has no relation to the book class.The mapping for book is 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.unidirectional2">
    <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">
            <column name="SHELF_ID" not-null="true"></column>
        </many-to-one>
    </class>
</hibernate-mapping>
The book class includes the association to shelf. The not-null constraint here enforces that a book must be present on a shelf (used within the DDL only). The java classes for the entities is as below:
public class Shelf {
    private Integer id;
    private String code;
//getter - setter methods
}
The class for Book includes a reference to the Shelf object:
public class Book {
    private String name;
    private Integer id;
    private Shelf shelf;
//getter - setter methods
}
On start up the logs indicate the executed DDL statements :
2531 [main] DEBUG org.hibernate.tool.hbm2ddl.SchemaExport  - 
    create table BOOK (
        ID integer not null auto_increment,
        Name varchar(50) not null,
        SHELF_ID integer not null,
        primary key (ID)
    )
2578 [main] DEBUG org.hibernate.tool.hbm2ddl.SchemaExport  - 
    create table SHELF (
        ID integer not null auto_increment,
        CODE varchar(50) not null,
        primary key (ID)
    )
2578 [main] DEBUG org.hibernate.tool.hbm2ddl.SchemaExport  - 
    alter table BOOK 
        add index SHELF_FK_1 (SHELF_ID), 
        add constraint SHELF_FK_1 
        foreign key (SHELF_ID) 
        references SHELF (ID)
I tried creating a single shelf with two books
static void create() {
    Shelf shelf1 = new Shelf();
    shelf1.setCode("SH01");
        
    Book book1 = new Book();
    book1.setName("Lord Of The Rings");
    book1.setShelf(shelf1);
    Book book2 = new Book();
    book2.setName("Simply Fly");
    book2.setShelf(shelf1);
        
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    session.save(shelf1);
    session.save(book1);
    session.save(book2);
    t.commit();
    System.out.println("The shelf 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 executed code will fire an sql insert for each of the save call made in the transaction.
2938 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        SHELF
        (CODE) 
    values
        (?)
...
2984 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
...
3000 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
3000 [main] DEBUG org.hibernate.impl.SessionImpl  - after transaction completion
The shelf with code SH01 was created with id 1
Book1 saved with id 1 and Book2 saved with id 2 
It is possible to reduce the number of save calls by using the association to mange cascading operations.(Although this is not a clean code in this example )We can save a shelf by simply saving the book the shelf is associated with it. The change needed would be to introduce cascade settings in the relationship.
<many-to-one name="shelf" class="Shelf" 
         foreign-key="BOOK_FK_1" cascade="save-update">
    <column name="SHELF_ID" not-null="true"></column>
</many-to-one>
The save for the above entities can be modified to below:
Transaction t = session.beginTransaction();
//  session.save(shelf1);
    session.save(book1);
    session.save(book2);
    t.commit();
I first executed the code without commenting the first save (one involving shelf). As I had used cascade update settings for the Book, I had expected some issue to occur if Hibernate tried to save the associated (and already existing shelf record) as a part of the cascade settings. However the session smartly detected that Shelf was a persistent instance and so the save operation wasn't necessary.
6453 [main] DEBUG org.hibernate.event.def.AbstractFlushingEventListener  - flush
ing session
6453 [main] DEBUG org.hibernate.event.def.AbstractFlushingEventListener  - proce
ssing flush-time cascades
6453 [main] DEBUG org.hibernate.engine.Cascade  - processing cascade ACTION_SAVE
_UPDATE for: com.collection.undirectional2.Book
6453 [main] DEBUG org.hibernate.engine.CascadingAction  - cascading to saveOrUpd
ate: com.collection.undirectional2.Shelf
6453 [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener  - persisten
t instance of: com.collection.undirectional2.Shelf
6453 [main] DEBUG org.hibernate.event.def.DefaultSaveOrUpdateEventListener  - ig
noring persistent instance
6453 [main] DEBUG org.hibernate.event.def.DefaultSaveOrUpdateEventListener  - ob
ject already associated with session: [com.collection.undirectional2.Shelf#1]
6453 [main] DEBUG org.hibernate.engine.Cascade  - done processing cascade ACTION
_SAVE_UPDATE for: com.collection.undirectional2.Book
With the commented line, Hibernate will very smartly decide the sequence of SQL operations to execute and generate the same sequence of SQL statements as it did before.
Cascade settings can also be applied for deletion.
<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>
On trying to delete book1
Transaction t = session.beginTransaction();
Book book1 = (Book) session.get(Book.class, 1);        
session.delete(book1);
t.commit();
The logs generated indicating a failure to complete deletion:
2859 [main] ERROR org.hibernate.util.JDBCExceptionReporter  - Cannot delete or u
pdate a parent row: a foreign key constraint fails (`collections`.`book`, CONSTR
AINT `BOOK_FK_1` FOREIGN KEY (`SHELF_ID`) REFERENCES `shelf` (`ID`))
Exception in thread "main"org.hibernate.exception.ConstraintViolationException:
 Could not execute JDBC batch update
Shelf1 is bound to two Books - book1 and book2. Attempts to delete book1 fails as book2  is also placed on Shelf1. Hibernate expects the user to perform this delink manually. The entity book2 needs to be reassigned to a different shelf before Shelf1 can be deleted.
Ideally a shelf must be created first before books must be saved. Similarly no books must be on the shelf when a shelf is to be deleted. For these reasons having the Book entity manage the shelf life-cycle does not make sense.
So I decided to go the other way - let Shelf manage the books that reside on it.  And then I thought why not try and merge the two relations ?

No comments:

Post a Comment