Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

The PathProxy pattern: Persisting complex associations

Illustrated With JPA, Spring, and JSF

  • Print
  • Feedback

Page 5 of 7

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.

  • Print
  • Feedback

Resources
  • Download the sample application for this article, built with Spring, JPA/Hibernate, and JSF. The application build tool was Maven 2 and it was tested on Tomcat 6.
  • PathProxy isn't Matt Tyson's first published design pattern. Learn about his design strategies for Ajax-style development in JavaServer Faces: the AjaxCommand strategy and the AjaxComponent strategy, both published by JavaWorld.
  • The Spring Framework Documentation Section 3.3.4.1 explains Spring's lookup method injection.
  • See the JPOX homepage for a good discussion of JPA inheritance.
  • "Understanding the Java Persistence API" (Aditi Das, JavaWorld, January 2008) is a two-part introduction to Java-platform persistence with JPA. Part 2 walks through an example implementation of JPA annotations.
  • "J2EE design decisions" (Chris Richardson, JavaWorld, January 2006) explores five design patterns for implementing enterprise applications using lightweight frameworks such as Spring and Hibernate. An excerp from POJOs in Action; Manning Publications, January 2006.
  • "Get started with Hibernate" (Christian Bauer and Gavin King, JavaWorld, October 2004) is a short introduction to Hibernate written by its creator, Gavin King. Excerpted from Hibernate in Action; Manning 2004.)
  • Visit the JavaWorld Java Standard Edition research center for more articles about core Java programming tools, design patterns, and concepts.
  • Also see Network World's IT Buyer's Guides: Side-by-side comparison of hundreds of products in over 70 categories.