PathProxy offers an easier way to persist complex relationships without so many lookup tables PathProxy is a design pattern for persisting complex relationships without cluttering up your database. In this article JavaWorld contributor Matthew Tyson introduces his PathProxy pattern and walks you through an example application implementation based on Spring, JSF, and JPA/Hibernate. When to use PathProxy Use PathProxy if: You have many entities whose interrelationships are complex and require knowledge of other relationships. Creating explicit objects to represent these types of relationships becomes burdensome. This is especially true if the objects must be persisted, creating a proliferation of database tables. Consider PathProxy if: Your system design calls for a number of classes whose sole or primary function is to model the relationships between other objects. Using PathProxy is more complicated than using simple objects to represent relationships, so consider your particular situation. If you have few relationships to store and they are not too complicated, PathProxy may not be the right choice. On the other hand, once you reach a certain level of relationship complexity, using PathProxy will greatly simplify your overall system design. Being able to reuse the same mechanism over and over again is also a huge time-saver. It is easy to persist simple object relationships in a database using the Java Persistence API, but what’s a good way to store information about complex relationships in your system? When your class relationships require pathing knowledge, that is, knowledge about a number of related objects, then the standard “many-to” associations won’t cut it. The PathProxy class is an abstraction of such relationships, allowing you to manage, persist, and extend them without complicating the classes themselves, and without a proliferation of lookup tables. The fundamental idea is this: create a class that can point to any entity in the system, and that also can reference its own class as a parent. With this class, you create a tree structure that maintains the interrelationships outside of the referenced objects. Building a JPA mapping around this class requires some thought, but is quite powerful. In this article I’ll introduce the beginnings of a simple application — a project tracking system — built using JPA/Hibernate, Spring, and JSF. I’ll then show you how how the PathProxy class handles the application’s data structure, specifically mapping relationships between project managers, developers, projects, and tasks. Path-specific relationships The PathProxy solution applies in any situation where an entity can appear as an association of another entity type, but only for a specific path. I refer to such relationships as path specific. E is a child of D, but only for the path A–>B–>C–>D–>E. On another path, D might have no children (A–>B–>Q–>D) or might have a different child or children (A–>B–>X–>D–>Z). As an example, imagine a development team consisting of a project manager named Johnie and two developers, Robert and Mukunda. On project A, Johnie leads Robert and on Project B, Johnie leads Mukunda. This is a somewhat contrived example, but not an uncommon scenario in the world of corporate structures. In the real world, you might have the efficiency of the same process in different business locations, or the actions taken in response to the same event by different teams. For the purpose of this article, we’ll stick with the simple example. The development team structure is shown in Figure 1. Mapping the relationships Before diving into how the PathProxy class would handle this structure, let’s consider how we might approach the data modeling requirements for this scenario without it. We know that a project can have many managers, and vice versa, so we have a many-to-many relationship there. The same is true of the manager-to-developer relationship. So there are two many-to-many relationships that imply two cross-reference tables in the database, a project_manager table and a manager_developer table. In the case of the manager_developer table, however, having just two columns (manager_id and developer_id) isn’t enough: we need to know which project the manager and developer are working on. If we add a project_id column, we end up with a project_manager_developer table. That might be fine. Or it might not. Figure 2 — an illegal hybrid of ERD and UML Object Diagrams, by the way — shows the project_manager_developer table in action. What happens if we need to extend the hierarchy in either direction? That is, what if at some point we have to track projects as children of locations; or what if we need to track the tasks a developer is working on? We could easily end up with a project_manager_developer_task table. The trouble with this sort of mapping is that it only gets more complex, requiring more tables and classes, as the project hierarchy evolves. Imagine what happens if we need to also have tasks on managers? Basically, every time we want to support having the task type show up under a specific path, we have to add a table that contains all the required data for that path. We could end up with a project_manager_ba_task table, and a project_manager_qa_task table, and — you get the picture. To summarize: if you use simple cross-reference tables (also called lookup tables or crosswalk tables), every time you add another possible path through your objects, you have to add another table, an object to map that table, and all the supporting code to deal with those additions. Wouldn’t it be nice to be able to handle all this pathing in a consistent way — and all in one table? That’s what the PathProxy pattern does. Using the PathProxy pattern The PathProxy class adds a supportive structure to your data model, as shown in in Figure 3. Note that I’ll be using the project–>manager–>developer table to introduce the PathProxy pattern. You probably wouldn’t need the PathProxy class to handle those simple relationships; you could store them in lookup tables. The point is to keep things simple enough that you can focus on the PathProxy pattern. At the end of the article, we’ll briefly discuss the steps required to add a task relationship to the table. Doing so is straightforward with the PathProxy in place. Pathing, or lateral association Although the PathProxy class has a parent, and I’ve used the term hierarchical, the PathProxy pattern doesn’t necessarily refer to hierarchical information. Pathing is a better word for it. For example, tracking which QA engineer was working with which developer would be a “lateral” association. Our sample application is built with Spring, JPA/Hibernate, and JSF. I’ve used Maven 2 as my build utility and tested the application on Tomcat 6. Installing and running the app is very easy: just unzip it, and type “mvn clean install” at the root (you’ll need to have Maven 2 installed). You can also copy the tutorial.war file into Tomcat’s /webapps directory. Finally, you can use mvn eclipse:eclipse to generate the appropriate .project files for Eclipse, and then just import the projects into your IDE. Maven includes similar goals for the other IDEs. I’ve used JPA annotations to describe the persistence of the domain objects. I’ll quickly walk you through the annotations and the Spring-JSF application setup. Then we can move on to learning about where the PathProxy class comes in. The applicationContext.xml file in WEB-INF refers to one of two database configuration files: Listing 1. Database configuration files in applicationContext.xml <!-- <import resource="oracleConfig.xml" /> --> <import resource="hsqlConfig.xml" /> If you want to use Oracle, you can expose the oracleConfig and comment out the HSQLDB config. The app should work out of the box without any intervention from you, although the data will only be “persisted” to RAM. If you check out the oracleConfig.xml file, you’ll see its set up to use Oracle XE running on localhost. It should be pretty easy to set that up for your environment if you want to. In both database config files, the entityManagerFactory bean has <property name="generateDdl" value="true"/>, which means that the app will check for required database structures to realize the persistence mappings. If not found, it will automatically create them. Aside from being cool, this feature is great for rapid prototyping. We’ll rely closely on the sample application to see what’s going on, so be sure you have it set up properly. Assuming that’s done, let’s get started! The domain model We know we’ll need three objects: Project, Manager, and Developer. You’ll see the classes in the example are marked up with annotations to define how they are persisted to the database. Take a look at the Manager property of Project. We’re not going to use PathProxy to model the Project-->Manager relationship because it doesn’t need it. We’ll use a simple many-to-many mapping instead. As you’ll see, you can do that and still use PathProxy to track the relationship. This would be considered a kind of de-normalization of the database, because the information regarding Project-->Manager is repeated. To be clear, what we are doing is using a normal many-to-many for the Project-->Manager relationship, and then using PathProxy to represent that relationship when necessary — specifically, when there is a developer on a given path. We’ll use the JPA many-to-many annotation to map the Project-->Manager relationship. We won’t need a class for this, but JPA will create the Project_Manager table. Just keep in mind that the Project_Manager table is the cross-reference for the Project and Manager tables, not a table holding data about your ever-cheerful project manager. The rest of the domain model is standard fare. You’ll notice that the business objects implement the ManagedObject interface. ManagedObject allows us to treat all the objects the same in one important regard: we can be assured their ID is found under the getId() method. We also use the JPA’s @MappedSuperclass annotation in the Person class. This is gives us the ability to keep the common functionality in the class while persisting the data to each subclass table. (See the Resources section for a nice discussion of JPA inheritance.) Now let’s take a closer look at the PathProxy class in your IDE. It has an entityType and entityId fields, which allow it to refer to any object in the system. One thing to notice about this mapping is the cascade flag on the children. What that means is that when you remove a PathProxy instance, the JPA provider (Hibernate in our case) will automatically remove any children associated with that instance. That’s very important because you don’t want any dangling PathProxy instances corrupting your data. Service and DAO layers The application’s service and DAO layers are also mostly vanilla flavored, except for the PathProxy stuff. Notice that I’ve gathered the common operations that can be used on any object into the CommonService interface. That way every persistent object doesn’t require its own version of every operation, like the findById() method. The PathProxyDAO class contains the heart of the logic that manages the PathProxy instances. This class provides us the CRUD capabilities for objects that rely on the PathProxy. Using it, we can add an object to a path, get objects of a type from a path, and remove objects from a path. The Tree and EntityPath classes The simple application has only a handful of backing beans. For getting into the PathProxy, the AjaxTreeBean is the most important. The AjaxTreeBean supplies data to the tree on the main page of the application. As you know, there is a real issue with HTTP, which is that everything has to go over the wire as strings. Don’t get me wrong, I love HTTP for what it’s given us (simplicity, universal adoption, and a standard protocol). It’s just … well, everything is strings. That includes the paths we are representing in our system. The AjaxTree used in the sample application was built using concepts explained in a previous JavaWorld article (see “The AjaxComponent strategy for JSF“). It represents every node with a nodeString. It is up to the application builder to decide how the nodeString is marshalled and unmarshalled. When the AjaxTree is rendering, the first thing it needs to do is output the root nodes of the tree. You can see this in Listing 2. Listing 2. AjaxTreeBean.getRootNodes() public List<AjaxTreeNode> getRootNodes(){ rootNodes = new ArrayList<AjaxTreeNode>(); rootNodes.add(new AjaxTreeNode(Constant.PEOPLE, false, "People", Constant.PEOPLE)); Long projectCount = commonService.getCount(Project.class); rootNodes.add(new AjaxTreeNode(Constant.PROJECTS, projectCount <= 0, "Projects", Constant.PROJECTS)); return rootNodes; } Listing 2 shows how the AjaxTreeBean provides the data necessary to render the root nodes of the tree. The AjaxTreeNode constructor’s first argument is the NodeString. In the case of the root nodes, it’s very simple. Those constants are strings. When someone clicks on the first root node, the AjaxTree will send back the string “people“. It then falls to the method getChildNodes() to take that string and return a new list of nodes, representing the children of the people node. Listing 3. AjaxTreeBean.getChildNodes() – part 1 public List getChildNodes(String nodeString){ if (log.isInfoEnabled()) { log.info("BEGIN getChildNodes(): " + nodeString); } EntityPath path = this.getNewPath().setPathAsString(nodeString); // Get an EntityPath object representing the path ArrayList<AjaxTreeNode> children = new ArrayList<AjaxTreeNode>(); if (path.equals(Constant.PEOPLE)){ Long devCount = commonService.getCount(Developer.class); children.add(new AjaxTreeNode(Developer.class.getName(), devCount <= 0, "Developers")); Long managerCount = commonService.getCount(Manager.class); children.add(new AjaxTreeNode(Manager.class.getName(), managerCount <= 0, "Managers", "normal", "managers")); } else if (path.equals(Developer.class.getName()) || path.equals(Manager.class.getName()) || path.equals(Project.class.getName())){ try { children.addAll(this.getMoNodeList(commonService.getAll(Class.forName(path.toString())))); } catch (Exception e){ throw new RuntimeException("Unknown type (classname expected): " + path); } // } else if... // Continued in Listing 4 ... Listing 3 shows the first part of getChildNodes. The first thing we’ve done is to wrap the nodeString in an EntityPath object. You’ll learn more about the EntityPath class in a bit. We’ve also tested whether the path is Constant.PEOPLE (“people“). If so, we create a couple more nodes, each with their own nodeStrings. The process can then repeat on the newly added nodes. Getting children on a path In the case of Managers, Developers, and Projects, the logic is simple: grab all the instances of each type and wrap them in AjaxTreeNodes. Things get more interesting when we start dealing with the pathing. We want to show the managers who are on the project, and also the developers working for each manager on each project. You can see this in Listing 4. Listing 4. AjaxTreeBean.getChildNodes() – part 2 // ... Continued from Listing 3 } else if (path.getSize(true) > 0){ Project clickedProject = (Project)path.getEntity(0); // Get the project, which is the first object in the path // In this system, only a project instance is alone in a path if (path.getSize(true) == 1) { // A project instance if (path.getPathAsString().indexOf("managersOnProject") == -1) { if (log.isTraceEnabled()) { log.trace("clickedProject: " + clickedProject); } int manOnProjectCount = clickedProject.getManagers().size(); if (log.isTraceEnabled()) { log.trace("manOnProject: " + manOnProjectCount); } PathHeader header = new PathHeader("Managers On Project"); children.add(new AjaxTreeNode(nodeString + EntityPath.NODE_STRING_SEPARATOR + EntityPath.getObjectAsNodeString(header), manOnProjectCount <= 0, header.getLabel())); } } else if (path.getSize(true) == 2) { if (log.isTraceEnabled()) { log.trace("Opening managersOnProject node."); } for (Manager manager : clickedProject.getManagers()){ String manOnProjectNodeString = nodeString + EntityPath.NODE_STRING_SEPARATOR + EntityPath.getObjectAsNodeString(manager); // Notice we just add the new child node (manager) to the path to get the child developers, to determine the leafs status of the node List<Object> projectManagerDev = pathProxyService.getPathChildren(path.addChild(manager), Developer.class.getName()); children.add(new AjaxTreeNode(manOnProjectNodeString, projectManagerDev.size() <= 0, manager.getFirstName(), Manager.class.getName(), manager)); } } else if (path.getSize(true) == 3) { // project->header->manager List<Object> projectManagerDev = pathProxyService.getPathChildren(path, Developer.class.getName()); for (Object dev : projectManagerDev){ Developer developer = (Developer)dev; String devNodeString = nodeString + EntityPath.NODE_STRING_SEPARATOR + EntityPath.getObjectAsNodeString(developer); List<Object> projectManagerDevTask = pathProxyService.getPathChildren(path.addChild(developer), Task.class.getName()); children.add(new AjaxTreeNode(devNodeString, projectManagerDevTask.size() <= 0, developer.getFirstName(), Developer.class.getName(), developer)); } } else if (path.getSize(true) == 4){ // Again, we know a three node path is project->manager->developer because the system is so simple List<Object> tasksOnDev = pathProxyService.getPathChildren(path, Task.class.getName()); children.addAll(this.getMoNodeList(tasksOnDev)); } } else { throw new IllegalArgumentException("Unknown node type: " + nodeString); } if (log.isInfoEnabled()) { log.info("RETURNING children: " + children); } return children; } Listing 4 continues the handling of the nodeStrings. It operates by checking how many nodes are in the nodeString. This is a very simple strategy for figuring out what node has been clicked on. In a more complex system, you’d need more complex rules. This method is very clean and neat; the challenge is to keep the logic that way as your relationships grow more complex and involved. A couple of tips: Use the nodeString to keep your logic neat, you can define it however you want. Push complexity into the EntityPath class. The way I’m doing it here is a great template, but I encourage you to find strategies that suit your situation. After determining what node has been clicked on, the method delegates to the service layer to get the children. You’ll notice that when we need to get the developers that are assigned to a manager on a particular project, we make a call to PathProxyService. We’ll take a look at that class in just a moment. The EntityPath class You might also have noticed the class EntityPath in Listing 4. This class basically encapsulates a path in the system, which has a number of advantages: It holds all the logic for manipulating paths (like adding a child in Listing 4). It allows you to pass the object around the layers of your app without leaking any information. In a large system, you would probably want to hide all information regarding the string representation of paths inside the EntityPath. Anytime you want a string path, you get it from the EntityPath. Note that the EntityPath implementation here does not do everything listed above, but it does show you the role it can play in your persistence architecture. A quick note about how we’re getting instances of EntityPath in Listing 4. This technique is called Lookup method injection in Spring terminology (check out section 3.3.4.1 of the Spring docs). It’s a little funky, but it suits our purposes here. The PathProxyService interface PathProxyService is the interface implemented by our PathProxyDao (this simple application doesn’t really require a well-defined service layer). Listing 5 displays the PathProxyDao method we use to get children from a path. Listing 5. getPathChildren() public List<Object> getPathChildren(EntityPath path, String type){ PathProxy parentPp = this.getPathProxyFromMoArray(path.getPathAsObjects(), false); if (log.isTraceEnabled()) { log.trace("parentPp: " + parentPp); } List<Object> list = new ArrayList<Object>(); if (parentPp == null){ if (log.isInfoEnabled()) { log.info("No object found on path: " + ArrayUtils.toString(path)); } } else { String hql = "select t from " + type + " t, PathProxy pp where t.id = pp.entityId " + " and pp.parent = :parentPp"; if (log.isTraceEnabled()) { log.trace("hql: " + hql); } Map<String, Object> params = new HashMap<String, Object>(); params.put("parentPp", parentPp); list = this.getJpaTemplate().findByNamedParams(hql, params); } if (log.isInfoEnabled()) { log.info("RETURN list: " + list); } return list; } You can see this method is actually pretty simple. That’s because most of the complexity is in the getPathProxyFromMoArray() method. Let’s dive right into that, because it is the heart and soul of the PathProxy pattern’s magic. The heart of the PathProxy pattern getPathProxyFromMoArray() is a fairly dense method. I’ll list it all at once here and then refer back to it. Listing 6. getPathProxyFromMoArray() public PathProxy getPathProxyFromMoArray(ManagedObject[] moArray, boolean createIfNotFound){ if (log.isInfoEnabled()) { log.info("= = = = = = =BEGIN getPathProxyFromVoArray(): " + ArrayUtils.toString(moArray)); } if (moArray == null || ArrayUtils.isEmpty(moArray)){ if (log.isInfoEnabled()) { log.info("Got an empty voArray, returning null."); } return null; } // Build a query string that has as many name/id parameters as there are vo in the array // Note, this may not work with entities using composite keys - hibernate issues invalid sql for null StringBuffer queryString = new StringBuffer("select pp from PathProxy pp " + "where "); // (A) if (moArray.length > 0) { for (int i = moArray.length - 1; i >= 0; i--){ // (B) if (log.isTraceEnabled()) { log.trace("Creating ppClause for VO: " + i + " : " + moArray[i]); } StringBuffer ppClause = new StringBuffer("pp"); // (C) // Add as many .parent's to the pp as we have counted into the hierarchy, this gets us the parent pp (eg, pp.parent.parent when i = 2) for (int parentCount = moArray.length - 2; parentCount >= i; parentCount--){ // (D) ppClause.append(".parent"); } // (E) if ((moArray[i] != null) && (!moArray[i].equals("null"))){ // If not null, add type and id String ppEntityType = ppClause.toString() + "." + PathProxy.ENTITY_TYPE_PROPERTY + " = '" + moArray[i].getClass().getName() + "'"; // Here's where we benefit from using the ManagedObject base class ... no need to figure out what property to use for the ID String ppEntityId = ppClause.toString() + "." + PathProxy.ENTITY_ID_PROPERTY + " = " + moArray[i].getId(); queryString.append(ppEntityId); queryString.append(" and "); queryString.append(ppEntityType); if (i > 0){ // Add an 'and' if we will add more ppClauses to the queryString queryString.append(" and "); // (F) } // (G) } else { // This vo is null, so add tp.parent = null // This is where Hibernate generates bad sql for composite keys, removing fixes that: queryString.append(ppClause.toString() + " is null"); // Remove and added by last iteration (if you disable the 'is null') // queryString.delete(queryString.length() - 4, queryString.length() ); //(" and ") } } } else { // Shouldn't get here return null; } if (log.isTraceEnabled()) { log.trace("------ Here's the queryString: " + queryString + " ------"); } List lst = this.getJpaTemplate().find(queryString.toString()); if (log.isTraceEnabled()) { log.trace("Query for path proxy with moArray returned: " + lst); } PathProxy ppToReturn = null; if (CollectionUtils.isEmpty(lst)) { // No path proxy there yet if (createIfNotFound){ if (log.isInfoEnabled()) { log.info("No pathproxy on that path, creating new..."); } ppToReturn = createPathProxy(moArray); } else { ppToReturn = null; } } else { ppToReturn = (PathProxy)lst.get(0); } if (log.isTraceEnabled()) { log.trace("RETURN ppToReturn: " + ppToReturn); } return ppToReturn; } First off, “Mo” stands for ManagedObject. So what we are saying with the call to getPathProxyFromMoArray() is, “Give me an array of ManagedObjects representing a path, and I’ll give you back the PathProxy — the one and only unique PathProxy — which points to that node. This last point is important. There is at most one PathProxy for every unique path. Notice that getPathProxyFromMoArray() also has a boolean switch to determine if a PathProxy should be generated if one is not found: createIfNotFound. For some operations it makes sense to create the PathProxy, for example, when you are adding a child to the path. If you are deleting or checking for the existence of children, however, it does not make sense! The core idea behind finding a PathProxy is building an HQL query (actually, a JPA-QL query) that will return us the unique PathProxy for the Object array. We do this by saying, “select from pathProxy pp where pp.entityType = ? and pp.entityId = ? and pp.parent.entityType = ? and pp.parent.entityId = ? ...“. Essentially, as long as there are more parents in the array, we slap on another .parent. Footnotes to Listing 6 I’ve added some footnotes to Listing 6, I’ll quickly go over them here. A : this line sets up the beginning of the Query . B: counts backward from the moArray because our path goes from parent-->child. This is just a decision. You could go from child-->parent. C: begins a StringBuffer that will hold our “PathProxy clause”. D: adds on as many “.parents” as needed. You’ll notice that the first time through, there is no “.parent” added — that’s because we want the first PathProxy to point directly to the first child. Basically, this inner for loop counts to 1 less than the number of path objects the outer loop has traversed , adding a .parent each time. So (not to belabor the point but), the first time through there are no .parents added. The fifth time, there would be four: pp.parent.parent.parent.parent. The query adds a new part of the where clause that refers to each node in the path. E: shows a little logical structure that takes our PathProxy clause and adds an .entityType and .entityId to two different Strings. Next, we add those together and add them to the query. If we’re not at the end of the path yet, we append an “and” (see F). Also, because we have a ManagedObject to deal with, we can easily get the objects’ ids (see inline comments). Stepping back, I want to point out that we’re using the Class names for the objects as their type. This has a lot of synergy with JPA, which also uses the Class to identify objects. We could have added a getType() method to ManagedObject and used that. I’ve found, though, that using the classname is a good solution. G: shows an else that handles a null in the path array. This should occur only at the end of the array. At the end of the query, it will add a “... and pp.parent.parent.parent is null“. As the inline comments note, if you are dealing with composite keys, Hibernate doesn’t output the correct SQL, and you will get an error. You can usually get away with leaving this off, so long as your root-level objects are unique for the paths in your system. Now we’re at the point where we can execute the query and see whether we have a PathProxy or not. If not, we’ll either create one or return null, based on the createIfNotFound flag. 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. 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. 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. 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. Adding another entity I mentioned at the beginning of this article that PathProxy would make adding another entity to our system easy. To prove it, I added a Task entity and its relationship to Developer to the system in just a couple of hours. The PathProxy really helped on the back end, because I had only one class and one table to add. Besides that, all the work is in the UI: addTask.jsp, developerDetail.jsp, adding a facet to the index.jsp tree, and backing bean stuff: adding the TaskBean, adding a couple methods to PersonBean, and adding the node handling for Task to AjaxTreeBean. The PathProxy itself did not change at all. The UI and backing bean code would have to happen regardless of how the relationship were maintained. It is simpler with the PathProxy in place, however. You don’t have to add a class and table for the relationship, and there’s less churn in the changes made to the backing bean code because it relies on the same PathProxy logic, and doesn’t have to deal with a new class. Seeing everything that doesn’t have to be done to add in the Task class is great confirmation that the PathProxy pattern keeps the domain model and persistence layer stable — what a relief! In conclusion The key benefits to the PathProxy pattern are consistency and extensibility. PathProxy’s enforced consistency greatly simplifies the overall system design. Its extensibility makes a routine of adding more relationships, even ones with extremely complicated pathing. Just build the path and hand it off to the PathProxy. You can do that as many times as you need to without changing the database schema or adding any more classes. Like most design patterns, the PathProxy really needs to be justified in its usage, otherwise you risk over-complexity. Also like other design patterns, if your situation calls for it, it is exactly what you need and can provide a major foundation to your system, becoming an architectural principle as well as a design pattern. I hope you will use the PathProxy pattern for those situations where it is just what you need. Open SourceData ManagementDesign PatternsSoftware DevelopmentJavaLibraries and Frameworks