Load and cache tree structures with Hibernate

Filed under:
I have recently been busy with loading and caching tree structures with Hibernate, and i had a few struggles with achieving this with a minimal amount of database queries and at the same time caching the entire tree structure. This post will explain how i load and cache a quite plain vanilla tree structure. For a history on the issues i came across on my way to this "solution", please have a look at the related posts on the Hibernate forum. First of all, a brief overview of the POJO's and Hibernate mapping files. Node.java
  1. public class Node {
  2. private UUID id = null;
  3. private Calendar updated = null;
  4. private String name = null;
  5. private Node parent = null;
  6. private List children = new java.util.ArrayList();
  7.  
  8. public UUID getId() {}
  9. public void setId(UUID id) {}
  10. public void setUpdated(Calendar updated) {}
  11. public Calendar getUpdated() {}
  12. public String getName() {}
  13. public void setName(String name) {}
  14. public Node getParent() {}
  15. public void setParent(Node parent) {}
  16. public List getChildren() {}
  17. public void setChildren(List children) {}
  18. }
Node.hbm.xml
  1. <hibernate-mapping>
  2. <class name="com.tracker.db.entities.Node" table="keywordnode">
  3. <cache usage="read-write"/>
  4. <id column="id" length="36" name="id" type="com.tracker.db.usertypes.CustomUUID"/>
  5. <version column="updated" name="updated" type="java.util.Calendar" unsaved-value="null"/>
  6. <property column="name" length="200" name="name" type="java.lang.String"/>
  7. <many-to-one cascade="none" class="com.tracker.db.entities. Node" column="parent_id" name="parent" not-null="false"/>
  8. <bag cascade="none" inverse="true" name="children" order-by="name asc">
  9. <cache usage="read-write"/>
  10. <key column="parent_id"/>
  11. <one-to-many class="com.tracker.db.entities.Node"/>
  12. </bag>
  13. </class>
  14. </hibernate-mapping>
ehcache.xml
  1. <ehcache>
  2. <!-- Default entity cache -->
  3. <defaultCache
  4. eternal="false"
  5. timeToIdleSeconds="0"
  6. timeToLiveSeconds="0"
  7. overflowToDisk="false"
  8. memoryStoreEvictionPolicy="LFU"
  9. />
  10.  
  11. <cache
  12. name="com.tracker.db.entities.Keyword"
  13. maxElementsInMemory="1500"
  14. />
  15.  
  16. <cache
  17. name="com.tracker.db.entities.Keyword.children"
  18. maxElementsInMemory="3000"
  19. />
  20.  
  21. <!-- Default query cache -->
  22. <cache
  23. name="org.hibernate.cache.StandardQueryCache"
  24. maxElementsInMemory="2500"
  25. eternal="false"
  26. timeToLiveSeconds="900"
  27. overflowToDisk="false"
  28. />
  29.  
  30. <!-- Default timestamps cache -->
  31. <cache
  32. name="org.hibernate.cache.UpdateTimestampsCache"
  33. maxElementsInMemory="2500"
  34. eternal="true"
  35. overflowToDisk="false"
  36. />
  37. </ehcache>
In order to load the tree structure in as little time as possible, i chose to use an HQL statement which would load all nodes from the database where both the parent property and children collection would be initialised. This was achieved through the following code:
  1. Query q = getSession().createQuery("select distinct k from Node n left join fetch n.children left join fetch n.parent");
  2. q.setCacheable(true);
  3.  
  4. Node rootNode = null;
  5.  
  6. List l = q.list();
  7. for (int i = 0; i < l.size(); i++) {
  8. Node n = (Node) l.get(i);
  9.  
  10. //This will make sure that Hibernate caches the children collection
  11. Hibernate.initialize(n.getChildren());
  12.  
  13. //If the current Node has no parent, regard it as the topmost node in the Node hierarchy.
  14. if (n.getParent() == null) {
  15. rootNode = n;
  16. }
  17. }
  18.  
  19. return rootNode;
That's it. Whenever i load the tree structure, all Nodes will be fully initialised. Even better, whenever i need to get hands on the tree structure for the second time, the entire tree structure is loaded from the cache. This, of course, is only true until i add, remove or edit nodes in the tree. Whenever i do that, the entire tree needs to be loaded from the database once more.