I've got an example where I have the following object hierarchy:
Order 1-* Bag 1-* Item
I have a lot of these objects, so I want to cache them in a transactional manner (perhaps using TreeCache) to minimize the number of database queries. However, it looks like Hibernate is not caching the object collections correctly.
When I create an Order, and then a Bag (adding it to the Order) and then an Item (adding it to the Bag), the object cache's Order, Bag, and Item are correctly populated with these new objects. However, the collection cache's Order.bags and Bag.items are not. It looks like they are populated the next time these collections are loaded, but even that is not consistent.
I put together a test app which isolates just these objects and runs a JUnit test that shows the problem with the caching. It can be downloaded
here.
Output from JUnit test (show_sql=true):
Code:
====== step 1 ======
Hibernate: select nextval ('order_sequence')
Hibernate: select nextval ('bag_sequence')
Hibernate: select nextval ('item_sequence')
Hibernate: insert into ce_order (id) values (?)
Hibernate: insert into ce_bag (order_id, index, id) values (?, ?, ?)
Hibernate: insert into ce_item (bag_id, index, id) values (?, ?, ?)
Hibernate: update ce_bag set order_id=?, index=? where id=?
Hibernate: update ce_item set bag_id=?, index=? where id=?
model.Bag.items ** {}
model.Item ** {1={index=null, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=false}}
model.Bag ** {1={index=null, _subclass=model.Bag, order=1, items=1, _lazyPropertiesUnfetched=false}}
model.Order ** {1={_subclass=model.Order, bags=1, _lazyPropertiesUnfetched=false}}
model.Order.bags ** {}
====== step 2 ======
Hibernate: select bags0_.order_id as order2_1_, bags0_.id as id1_, bags0_.index as index1_, bags0_.id as id0_, bags0_.order_id as order2_1_0_, bags0_.index as index1_0_ from ce_bag bags0_ where bags0_.order_id=?
Hibernate: select items0_.bag_id as bag2_1_, items0_.id as id1_, items0_.index as index1_, items0_.id as id0_, items0_.bag_id as bag2_2_0_, items0_.index as index2_0_ from ce_item items0_ where items0_.bag_id=?
Hibernate: select nextval ('item_sequence')
Hibernate: insert into ce_item (bag_id, index, id) values (?, ?, ?)
Hibernate: update ce_item set bag_id=?, index=? where id=?
model.Bag.items ** {}
model.Item ** {2={index=null, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=false}, 1={index=0, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=true}}
model.Bag ** {1={index=0, _subclass=model.Bag, order=1, items=1, _lazyPropertiesUnfetched=true}}
model.Order ** {1={_subclass=model.Order, bags=1, _lazyPropertiesUnfetched=false}}
model.Order.bags ** {1=[1]}
====== step 3 ======
Hibernate: select items0_.bag_id as bag2_1_, items0_.id as id1_, items0_.index as index1_, items0_.id as id0_, items0_.bag_id as bag2_2_0_, items0_.index as index2_0_ from ce_item items0_ where items0_.bag_id=?
Hibernate: select nextval ('item_sequence')
Hibernate: insert into ce_item (bag_id, index, id) values (?, ?, ?)
Hibernate: update ce_item set bag_id=?, index=? where id=?
model.Bag.items ** {}
model.Item ** {2={index=1, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=true}, 1={index=0, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=true}, 3={index=null, _subclass=model.Item, bag=1, _lazyPropertiesUnfetched=false}}
model.Bag ** {1={index=0, _subclass=model.Bag, order=1, items=1, _lazyPropertiesUnfetched=true}}
model.Order ** {1={_subclass=model.Order, bags=1, _lazyPropertiesUnfetched=false}}
model.Order.bags ** {1=[1]}
Notice how the first thing it does in step 2 is go to the database for the Bag and Item objects it just stored. These objects are actually in the cache (as seen in the cache dump from step 1), but the collection caches Order.bags and Bag.items are not correctly set.
XDoclet generated Mapping documents:Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping
>
<class
name="model.Order"
table="ce_order"
>
<cache usage="transactional" />
<id
name="id"
column="id"
type="java.lang.Long"
>
<generator class="sequence">
<param name="sequence">order_sequence</param>
<!--
To add non XDoclet generator parameters, create a file named
hibernate-generator-params-Order.xml
containing the additional parameters and place it in your merge dir.
-->
</generator>
</id>
<list
name="bags"
lazy="false"
cascade="save-update"
>
<cache
usage="transactional"
/>
<key
column="order_id"
>
</key>
<index
column="index"
/>
<one-to-many
class="model.Bag"
/>
</list>
<!--
To add non XDoclet property mappings, create a file named
hibernate-properties-Order.xml
containing the additional properties and place it in your merge dir.
-->
</class>
</hibernate-mapping>
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping
>
<class
name="model.Bag"
table="ce_bag"
>
<cache usage="transactional" />
<id
name="id"
column="id"
type="java.lang.Long"
>
<generator class="sequence">
<param name="sequence">bag_sequence</param>
<!--
To add non XDoclet generator parameters, create a file named
hibernate-generator-params-Bag.xml
containing the additional parameters and place it in your merge dir.
-->
</generator>
</id>
<many-to-one
name="order"
class="model.Order"
cascade="none"
outer-join="auto"
update="true"
insert="true"
column="order_id"
not-null="true"
/>
<property
name="index"
type="java.lang.Integer"
update="true"
insert="true"
column="index"
/>
<list
name="items"
lazy="false"
cascade="none"
>
<cache
usage="transactional"
/>
<key
column="bag_id"
>
</key>
<index
column="index"
/>
<one-to-many
class="model.Item"
/>
</list>
<!--
To add non XDoclet property mappings, create a file named
hibernate-properties-Bag.xml
containing the additional properties and place it in your merge dir.
-->
</class>
</hibernate-mapping>
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping
>
<class
name="model.Item"
table="ce_item"
mutable="false"
>
<cache usage="transactional" />
<id
name="id"
column="id"
type="java.lang.Long"
>
<generator class="sequence">
<param name="sequence">item_sequence</param>
<!--
To add non XDoclet generator parameters, create a file named
hibernate-generator-params-Item.xml
containing the additional parameters and place it in your merge dir.
-->
</generator>
</id>
<many-to-one
name="bag"
class="model.Bag"
cascade="none"
outer-join="auto"
update="true"
insert="true"
column="bag_id"
not-null="true"
/>
<property
name="index"
type="java.lang.Integer"
update="true"
insert="true"
column="index"
/>
<!--
To add non XDoclet property mappings, create a file named
hibernate-properties-Item.xml
containing the additional properties and place it in your merge dir.
-->
</class>
</hibernate-mapping>
JUnit test demonstrating problemCode:
package test;
import model.Order;
import model.Bag;
import model.Item;
import java.util.Map;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.stat.Statistics;
/**
* 16:36 < unlord> anyway, the test should create an order, then create a bag and add it to the order, then create an Item and add it, close the tx
* 16:36 < unlord> then open another tx, try to retrieve the Order, then get the Bag off the order, and add a new Item
* 16:37 < unlord> close the tx, and then do the exact same thing a 3rd time
* 16:37 < unlord> on the second Tx, it should find the Order just fine, but will go to the DB for the bag and item
* 16:37 < unlord> on the third Tx, it should find the Bag just fine, but go to the db for the second Item
* 16:39 < unlord> with a transactional cache, there is no way the cache can be out of sync with the DB, so it should never need to go to the db for stuff it knows
*/
public class CacheTest extends BaseTestCase {
protected Long orderId;
protected Long bagId;
protected void printCache() {
Statistics st=sf.getStatistics();
String[] regs=st.getSecondLevelCacheRegionNames();
for (int i=0;i<regs.length;i++) {
String r=regs[i];
Map cacheEntries=st.getSecondLevelCacheStatistics(r).getEntries();
System.out.println(r+" ** "+cacheEntries);
}
}
protected void step1() {
Session session=sf.openSession();
Transaction tx=session.beginTransaction();
// create an order
Order o=new Order();
session.save(o);
// create a bag
Bag b=new Bag();
b.setOrder(o);
session.save(b);
o.getBags().add(o);
// create an item
Item i=new Item();
i.setBag(b);
session.save(i);
b.getItems().add(i);
// save it all
session.update(o);
orderId=o.getId();
bagId=b.getId();
// close the tx
tx.commit();
session.close();
}
protected void step2() {
Session session=sf.openSession();
Transaction tx=session.beginTransaction();
// load the order
Order o=(Order)session.get(Order.class,orderId);
// retrieve the bag
Bag b=(Bag)o.getBags().get(0);
// create an item
Item i=new Item();
i.setBag(b);
session.save(i);
b.getItems().add(i);
// save the item
session.update(o);
tx.commit();
session.close();
}
public void testCache() {
System.out.println("====== step 1 ======");
step1();
printCache();
System.out.println("====== step 2 ======");
step2();
printCache();
System.out.println("====== step 3 ======");
step2();
printCache();
}
};
Tested against Hibernate versions
2.1.8
3.0.5
3.1.2
Name and version of the database you are using:
PostgreSQL 8.0