Advanced Lazy Loading using Oracle APEX Tree Region

Jack DroletJack Drolet
6 min read

Problem with the Tree Region

When using a tree in Oracle APEX it can be easy to run into performance problems that don’t exist with other regions displaying the same data. This is because the Tree region will always load every single row returned by the Region Source query, whereas other regions such as Interactive Report or Interactive Grid will only load a page-worth of data and will retrieve the data when the page is changed. This can cause the page to take several seconds to load due to only the tree region loading every single row of data.

We can attempt to address the issue by using the built-in Lazy Loaded flag to allow the page to render before the tree. However, the tree will still load every single node in the tree and take several seconds. This is also not ideal if the tree is central to that page’s utility and there a lot of values in the tree.

What I am going to describe here is a way to only load the root node at page load time, and to dynamically fetch the child nodes of a node only when that node is expanded.

What to do

Below I will use the EMP table as an example on how to set up lazy loading. The users have asked for a tree that shows the employee hierarchy.

Tree Query

In the Tree Region Source, you want to only retrieve the root node of your tree.

Key columns are:

  • VAL

    • The VAL column will be how we pass parameters to the AJAX callback. In this example we will pass the primary key of the table (empno) along with a checksum for security.
  • LVL

    • Will be set to 1 to designate the root as the root
  • LEAF_STATUS

    • Set to 1. Will mark this node as a non-leaf.
select ename as display
     , 'fa-user ' as css_class
     , 'javascript:alert("' || ename || '");' as link
     , empno ||':'|| APEX_UTIL.get_hash(apex_t_varchar2(empno, 'PXXX_EMP_TREE')) as val
     , mgr as parent_id
     , 1 lvl -- makes it root
     , 1 leaf_status -- tells apex that this node is not a leaf
  from emp
 where mgr is null -- we only get the top level in this query

Tree Region Attributes

Make sure that the following Tree Region Attributes are set.

  • Node Value Column (VAL)

    • Will place this column value into the node.id that we will access when we expand the node. (doc)
  • Hierarchy (Not Computed)

    • Prevents APEX from attempting to build the tree based of the SQL source.
  • Node Status Column (LEAF_STATUS)

  • Hierarchy Level Column (LVL)

TreeNodeAdapter

In order to change how the TreeView widget retrieves child nodes, the TreeNodeAdapter must be modified.

childCount function

The childCount function returns the number of children a node has. This is used by APEX when loading the root node. The fetchChildNodes function only runs if childCount returns null. The childCount function that comes with $.apex.treeView.makeDefaultNodeAdapter never returns null so we need to override it.

adapter.childCount = function(pNode) {
    if(pNode.children && pNode.children != null) {
       return pNode.children.length;
    }
    else
    {
       return null;
    }
 }

fetchChildNodes function

This is the main function that will retrieve the children of a node. Notice how we use the pNode.id to get our parameters to the AJAX callback. We add the dummy node to childNode.children so the toggle that allows the parent to be expanded will be shown.

adapter.fetchChildNodes = function(pNode, pCallback) {
    var nodeInfo = pNode.id.split(":");
    var empno = nodeInfo[0];
    var checksum = nodeInfo[1];

    apex.server.process( // ajax to get children of a node
       "GET_TREE_CHILDREN"
     , {
          x01: empno
        , x02: checksum 
     }
     , {
          success: function(data){
             var children = data.children;
             pNode.children = [];

             for(let i = 0; i < children.length; i++) {
                var childJson = children[i];

                var childNode = {
                   id: childJson.id
                 , label: childJson.label
                 , icon: childJson.icon
                 , link: childJson.link
                }

                childNode.children = [{
                   label: "Fetching infos"
                 , icon: "fa fa-refresh fa-anim-spin"
                }] // placeholder so that we have the option to expand

                pNode.children.push(childNode);
             }

             pCallback(children.length); // return result to treeview widget
          },
          error: function( jqXHR, textStatus, errorThrown ) {
             pCallback(false);
          }
       }
    ); 
 }

setExpanded function

The APEX Tree region never calls fetchChildNodes again after the initial fetch of the root’s children. Therefore, we also need to call fetchChildNodes when we expand a node. A custom property is added to the node to flag if that parent has been loaded before or not to prevent unnecessary loading.

adapter.setExpanded = function(pTreeId, pNode, pExpanded) {
    if(pExpanded && pNode.lazyLoaded != true) { // we fetch async on expansion and it hasnt been loaded yet
       adapter.fetchChildNodes(pNode, function(pData){
          // get the JQuery object reference of the node for TreeView function calls
          var newNode = apex.region(options.regionStaticId).widget().treeView("find", {
             depth: -1
          , match: function(n) {return n.id == pNode.id;}
          });

          pNode.lazyLoaded = true; // flag to only load data once

          // we have to refresh to see the new children nodes
          apex.region(options.regionStaticId).widget().treeView("refresh", newNode);

          newNode = apex.region(options.regionStaticId).widget().treeView("find", {
             depth: -1
          , match: function(n) {return n.id == pNode.id;}
          });

          // Refresh causes the node to collapse so we expand the node again
          // have to find node again because refreshes changes the jquery object
          apex.region(options.regionStaticId).widget().treeView("expand", newNode); 
       });
    }
 }

AJAX Callback

This AJAX callback essentially becomes the true data source for the tree beyond the root node as we saw for the Tree region source. Note that in this SQL query the VAL column needs to be present and follow the same syntax as the Tree region source so that we can continue to fetch the children of the children.

declare
   l_empno                 apex_application.g_x01%type := apex_application.g_x01;
   l_checksum              apex_application.g_x02%type := apex_application.g_x02;
   l_calculated_checksum   varchar2(4000);

   l_node_json             JSON_OBJECT_T;
begin
   l_calculated_checksum := APEX_UTIL.get_hash(apex_t_varchar2(l_empno, 'PXXX_EMP_TREE'));

   if l_calculated_checksum != l_checksum then -- security check
      return;
   end if;

   APEX_JSON.open_object();
   APEX_JSON.open_array('children');

   for node_child in (
      select ename as display
           , 'fa-user' as icon_class
           , 'javascript:alert("'||ename||'");' as link
           , empno ||':'|| APEX_UTIL.get_hash(apex_t_varchar2(empno, 'PXXX_EMP_TREE')) as val
        from emp e1
       where mgr = l_empno
   ) loop
      APEX_JSON.open_object();
      APEX_JSON.write('label', node_child.display);
      APEX_JSON.write('icon', node_child.icon_class);
      APEX_JSON.write('link', node_child.link);
      APEX_JSON.write('id', node_child.val);
      APEX_JSON.close_object();
   end loop;

   APEX_JSON.close_array(); --children
   APEX_JSON.close_object();
end;

Additional possible features

Conditional expand option

With a bit more work, it is possible to only add the dummy child node when the node has no children. This would make it so the user does not have to press expand before knowing that the node has no children.

Compound keys

Some database structures use a compound key consisting of multiple columns to determine hierarchy. Therefore passing only the primary key (EMPNO) might not be sufficient to determine node identity. In that case it is possible to tweak the pNode.ID (VAL) to use multiple keys by either using a longer colon separated list, or even by packing the info in a JSON string.

1
Subscribe to my newsletter

Read articles from Jack Drolet directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jack Drolet
Jack Drolet