Search This Blog

Sunday 25 December 2011

Using a Map with the formula attribute

In different posts I have worked with different data in collections - with java data type (Strings), components and then with entities. For maps the technique was based on having a value that became the map key and the value was the data as String or a Component or the Entities. This meant that there were two columns - one holding the key and the other representing the value.
However it is also possible to represent a map of Entities without any separate map key. In this case the key column can be the identifier of the map or some suitable value while the value field is filled by the entity. I tried to do the same with the Shelf and Books example. Instead of representing the association as a set of books I instead created a map of books. The java classes are as below:
public class Shelf {
    private Integer id;
    private String code;
    private Map<Integer, Book> books = new HashMap<Integer, Book>();
    
    public void addBook(Book book) {
        if (null != book.getShelf() 
                && this != book.getShelf()) {
            Shelf otherShelf = book.getShelf();
            otherShelf.getBooks().remove(book);
        } else {
            book.setShelf(this);
        }
        books.put(book.getId(), book);
    }
//setter getter methods
}

public class Book {
    private String name;
    private Integer id;
    private Shelf shelf;
    
    @Override
    public boolean equals(Object other) {
        boolean equals = false;
        if (other instanceof Book) {
            Book otherBook = (Book) other;
            equals = otherBook.getName().equals(this.getName());
        }
        return equals;
    }
    
    @Override
    public int hashCode() {        
        return this.getName().hashCode();
    }
//setter getter methods
}

The hbm files for the above are as below:
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.map">
    <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>

        <map name="books" cascade="delete,delete-orphan" inverse="true">
            <key column="SHELF_ID" foreign-key="BOOK_FK_1" />
            <map-key type="integer" formula="ID" />
            <one-to-many class="Book" />
        </map>

    </class>
</hibernate-mapping>
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.map">
    <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 cascade settings did not include save update here.
The reason being the map key is the id field. For an unsaved book the value will be null. The HashMap can have only record with a null key. As a result if there are multiple unsaved books, only one will be saved. So the cascade has been set to manage deletion only.
To avoid the issue we can change the formula attribute to use the book name. The map would then be
<map name="books" cascade="save-update,delete,delete-orphan" inverse="true">
    <key column="SHELF_ID" foreign-key="BOOK_FK_1" />
    <map-key type="string" formula="Name" />
    <one-to-many class="Book" />
</map>
The start up logs indicate the following DDL:
create table BOOK (
        ID integer not null auto_increment,
        Name varchar(50) not null,
        SHELF_ID integer not null,
        primary key (ID)
    )
    create table SHELF (
        ID integer not null auto_increment,
        CODE varchar(50) not null,
        primary key (ID)
    )
    alter table BOOK 
        add index BOOK_FK_1 (SHELF_ID), 
        add constraint BOOK_FK_1 
        foreign key (SHELF_ID) 
        references SHELF (ID)

The code to create a shelf and add a couple of books to it would be as follows:
static void create() {
    Shelf shelf1 = new Shelf();
    shelf1.setCode("SH001");
        
    Book book1 = new Book();
    book1.setName("Lord Of The Rings");
    Book book2 = new Book();
    book2.setName("Simply Fly");
        
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    shelf1.addBook(book1);
    shelf1.addBook(book2);

    session.save(shelf1);
    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());
}
On executing the logs indicate the inserts scripts executed:
2688 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        SHELF
        (CODE) 
    values
        (?)
...
2750 [main] DEBUG org.hibernate.impl.SessionImpl  - after transaction completion
...
2750 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        BOOK
        (Name, SHELF_ID) 
    values
        (?, ?)
...
2813 [main] DEBUG org.hibernate.impl.SessionImpl  - after transaction completion
The Shelf with name SH001 was created with id 1
Book1 saved with id 1 and Book2 saved with id 2
The code to load the shelf and books is as below:
static void testLoad() {
    Session session = sessionFactory.openSession();
    Shelf shelf1 = (Shelf) session.get(Shelf.class, 1);        
    Map<String, Book> books = shelf1.getBooks();
    System.out.println("Shelf " + shelf1.getCode() + " has books " + books.size() );        
    System.out.println("The books are");
    for (String bookId : books.keySet() ) {
        System.out.println("Book Name is " + books.get(bookId).getName());
    }
}
The select query for the collection is slightly different here:
2500 [main] DEBUG org.hibernate.SQL  - 
    select
        shelf0_.ID as ID0_0_,
        shelf0_.CODE as CODE0_0_ 
    from
        SHELF shelf0_ 
    where
        shelf0_.ID=?
...
2671 [main] DEBUG org.hibernate.SQL  - 
    select
        books0_.SHELF_ID as SHELF3_1_,
        books0_.ID as ID1_,
        books0_.Name as formula0_1_,
        books0_.ID as ID1_0_,
        books0_.Name as Name1_0_,
        books0_.SHELF_ID as SHELF3_1_0_ 
    from
        BOOK books0_ 
    where
        books0_.SHELF_ID=?
...
Shelf SH001 has books 2
The books are
Book Name is Lord Of The Rings
Book Name is Simply Fly
I now tried to remove a book from the map. The below code would be expected to remove the book:
static void deleteOrphanElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    Shelf shelf1 = (Shelf) session.get(Shelf.class, 1);
    Book book2 = (Book) session.load(Book.class, 1);
    shelf1.getBooks().remove(book2.getName());//Remove Book with Id 1
    t.commit();        
}

However the remove operation does not make any difference to the Hibernate managed map. It does not even remove the element from the map.
The reason being the "formula" attribute. It makes the column a read-only field. No updates occur to the map when you perform operations on the basis of this column. Instead the below ( horrible!!!) code makes the orphan-delete happen.
static void deleteOrphanElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    //removing an element from the set
    Shelf shelf1 = (Shelf) session.get(Shelf.class, 1);
    Book book2 = (Book) session.load(Book.class, 2);
    shelf1.getBooks().clear();//all books (Book1 and Book2) cleared
    shelf1.getBooks().put(book2.getId(), book2);//book2 placed in the set again
    t.commit();        
}
The logs indicate the delete query fired:
2656 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        BOOK 
    where
        ID=?

1 comment:

  1. Book1 saved with id null and Book2 saved with id2

    ReplyDelete