jpajakarta-eelazy-loadingdeltaspike

How to prevent loading a full database recursive tree at once?


I'm trying to use the Tree component, in the PrimeFaces library, but due to a large amount of data I have to load it lazily. I followed a tutorial to do it, (http://blog.disy.net/primefaces-lazy-tree/) but it takes 15 seconds to load (instead of 23 seconds). Am I missing something in this tutorial or in my code? I can't let this like that because 15 seconds is way too long for the users.

Code:

public interface TreePodeFindWithParent {

List<VArboParObjectifsParents> findActiviteWithParent(Integer parentId); 

}

Method in the lazy loading class

public class PodeLazyTreeNode extends DefaultTreeNode
{
...
...
public PodeLazyTreeNode(VArboParObjectifsParents data, TreePodeService service)
{
super(vArboParObjectifsParents.class.getSimpleName(), data, null);
this.service = service;
}

...

private void ensureChildrenFetched() 
{
    if (!childrenFetched)
    {
        childrenFetched = true;
        if ((VArboParObjectifsParents)getData() != null)
        {
            Integer parentId = ((VArboParObjectifsParents)getData()).getIdRoot();

            List<PodeLazyTreeNode> childNodes = service.findActiviteWithParent(parentId).stream().map(item 
                    -> new PodeLazyTreeNode(item, service)).collect(Collectors.toList());

            super.getChildren().addAll(childNodes);
        }

    }
}

Method in the Service

public class TreePodeService implements Serializable, TreePodeWithParent
{
...
...
@Inject
private PodeArboParObjectifsParentsDao podeArboObjParentDao;
...
@Override
public List<VArboParObjectifsParents> findActiviteWithParent(Integer parentId) {
    // TODO Auto-generated method stub
    return podeArboObjParentDao.findByIdParent(parentId);
}

DAO (Requests are done with the Data Module of Apache DeltaSpike):

@Repository(forEntity=VArboParObjectifsParents.class)
public interface PodeArboParObjectifsParentsDao extends EntityRepository<VArboParObjectifsParents, Integer>
{
List<VArboParObjectifsParents> findByIdParent(Integer idParent);
List<VArboParObjetcifsParents> findByIdTypeActivite(Integer idType);
}

Call of the method in the View:

@PostConstruct
public void initView()
{
    initArbo();
}

public void initArbo()
{
    List<VArboParObjectifsParents> vArbos = treePodeService.getPodeArboObjParentDao().findByIdTypeActivite(1);
    this.root = new PodeLazyTreeNode(null, treePodeService);
    for (int i = 0; i < vArbos.size(); i++)
    {
        root.getChildren().add(new PodeLazyTreeNode(vArbos.get(i), treePodeService));
    }
}

UI:

<p:tree value="#{testView.root}" var="_node" >
                <p:treeNode type="VArboParObjectifsParents">
                    <h:outputText value="#{_node}"/>
                </p:treeNode>

             </p:tree> 

Solution

  • Cause of the problem

    From the PrimeFaces Showcase:

    Tree has two modes, in client mode all nodes are available at client side whereas in ajax mode only expanded nodes are available.

    This means that all nodes of the tree are loaded into the browser before the tree becomes visible. The more nodes you have, the more calls to the service are required to load the whole tree. There is also a hint on the page that you linked:

    n+1 Problem

    Our implementation has so far been very simple. However, it does put a lot of stress on the BackendService. Whenever a node is expanded a call to BackendService.findWithParent(...) is made for each child of the expanded node. This is called the n+1-Problem, sinces you need n+1 service calls to provide the expanded node.


    Solution

    Use the ajax mode of the tree by setting dynamic="true" (and optionally cache="false" to always reload the child nodes) and load / remove the child nodes by calling a listener in the view.

    Also please do not specify the service via constructor. The tutorial you found is pretty bad in my opinion. Inject the service into the backing bean and load the node data there.

    Example:

    Facelet (UI)

    <p:tree cache="false"
            dynamic="true"
            value="#{testView.root}" var="_node">
    
      <!--load/update nodes on demand-->
      <p:ajax event="expand"
              listener="#{testView.onNodeExpand}" />
      <p:ajax event="collapse"
              listener="#{testView.onNodeCollapse}" />
    
      <p:treeNode type="VArboParObjectifsParents">
        <h:outputText value="#{_node}"/>
      </p:treeNode>
    </p:tree>
    

    Backing Bean (View)

    @Named
    @ViewScoped
    public TestView implements Serializable {
    
        @PostConstruct
        public void initView()
        {
            initArbo();
        }
    
    
        public void initArbo()
        {
            List<VArboParObjectifsParents> vArbos = treePodeService.getPodeArboObjParentDao().findByIdTypeActivite(1);
            this.root = new PodeLazyTreeNode(null);
            for (int i = 0; i < vArbos.size(); i++)
            {
                PodeLazyTreeNode childNode = new PodeLazyTreeNode(vArbos.get(i), this.root);
                addDummyTreeNode(childNode);
            }
        }
    
        public void onNodeExpand(NodeExpandEvent event) {
            final TreeNode expandedTreeNode = event.getTreeNode();
    
            // load child data from service
            // ...
            if (/* child data available */) {
                // create child tree nodes with dummy tree node
                // ...
                expandedTreeNode.setExpanded(true);
            } else {
                // remove dummy tree nod to mark as leaf
                // ...
                expandedTreeNode.setExpanded(false);
            }
        }
    
        public void onNodeCollapse(NodeCollapseEvent event) {
            final TreeNode collapsedTreeNode = event.getTreeNode();
    
            // remove child nodes and add dummy tree node
            // ...
    
            collapsedTreeNode.setExpanded(false);
        }
    
        /**
         * Adds a dummy {@link TreeNode} to the specified parent node. The dummy is
         * used to prevent nodes with child nodes from being rendered as leaf nodes
         * until they are expanded. The dummy will be removed when the parent node
         * is expanded the first time.
         *
         * @param parentTreeNode
         */
        private void addDummyTreeNode(TreeNode parentTreeNode) {
            final TreeNode dummyNode = new DefaultTreeNode("Loading...",
                    parentTreeNode);
        }
    
    }