matt this is a great tutorial. this scenario is one i have always struggled with and tried to avoid with crappy coding!!! i don't comment enough on tutorials - so now i will start. awesome work. mike
This tutorial is dedicated to megan, who made me think and stretch my mind to come up with an efficient, scalable solution to a real-world application development problem; to dduck1934, who encouraged me to make the time to finish writing this tutorial after letting it sit unfinished for 6+ months; to all EasyCFM members and visitors past, present, and future; and lastly, to the ColdFusion community as a whole, which is in my opinion the greatest group of developers known to the web.
One day, I was reading a post on the EasyCFM forums where a member was expressing their difficulty with categories and subcategories. So, in my infinite wisdom I chimed in on the thread and gave an example of my then-preferred method of handling them. At the time, the solution seemed simple and easy to implement.
Create two tables in your database: tblCategories, and tblSubCategories. They are structured like so:
categoriesID (primary key)
category_name
sub_categoriesID (primary key)
category_id (foreign key, related to 'tblCategories.categoriesID')
sub_category_name
Now I had my categories and my subcategories, and they were really easy to retrieve and output to the screen. You just run a query for your categories, and then as you're outputting them, query to get the subcategories.
See Listing 1.
Thanks to another member who posted (thank you megan!), I realized that there are two major flaws with this table schema:
So, one solution to the limitations of the two-table schema is to narrow it down to just one single table, constructed in this fashion:
categoriesID (primary key)
category_name
parent_id
Notice that this table is identical to my original categories table, with the exception of one additional field: parent_id. This field defaults to a zero (0), which indicates that the category is a top-level category. However, if the category is a subcategory of another category, we simply enter the categoriesID of the parent into this field. (If that is really confusing, don't leave just yet, we'll cover it in more detail in a moment.)
We have successfully reduced our table requirements from two down to one, but this schema, by itself, also only reduces our problems from two down to one. We can now go as many levels deep with our subcategories as we want to without changing our table schema, but we are still required to run another query for each subcategory. Unfortunately, we've failed to reduce the load on our database and server(s).
So then, what is the solution? Enter two things that have a tendency to scare ColdFusion developers to death:
We're about to get down-and-dirty here, so if you need to take a quick break, now's the time to do it!
While the techniques discussed in this tutorial use OO practices and thought patterns, it is not intended to be a study on OOP. There are many great books out there that will assist you in learning and understanding OOP. There are also quite a number of online resources available to assist you. Just go to google.com and type in OOP.
For the purpose of this tutorial, all you need to understand about OOP is this:
What on earth is recursion? Here's what the dictionary says:
"To cause to surge or flow back; specifically, to bring partly digested food from the stomach back to the mouth."
Ooooops! My apologies. That's the definition of regurgitate. But, as you will soon see, it is actually very similar. Here's the correct dictionary definition for recursion:
"An expression, such as a polynomial, each term of which is determined by application of a formula to preceding terms; a formula that generates successive terms of a recursion."
If you're like I was when I first read that definition, you're sitting there with a blank stare saying to yourself: "HUH!?!"
So, I set out to find out what recursion was, in terms that were understandable to those of us who are not math or computer science majors, and ran across this little tidbit at Wikipedia.com.
"Recursion is the process a procedure goes through when one of the steps of the procedure involves rerunning the entire same procedure. A procedure that goes through recursion is said to be recursive. Something is also said to be recursive when it is the result of a recursive procedure."
When I read that, it reminded me of the cow and regurgitation, and voila! What had just been completely confusing suddenly made perfect sense. So then, in the world of ColdFusion, recursion, or a recursive function, is a function that performs a particular task, but in order to complete the task it must "regurgitate", and run itself all over again. Some recursive tasks actually require the function to run itself many times over.
So then, without further ado, let's get to some practical application of our newfound regurgi... I mean recursive wisdom.
To get started, we first need to create our database table. Here are scripts for SQL Server and MySQL.
Next we'll populate our new categories table with some categories that we'll be using throughout the remainder of the tutorial.
See Listing 3.
After populating the table, we run a query to get all of the categories. Assuming that all went according to plan in our Listing 4 code, you should end up with something like the following on your screen.
Now that we have the basics out of the way...
For those who are still scratching their head about the whole parent_id thing, let's look at a detailed diagram of how our categories table actually works.
As you can see by the 'Rules of Conduct IV' category, we can now go as many levels deep in our categories as we need to, and they are all stored in one convenient table. Flaw #1 fixed!
This tutorial assumes that you possess at least basic knowledge of ColdFusion Components (CFCs), so we won't be spending any time discussing them.
So then, let's build our recursive object! Open up a blank page in your favorite editor, and write the following code:
<cfcomponent hint="An object for recursive searching" displayname="objRecursive" output="false"> <!--- "constructors" ---> <cfset variables.properties = structNew() /> </cfcomponent>
Now save that file as 'recursiveObject.cfc' (minus the quotes, of course). Congratulations! You've just created your first recursive object! Okay, not really, but we're headed in the right direction.
You'll notice that we've created a variable inside our object named 'properties', which is a structure. This structure will be used to hold all of the properties that provide the makeup of the object. Now we need to add a couple of methods that allow us to set (define) and get (retrieve) these properties. We aren't going to discuss them; they're simple enough that you should have no trouble figuring out what they do and how they do it. Just plug them into the recursiveObject file.
See Listing 5.
Now that our object contains the methods that allow us interact with its properties, we need to add the method that will initialize our object. Conveniently enough, this method is named init.
<!--- begin init() method ---> <cffunction name="init" hint="Initializes the object" returntype="recursiveObject" output="false" access="public"> <!--- set the method arguments ---> <cfargument name="qry" hint="The query object that the recursion is based upon" required="yes" type="query" /> <cfargument name="dspField" hint="The name of the field in the provided query from which to retrieve the display text" required="yes" type="string" /> <cfargument name="parentField" hint="The name of the field in the provided query that contains the parent ID; this is typically the PRIMARY KEY for the table" required="yes" type="string" /> <cfargument name="childOfField" hint="The name of the field in the provided query that contains the ID of the parent element; this is often named 'parent_id'" required="yes" type="string" /> <cfscript> setProperty("qry", arguments.qry); setProperty("dspField", arguments.dspField); setProperty("parentField", arguments.parentField); setProperty("childOfField", arguments.childOfField); return this; </cfscript> </cffunction> <!--- end init() method --->
The init() method contains several key things that we need to make sure and be aware of:
The arguments that are passed into the init() function are stored as object properties. This allows us to use them over and over and over again inside of our object. You'll see plenty of examples of this shortly.
NOTE: You should never use the 'this' keyword inside your objects for any purpose other than the one we just reviewed; returning the object to the caller. Doing so will make your variables available outside of the object, which we do not want to do!
Two more methods need to be added before we get to our first recursive method. Both of these methods are private methods that perform a Query of Queries (QofQ) and return a query object. While each is vital to the well-being of our object, they are very simple and I am assuming that they do not require explanation beyond the hints that are written into the code.
See Listing 6.
The getDepth() method is the first of our recursive methods that we're going to add into our object. Place the code below into your recursiveObject.cfc file, and then come back and we'll break it down and discuss what the method does.
1<!--- BEGIN getDepth() method ---> 2<cffunction name="getDepth" 3 hint="Returns the numeric value of how many levels deep an element resides in a recursive query 4 (NOTE: a return value of 1 indicates a top-level element, so depending on your purpose, 5 you might need to subtract 1 from the result)" 6 returntype="numeric" output="false" access="public"> 7 <!--- set the method arguments ---> 8 <cfargument name="childID" 9 hint="Provides the ID of the child whose parent is being searched for" 10 required="yes" type="string" /> 11 12 <cfscript> 13 var depth = 1; 14 var qParent = ""; 15 qParent = getParent(arguments.childID); 16 if (qParent[getProperty('childOfField')][1] GT 0) { 17 depth = depth + getDepth(qParent[getProperty('childOfField')][1]); 18 } 19 return depth; 20 </cfscript> 21</cffunction> 22<!--- END getDepth() method --->
So then, what does the getDepth() method do? Well, the summarization of its purpose is found on lines 3 - 5 in the hint attribute of our <cffunction> tag. But let's take a look at how it accomplishes this.
The first thing we need to point out is that the childID argument is required (lines 8 - 10). In respect to our tblCategories table, this is the categoriesID of the category whose parent category we're searching for. So if we're looking for the parent of 'Rules of Conduct', we'll supply a value of 4 to this argument.
Next, on line 15 we send the provided childID off to the getParent() method (listing 6) so we can find the parent category, if there is one. On line 16, we check to see if the 'childOfField' from our qParent QofQ is greater than zero, indicating whether or not a parent was found. If one was, then the 'childOfField' (parent_id) of our returned QofQ will be greater than zero. If no parent was found, the method returns the value of 'depth', which was defaulted to 1 back on line 13.
Line 17 proecesses only if a parent was found, and this is where the magic of the method takes place.
By referring back to the query dump, we can see that 'Rules of Conduct' is two levels deep: 'Membership Information' is its parent, which is on the first level, because it has no parent. So, if our getDepth() method is working properly, it will return a 2 when we provide it with the ID (4) for 'Rules of Conduct'.
Now look closely at line 17. Can you see what makes it so special? HINT: It is the code AFTER the plus sign (+).
getDepth(qParent[getProperty('childOfField')][1])
This little bit of code is what makes this a recursive method. Because we found a parent in line 16, we are going to submit the ID of that parent to the method (in this example, that would be 2). Even though the getDepth() method is what we're running, it needs to run itself again in order to finish calculating the depth. As a matter of fact, it runs itself once for every level of depth of the ID we originally provided. Stated differently, until line 16 returns false (no parent is found), line 17 will continue to process once for each level of depth.
So, knowing that 'Rules of Conduct' is two levels deep, and knowing that line 17 is run once for each level, we can see that the first time the method is run (our original call with the ID of 2), line 17 takes the value of 'depth' (which we set to 1 on line 13) and adds it to the value returned from the getDepth() method after we submit the 'Membership Information' ID (4), which is 1. So, in plain math, in our example, line 17 actually says:
depth = 1 + 1;
Pretty cool stuff! Now that we have our first taste of ColdFusion recursion, let's put it to practical use.
The boss called and said that the client needs a new form online within the hour. This form needs to allow them to add categories to their categories table. The catch? The form needs to give them the ability to select the parent category when they enter the new one. You have two basic options:
You think about it for a moment and decide that you like your job; angering the client poses a significant risk to keeping said job, so forcing them to manually enter everything doesn't seem too logical. The second option, while it might be a serious pain in the rear to go thru the table and hard code all of the options, you feel that it is the lesser of two evils, so you go with it. Just then, that really annoying know-it-all co-worker (you know which one I'm talking about) says: "Yeah, but if you do that, won't you have to add a new option tag for each category that they add?"
As badly as you want to kick the teeth out of the mouth that just uttered that question, you realize that it's true, and you think to yourself, "Geeeeeez, there has to be a better way." And that's when you remember about this tutorial that you read one time; something about some crazy regurgitation stuff. "Hmmmmmm," you say to yourself, "maybe there's a solution in the making?!"
1<!--- BEGIN getRecursiveOptions() method ---> 2<cffunction name="getRecursiveOptions" 3 hint="Returns a string containing the option tags for an option list from a recursive query search" 4 returntype="string" output="false" access="public"> 5 <!--- set the method arguments ---> 6 <cfargument name="parentID" 7 hint="Provides the ID of the parent category to search in" 8 required="yes" type="string" /> 9 10 <cfscript> 11 var strOut = ""; 12 var qSubs = ""; 13 var option = "<option value=""[id]"">[dsp]</option>"; 14 var dsp = ""; 15 var i = 1; 16 var depth = 0; 17 qSubs = getSubs(arguments.parentID); 18 if (qSubs.recordCount GT 0) { 19 for (; i LTE qSubs.recordCount; i=i+1) { 20 depth = getDepth(qSubs[getProperty('parentField')][i]); 21 strOut = strOut & option; 22 strOut = replace(strOut, "[id]", qSubs[getProperty('parentField')][i]); 23 dsp = repeatString(" – ", depth-1) & qSubs[getProperty('dspField')][i]; 24 strOut = replace(strOut, "[dsp]", dsp); 25 strOut = strOut & getRecursiveOptions(qSubs[getProperty('parentField')][i]); 26 } 27 } 28 return strOut; 29 </cfscript> 30</cffunction> 31<!--- END getRecursiveOptions() method --->
For the sake of time, we're going to skip over most of the method and point out just four lines of code: 18, 19, 20, and 25.
Line 18: We simply check to see if any children (subcategories) were returned. Lines 19 - 26 will only process if they were.
Line 19: Creates a loop of lines 20 - 25 that runs as many times as there are children.
Line 20: We send the ID off to the getDepth() method to find out how many levels deep the category that is currently processing resides. If you've forgotten what getDepth() is, take a review.
Line 25: Here's where we regurgitate again, and tell the getRecursiveOptions() method to rerun itself. We pass in the current category's ID as the argument. This line ensures that the method will continue to run until we've found all children of the current category, including grandchildren, great-grandchildren, great-great grandchildren, great-great-great... you get the picture.
You're now sitting there thinking, "Okay, I kind of understand what he's talking about here, but it'd probably make more sense if I saw the results of the method." I couldn't agree more, so let's create a quick index.cfm file so that we can find out exactly what it does.
1<cfquery name="qCats" datasource="#dsn#"> 2SELECT categoriesID, category_name, parent_id 3 FROM tblCategories 4ORDER BY parent_id, category_name; 5</cfquery> 6 7<cfscript> 8 cfcPath = "path.from.wwwroot.to.recursiveObject"; 9 objRecursion = createObject("component", cfcPath).init(qCats, "category_name", "categoriesID", "parent_id"); 10</cfscript> 11 12<cfoutput> 13 14<select name="mySelect"> 15 #objRecursion.getRecursiveOptions(0)# 16</select> 17 18</cfoutput>
IMPORTANT! Be sure to set the cfcPath variable (line 8) to the correct path on your box to where you stored your recursiveObject.cfc file. For instance, if you store the recursiveObject file in a directory named 'recursive' under the localhost root, you would set this to: "recursive.recursiveObject".
Line 9: create the object. Notice that we have concatenated the .init() method to the end of the createObject() function. Inside the init() method call is where we are passing the required arguments to the object: our query, the name of our display field, the name of our parent field, and the name of our childOf field. Once we've done this, we can now call on any of its public methods.
Line 15: By passing a zero as an argument, we'll be returned an option list containing every single category in the table. That's how incredibly easy our recursive object makes it to setup our select box on the new form for our client! If you want to list only categories that fall under the 'Member Information' category, change the zero to a 2. The number that you pass to the method can even be set dynamically, but I'll leave that part up to you to decide!
Another example of a real-world problem that is solved by recursion is breadcrumbs. How many times have you hard-coded your breadcrumbs into the page? Or used seemingly a million-and-one <cfif> and <cfelseif> statements to build them? Thanks to our getBreadcrumbs() method you no longer nee
matt this is a great tutorial. this scenario is one i have always struggled with and tried to avoid with crappy coding!!! i don't comment enough on tutorials - so now i will start. awesome work. mike
Thanks for the compliment! I'm not 100% sure what precisely you're looking for in regards to formatting as XML, but I'd like to. How about posting on the EasyCFM forums so that I/we can better define and answer the question! :-)
This is a great tutorial! Would you consider showing how to format this as xml?
megan, Sorry about the delay in my response. For some reason the notification emails didn't come to my inbox, but went to the trash can? Weird. Anyways, I'm going to go over it tonight and post back the modification(s). :-)
I should have added to my previous question this example, for instance if I was creating a directory type area and I only wanted to create links to top level (parentID is 0) and their immediate children (no children's children). I know how to do that using a regular query, but how would I do that using this cfc or how would I modify the code to add that kind of new function (I am just getting started learning cfc's - better late than never ;P) would be grateful for any hints and/or instruction thanks, ~megan
Hi Matt - I understand how I can Start my list anywhere along the line/levels, but what if I wanted to limit the number of levels down , for instance maybe have an option list with only items whose parent id is 2 but none of their children or only their first level children and none of the grand children ??? thanks for any hints you can offer, megan
Thank you for finishing this up and submitting it. I know I will be using this a lot at work. You did an awesome job on the tutorial. Thanks Again!! Matthew - dduck1934
I thought this was a Wonderful! tutorial - indepth, but fun and easy to read. You did an excellent job Matt :D - (even if you hadn't said nice things about me, I would still think so =P) BTW. If I hadn't mentioned the single table parentID thing, someone else was bound to. You did the work and took a basic idea and turned it into something multidemensional (ha ha!) thanks for writing this! megan