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 5
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.

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