The ADF Faces tree is potentially a very rich component that can add value to many user interfaces. Data sets frequently are hierarchically organized and the tree allows visual representation of that structure. The user can quickly and intuitively navigate through the data to get to the element he or she has an interest in, select it and start doing work in the context of the selected tree node.
Using the ADF Faces Tree component together with the ADF Tree Data Binding – that allows configuration of the hierarchical data structure by nesting various ViewObjects – is pretty straightforward. However, we have run into at times quite severe performance issues. It seems that the Tree Data Binding executes a query for every node, to collect the children of that node. That means that even for a relatively small tree, as soon as we start expanding nodes, the number of queries executed increases rapidly.
We want to leverage the knowledge we have of our data model as well as the strong hierarchical query functions in the Oracle RDBMS, rather than having the ADF middle tier traverse the tree, executing queries with every node it encounters. As a bonus we also fix another issue with the Tree Data Binding: it does not indicate that a node is a leaf-node – i.e. has no children. All nodes are presented as containers (that is: with a plus sign suggesting the node has children). While that limitation is a a way to save on performance, it can be quite annoying to the end user.
In this article I will describe how we can easily populate the Tree Component from our own middle tier POJO that in turn is based on a Read Only ViewObject that uses a Hierarchical SQL query to retrieve the tree data from the database in a single round trip.
The example as always is a simple one: Employees in Departments. At the end of the article, the JDeveloper project is available for download.
The data structure in the database is probably familiar: my root-nodes are the records in the DEPT table. The highest level Employee Nodes are EMP records with JOB equals MANAGER. The lower level Employees connect to their parents via the Foreign Key reference from their MGR column to the EMPNO column in the same EMP table.
It is fairly easy to come up with a single SQL Query that returns the entire tree-structure in one go:
with nodes as ( select deptno id , 'DEPT' node_type , -1 parent_id , dname node_label from dept union all select empno id , 'EMP' node_type , deptno parent_id , ename node_label from emp where job ='MANAGER' union all select empno id , 'EMP' node_type , mgr parent_id , ename node_label from emp where job <>'MANAGER' ) select node_label , level , id , parent_id , node_type from nodes connect by prior id = parent_id start with parent_id =-1
The result of this query:
After creating a new JDeveloper Application – ADF BC and JSF Technology Template – I create a read only ViewObject HrmTreeView, based on this query. I also create a new Application Module – HrmServiceAppModule – and add a ViewObject usage for HrmTreeView to the application module.
Next I create a new JSF page. I drag the HrmTreeView collection from the Data Control Palette to the JSF page and drop it as a read only table. And now is the time to create a TreeModel object, based on the HrmTreeView Collection.
In a previous article – Building ADF Faces Tree based on POJOs (without using the ADF Tree Data Binding) – I explained in detail how to base an ADF Faces Tree on a POJO that implements the TreeModel interface. We will now take it one step further, by populating that TreeModel not from static data defined in the Class definition, but from the Iterator for the HrmTreeView collection.
I have created a Java Class, HierarchicalQueryTreeModel, that takes the name of an ADF Iterator Binding and uses it to populate the TreeModel. This TreeModel is then injected into the TreeHandler class, that we have seen introduced in the previous article mentioned above. The faces-config definitions of the required beans are as follows:
<managed-bean> <managed-bean-name>HrmTreeHandler</managed-bean-name> <managed-bean-class>nl.amis.adffaces.tree.TreeHandler</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> <managed-property> <property-name>treemodel</property-name> <value>#{HrmTreeModel.treemodel}</value> </managed-property> </managed-bean> <managed-bean> <managed-bean-name>HrmTreeModel</managed-bean-name> <managed-bean-class>nl.amis.adffaces.tree.HierarchicalQueryTreeModel</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> <managed-property> <property-name>hierarchicalIteratorName</property-name> <value>HrmTreeViewIterator</value> </managed-property> </managed-bean>
we see that the HrmTreeModel bean – that is the instantiation of the TreeModel based on an Iterator with a predefined structure, more on that later – gets injected into the TreeHandler. The TreeHandler provides the foundation for the Tree component, as can be seen here:
<af:tree value="#{HrmTreeHandler.treemodel}" var="node" focusRowKey="#{HrmTreeHandler.focusRowKey}" varStatus="nodeStatus" binding="#{HrmTreeHandler.jsfTree}">
The Tree Component has its value attribute bound to the TreeHandler. It is also bound itself to the tree property in the TreeHandler.
The HrmTreeViewIterator is the iterator that was created in the PageDefinition when I dragged the HrmTreeView collection as read only table to the JSF page. It is used in the HierarchicalQueryTreeModel class to get the data from the underlying ViewObject and database. This class is implemented like this:
package nl.amis.adffaces.tree; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.faces.application.Application; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import oracle.adf.model.binding.DCIteratorBinding; import oracle.adf.view.faces.model.TreeModel; import oracle.binding.BindingContainer; import oracle.jbo.Row; import oracle.jbo.RowSetIterator; import oracle.jbo.domain.Number; public class HierarchicalQueryTreeModel { private String hierarchicalIteratorName; private String rootLabel; private TreeModel treemodel; public HierarchicalQueryTreeModel() { } private TreeNode findParent(List<TreeNode> nodes, int level) { // get last of the rootnodes TreeNode tn = nodes.get(nodes.size() - 1); // the parent for this node will be one level higher than the parameter level; // so if level = 2, than the parent will be on level 1 (rootlevel); if the level is 3, // the parent will be on level 2; the parent is the last node at the parentlevel for (int i = level; i > 2; i--) { // get lastchild of current node tn = (TreeNode)tn.getChildren().toArray()[tn.getChildCount() - 1]; } return tn; } private TreeModel initializeTreeModel() { // create the list of root nodes: List nodes = new ArrayList(); // TreeNode root = new TreeNode(getRootLabel(), "root"); DCIteratorBinding hierarchicalIterator = (DCIteratorBinding)getBindings().get(hierarchicalIteratorName); RowSetIterator hierarchicalRSI = hierarchicalIterator.getRowSetIterator(); Row currentNode = hierarchicalRSI.first(); boolean firstNode = true; do { // if the level of a node is larger than the previous one, it is a child if (!firstNode) currentNode = hierarchicalRSI.next(); else firstNode = false; TreeNode treenode = new TreeNode((String)currentNode.getAttribute("NodeLabel"), (String)currentNode.getAttribute("NodeType")); treenode.setLevel(((Number)currentNode.getAttribute("Level")).intValue()); Map attributes = new HashMap(); // copy all attributes from the Row to the Node // note: the Row will only be available as long as we are on a page // that has the iterator instantiated (which is basically only right at the // time of instantiating the TreeModel for (int i = 0; i < currentNode.getAttributeCount(); i++) { attributes.put(currentNode.getAttributeNames()[i], currentNode.getAttribute(i)); } treenode.setAttributes(attributes); if (treenode.getLevel() == 1) { nodes.add(treenode); } else { findParent(nodes, treenode.getLevel()) .getChildren().add(treenode); } } while (hierarchicalRSI.hasNext()); // Master return new SpecialTreeModel(nodes, "children"); } public BindingContainer getBindings() { FacesContext fc = FacesContext.getCurrentInstance(); Application app = fc.getApplication(); ValueBinding vb = app.createValueBinding("#{bindings}"); return (BindingContainer)vb.getValue(fc); } public void setHierarchicalIteratorName(String hierarchicalIteratorName) { this.hierarchicalIteratorName = hierarchicalIteratorName; } public String getHierarchicalIteratorName() { return hierarchicalIteratorName; } public void setRootLabel(String rootLabel) { this.rootLabel = rootLabel; } public String getRootLabel() { return rootLabel; } public TreeModel getTreemodel() { if (treemodel == null) { treemodel = initializeTreeModel(); } return treemodel; } }
Now we have all the pieces, under the condition that the ViewObject publishes that least the following Attributes: Level, NodeLabel, NodeType. Note: all other attributes you may define on the ViewObject are available on the TreeNode object using node.attributes[‘nameOfAttribute’].
At the present, the TreeModel is instantiated only once and will be static from there on. It is quite simple to add little more logic to the TreeModel to allow the user to explicitly refresh from the database or to perform such a refresh under certain conditions. It will still a single roundtrip for the entire tree. Only for much larger trees (many thousands of nodes or more will this approach become inferior to the ADF Tree Data Binding).
Adding an icon for each type of node is easy – as per the suggestion by Steve Muench. The result looks like this:
The only change required is the node definition in the tree:
<f:facet name="nodeStamp"> <af:panelGroup> <af:objectImage source="#{node.nodeType=='DEPT'?'/dept.png':(node.nodeType=='EMP'?'/emp.png':'/manager.png')}" shortDesc="#{node.nodeType}"/> <af:commandLink text="#{node.description}"> <af:setActionListener from="#{ObjectTreeHandler.jsfTree.rowKey}" to="#{ObjectTreeHandler.focusRowKey}"/> <af:setActionListener from="#{node}" to="#{ObjectTreeHandler.selectedNode}"/> <af:resetActionListener/> </af:commandLink> </af:panelGroup> </f:facet>
Well, I also changed the node type for the manager nodes from EMP to MANAGER.
Another Example
I can easily create another query, one that returns from a the current USER’s schema all objects with for Tables also the Column and Index details. The query looks like this:
with nodes as ( select distinct object_type id , 'TYPE' node_type , 'TYPE' parent_id , object_type node_label from user_objects where object_type in ('TABLE','VIEW','PACKAGE','PROCEDURE','FUNCTION') union all select object_name id , 'OBJECT' node_type , object_type parent_id , object_name node_label from user_objects where object_type in ('TABLE','VIEW','PACKAGE','PROCEDURE','FUNCTION') union all select table_name||'.'||column_name id , 'COLUMN' node_type , table_name parent_id , column_name node_label from user_tab_columns union all select INDex_name id , 'INDEX' node_type , table_name parent_id , index_name||' '||index_type node_label from user_indexes ) select node_label , level , id , parent_id , node_type from nodes connect by prior id = parent_id start with parent_id = 'TYPE' order siblings by node_label
The tree now is presented like this:
Resources
Download the JDeveloper 10.1.3.2 Project: DisconnectTreeDemo.zip .
Great post. Â Worked a treat on my 1000 node ragged hierarchy. Â I tried to programmatically set the initial disclosure level using the ADF Code Corner doc linked below but it didn’t work. Â Do you happen to know if this approach is incompatible with the POJO tree model for some reason?
http://www.oracle.com/technetwork/developer-tools/adf/learnmore/78-man-expanding-trees-treetables-354775.pdf
is it possible to refresh adf tree with new values on button click
Good work Lucas Jellema
Its really useful.
Do you have a version of this example for jdeveloper 11 g.
Hi Lucas,
can you send me an email, I was wondering the conditions of use of your example.
Thanks, Les.
Hi,
I am working on Adf Tree Component using Page-definitions.
I am able to create the Tree structure but the issue are..
the tree have any icon ,just a blue triangle,can’t display folds icon as adf demo shows
why?
Hi,
please can you send us the table shema it is not iclude in the zip
Thanks
You mentioned “It is quite simple to add little more logic to the TreeModel to allow the user to explicitly refresh from the database or to perform such a refresh under certain conditions.” How do you suppose is the best way to accomplish this?
I am using Oracle Apex, but having trouble with trees – can I plug it in to Oracle Apex? Has anyone already done it?
Wilfred, it’s more of an obligation I feel to share my findings…. Fun stuff like this can not be kept a secret now can they? Congratulations again on the two shiny awards you won this conference by the way! Not just the tallest, you’re the greatest as well! Lucas
Steve,
I have added the change you suggested, See the updated text and image in the article.
regards, Lucas
Cool. Any way to tweak the example to provide different node icon based on the node type, like a building icon for the “DEPT” node type and a “little person head” icon for an “EMP” type?
How addicted to blogging can one be? You must have uploaded this as soon as you touched down in Amsterdam.
Nevertheless a very interesting post.