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:
The hbm files for the above are as below:
Shelf.hbm.xml
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
The code to create a shelf and add a couple of books to it would be as follows:
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.
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 2The 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 FlyI 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=?
Book1 saved with id null and Book2 saved with id2
ReplyDelete