The PathProxy pattern: Persisting complex associations

PathProxy offers an easier way to persist complex relationships without so many lookup tables

1 2 3 4 5 6 7 Page 6
Page 6 of 7

Create a PathProxy

The createPathProxy() method handles the job of generating a PathProxy if one isn't found. Listing 7 looks into that method.

Listing 7. createPathProxy()

@Transactional
protected PathProxy createPathProxy(ManagedObject[] moPath){
    if (log.isInfoEnabled()) { log.info("| | | | | BEGIN createPathProxy: " + ArrayUtils.toString(moPath)); }
    // Get PP object
    PathProxy newPp = new PathProxy();

    int childIndex = moPath.length - 1;

    newPp.setEntityId(moPath[childIndex].getId());
    newPp.setEntityType(moPath[childIndex].getClass().getName());

    if (log.isTraceEnabled()) { log.trace("Set id: " + newPp.getEntityId() + " and type: " + newPp.getEntityType() + " on new PathProxy: " + newPp); }

    this.getJpaTemplate().persist(newPp);
    if (log.isTraceEnabled()) { log.trace("Saved newPp: " + newPp); }

    // Here is where we recursively build up the path until we find a parent path proxy that exists
    // Remove the current mo from path, to get the parent path
    Object[] objParentPath = ArrayUtils.remove(moPath, childIndex);
    // Convert back to mo array
    ManagedObject[] parentPath = new ManagedObject[objParentPath.length];
    for (int i = objParentPath.length - 1; i >= 0; i--){
        parentPath[i] = (ManagedObject)objParentPath[i];
    }

    // Get parent TreePoint
    PathProxy parentPp = (PathProxy)getPathProxyFromMoArray(parentPath, true);
    if (log.isTraceEnabled()) { log.trace("Here's the parentPp: " + parentPp); }
    // Set the parent
    newPp.setParent(parentPp);
    // This will either get the existing parentPp with its parents in place, or create a new one and get its parent, and so forth
    if (log.isTraceEnabled()) { log.trace("RETURN newPp: " + newPp + " with parent: " + newPp.getParent()); }

    return newPp;
}

The first thing to notice about Listing 7 is that it's a @Transactional method, because it updates the database when it creates a new PathProxy. The second thing to notice is that the method is recursive. In order to create the PathProxy, we need to get the parent proxy. Therefore, we create a parentPath, and invoke getPathProxyFromMoArray() on it. That will in turn end up at the create method if the parent can't be found. In this way, the algorithm recursively searches back to the earliest ancestor and, starting there, at the beginning, ensures that every node on the path exists. If a node does not exist, it creates it.

The way the algorithm works is important, because it ensures there is one and only one PathProxy for each node.

Creating path-specific associations

Now that you've seen how retrieving children from a path works, let's use the application to create some children, and see how that is accomplished. Remember that I use the term path-specific to refer to associations that require knowledge of other relationships -- exactly what the PathProxy is for. If you deploy the application and look at the first page, you'll see something similar to the screenshot in Figure 4.

Index.jsp with no data.
Figure 4. Index.jsp with no data

The extremely Spartan interface in Figure 4 has just enough to show off PathProxy's stuff. You can see that there are no instances of our domain classes. Creating some of these objects is straightforward, just use the Add links. After you do that, you'll see the tree reflect the new data, as in Figure 5.

Index.jsp with some objects created.
Figure 5. Index.jsp with some objects created

Now we can add a manager to a project by clicking on the project. Selecting a manager you've created previously and hitting OK will add it. Remember, that is being done via a JPA many-to-many mapping. Its pretty standard stuff. Once you've done that, however, you will be able to expand the tree down to the manager on the project. Clicking on the manager in the tree allows you to add a developer to the manager -- but only for that project!

How does that work? The first thing to realize is that when you click on the manager under the Managers on Project node of a Project, it actually sends back a nodeString to the server, which is saved in the session for use while the manager is being edited. To see what I mean, look at the SessionBean. Specifically, look at getCurrentSelection() in the (heavily truncated) Listing 8. That method is used by the managerDetail.jsp screen to get the current object, which also causes the method to grab the nodeString from the request parameters and save it. Since this bean is session scoped, we can then grab the nodeString later on and use it when setting a developer on the manager. That is, we'll use the nodeString to actually set the developer, via the pathProxy mechanism, on the path represented by the nodeString.

Listing 8. getCurrentSelection, truncated to show the essentials

public Object getCurrentSelection(){
        ...

        String selectionType = (String)context.getExternalContext().getRequestParameterMap().get("selectionType");
        String selectionNodeString = (String)context.getExternalContext().getRequestParameterMap().get("selectionNodeString");

        ...
        String[] nodeStringSplit = selectionNodeString.split("\\^");
        ...

                this.selectionNodeString = selectionNodeString;
}

When you select a developer on the Manager detail and click OK, what happens? Take a look at Listing 9.

Listing 9. Adding developers to a Project->Manager path

public void setDevelopersOnSelectedManager(List devsFromUi){
    assert(Manager.class.getName().equals(sessionBean.getSelectionType()));
    String nodeString = sessionBean.getSelectionNodeString();
    EntityPath path = this.getNewPath().setPathAsString(nodeString);
    if (log.isTraceEnabled()) { log.trace("path: " + ArrayUtils.toString(path)); }
    List existingO = pathProxyService.getPathChildren(path, Developer.class.getName());

    List<Developer> actualDevs = new ArrayList<Developer>();

    for (Object o : existingO){
        Developer existingDev = (Developer)o;
        if (!devsFromUi.contains(existingDev)){
            pathProxyService.removeChild(path, existingDev);
        }
    }

    for (Object o : devsFromUi){
        Developer newDev = (Developer)o;
        if (!existingO.contains(newDev)){
            pathProxyService.addChild(path, newDev);
        }
    }
}

As you can see, this method essentially gets the nodeString and, wrapping it in an EntityPath object, uses it with the pathProxyService to get all the developers that already exist on that path. The rest of the method is logic for removing any Developers that do not exist in the list passed back from the UI, adding any that are in that list, but don't exist yet, and leaving everything else the same (i.e., leaving the developers that exist both in the DB and in the UI list).

I think this method is pretty self-explanatory. It relies on the addChild() and removeChild() methods of the PathProxyService. If you look at those methods, you'll see that they are very simple. Their real complexity is all in the getPathProxyFromMoArray() method, which we've already covered!

In Figure 6 you can see I've recreated the example from the beginning of the article, Figure 1, with Robert, Mukunda and Johnie.

The extended project management tree.
Figure 6. The tree showing different developers on the same manager for different projects

What Figure 6 shows is that Johnie, the manager, has got Mukunda on one project and Robert on another. You can also see these relationships illustrated in the PathProxy table.

Table 1. The PathProxy table

path_proxy_id entity_id entity_type parent_id
2719748 4128769 tutorial.model.Project (null)
2719749 4096001 tutorial.model.Developer 2719747
2719744 3670016 tutorial.model.Manager 2719745
2719745 4128768 tutorial.model.Project (null)
2719746 4096000 tutorial.model.Developer 2719744
2719747 3670016 tutorial.model.Manager 2719748

As you can see, there is one PathProxy for every unique path. Each project, as the root, has a PathProxy, whose parent is null. There are two PathProxy objects pointed to the same manager, each having the appropriate project PathProxy as a parent. Finally, each developer has a PathProxy, with the managers as parents.

1 2 3 4 5 6 7 Page 6
Page 6 of 7