Nested Set Trees in ColdFusion (v0.8)

A common technique to manage hierarchical data in a relational database is to use an "Adjacency List" model, where you have both an ID column and a Parent ID column in a table. This is easy to understand and maintain but can be difficult or inefficient when you want to retrieve hierarchies of records.

The "Nested Set" model provides an alternative technique for managing this kind of data and is more efficient at reading a hierarchy but requires a little more work for inserts, updates, deletes and moves.

This project provides a library of ColdFusion code that you may like to use to help manage your hierarchical data using the nested set model.

Links

This project can be downloaded from RIAForge:
http://nstree.riaforge.org

The documentation for this release:
http://stannard.net.au/blog/page.cfm/nested-set-trees

Changes in this release

This release involved a significant rewrite of the library's structure, but I kept the API as similar as possible.

The main changes include:

  • Introduction of a NestedSetTreeTable object, which allows the creation of multiple NestedSetTree objects per table. So the creation of the NestedSetTree objects has now changed.
  • Introduction of a Node object to replace the previous struct nodes. So previous code that referred to node.id, node.l and node.r should be changed to use node.getId(), node.getLeft() and node.getRight()

Please add any comments/problems regarding this release to this entry, thanks.

Comments
25 Aug 2008 07:20PM
Hussein Grant said:
WOO! HOO!

Excellent Work Kevan! :)

I will be trying this out right away with my current flex implementation. I was holding off on certain things simply because I wanted the multi-tree features. I will keep you posted with my experience and hopefully if time allows I'll post a flex example for those interested in the flex side A.S.A.P.

Thanks Again,
Hussein
26 Aug 2008 07:07AM
Top stuff, I did a fair bit of work with nested set trees a couple of years ago and built a library I used internally from scratch but this looks like a huge step forward from what I was working with back then.

Love the simplicity and how well genericised the API is!
27 Aug 2008 03:51AM
Hussein, Craig, thanks.

If you have any further ideas I would be keen hear them.

At this stage I am still considering how the original NestedSetTreeTable object is created. It might be a nice to put all of it's setup in an xml config (similar to how ColdSpring or Transfer is configured). This XML would contain the datasources and definitions of all Nested Set Tree tables in the application. We would probably then have a NestedSetTreeTableFactory to work with, but I am not sure if this just adds unnecessary complexity for most use cases. Anyway I will think about this a bit further.
28 Aug 2008 08:29PM
Hussein Grant said:
XML is not a bad idea although I would rather the equivalent approach done using object instantiation via good old cf syntax. The "NestedSetTreeTableFactory" is a decent idea I think. I can see this being useful in a framework.

ON another note, In your query examples I noticed that you use cftransaction and cflocks around your database calls. This might be ok if you are using Access which doesn't support transactions but If you are using MSSQL, ORACLE or MySQL (Innodb) for example, this is an overkill and a potential performance hit under serious load, unless only a very small number of users interact with the category table, then I suppose it wouldn't matter greatly. Just my 2 cents.
29 Aug 2008 12:46PM
Hussein, thanks. My goal with the transactions and locking is as follows:

1) Transactions to provide a rollback mechanism in the case of a thrown error; such as adding a new node to the tree but the parent is no longer available (perhaps deleted by another user).

2) Locking to ensure the primary table data and NST table remain correctly aligned.

On thinking this though it would appear I don't need the locking on the code that calls the NST functions and the transaction would provide the rollback required if an error was thrown. Is that what you were thinking?

Your 2 cents is of great value :)
30 Aug 2008 07:03PM
Hussein Grant said:
Hi Kevan,

Yes, that was part of my thinking. I don't think as long as your database supports its own level of locking that you need to use cflocks around select queries in most cases. I guess some people use it along with cftransaction as a double safety measure, especially on query inserts which to me is more understandable than locking a select query. Truthfully the area of database locking and transactions is really not a straightforward beast to tackle as I am sure you already know. It really all depends.
1 Sep 2008 06:29AM
Hussein Grant said:
I should had mentioned in my last comment that using cflock around a potentially user-intensive query insert could cause loads of timeouts. I've experienced this first hand and was truly a pain in the behind. I ended up using cftransaction with read_committed isolation and haven't had the problem since.
2 Sep 2008 04:04AM
Hi Hussein

I would consider that in a site with a heavy load of updates/inserts then the use of this library would perhaps not be recommended - custom built nested set management with stored procedures would be a more efficient approach. However, that being said, I would still like this library to be as efficient as it can be.

I am sure that you are correct that the example usage can be improved. I will put some more thought into them.

Thanks for the feedback.
2 Sep 2008 09:30AM
Hussein Grant said:
Hi Kevan,

Here are some of my immediate thoughts of an xml implementation:

XML Config: tree.xml
<nstrees>

<nstree name="ProductCategoryTree" class="shopNutz.com.model.product.ProductCategoryTree">
<datasource>
<property name="NAME" value="shopNutz" />
<property name="USERNAME" value="admin" />
<property name="PASSWORD" value="rubberducky" />
</datasource>
<table>
<property name="NAME" value="categoryNST" />
<property name="TREE_ID" value="1" />
<property name="TREE_ID_COLUMN" value="treeID" />
<property name="ID_COLUMN" value="categoryID" />
<property name="LEFT_COLUMN" value="indexLeft" />
<property name="RIGHT_COLUMN" value="indexRight" />
</table>
</nstree>

</nstrees>

Inside your onApplicationStart() method
//Set up NSTree
<cfset application.nstree = CreateObject('component', 'nstree.NestedSetTreeTableFactory').init('/shopNutz/config/nstree/tree.xml') />

//Set up Product Category Service
<cfset application.productCategory = CreateObject('component', '').init( application.nstree.getTree('ProductCategoryTree') ) />

//Calling the Product Category Service to move a category from within your application controller
<cfset application.productCategory.moveCategory(form.sourceID, form.destinationID) />

Inside your Product Category Service component
//Move A Category Example

<cffunction name="moveCategory" access="public" output="false" returntype="boolean">
   <cfargument name="sourceID" type="numeric" required="true" />
   <cfargument name="destinationID" type="numeric" required="true" />
   <cfset var isMoved = false />

   <cfset isMoved = getProductCategoryTree().moveCategory(arguments.sourceID, arguments.destinationID) />

   <cfreturn isMoved />
</cffunction>

Thanks,
Hussein
2 Sep 2008 09:35AM
Hussein Grant said:
Oops! Forgot to include the service component reference.
"shopNutz.com.model.product.ProductCategoryService"


//Set up Product Category Service
<cfset application.productCategory = CreateObject('component', 'shopNutz.com.model.product.ProductCategoryService').init( application.nstree.getTree('ProductCategoryTree') ) />

That is what it should be.
3 Sep 2008 05:22AM
Hi Hussein, thanks for putting your thoughts down - much appreciated. I will think this through and come back with some ideas.
13 Nov 2008 04:40AM
Mark Ireland said:
There is a similar idea to this in farCry.

Its always worth a look.
Add a comment
(will not be published)
(include http://)