3

This seems like a pretty common task I want to do, but I can't wrap my head around the cfloops and cfqueries.

I have a database full of photo galleries. They all have an ID and a ParentID (except the root galleries - their ParentID is blank), and they can go multiple levels deep. Here's an example of the gallery structure:

  • 1
    • 1-1
    • 1-2
  • 2
    • 2-1
      • 2-1-1
      • 2-1-2
    • 2-2

I want to automatically output this structure above into nested ul's and li's (just like above). How is that done? I can't wrap my head around the placement of the ul's and li's to display the hierarchy levels correctly.

Michael
  • 2,546
  • 2
  • 20
  • 26
  • You can actually do this entirely within your database query with a stack, if I can see your table structure and the database that you're using I can tell you how. Basically the query will return that result set in order, along with a column that tells you the level deep the item is. – Nate Jul 12 '11 at 01:35
  • possible duplicate of [adjacency list in coldfusion](http://stackoverflow.com/questions/6447488/adjacency-list-in-coldfusion) – Henry Jul 13 '11 at 08:30

5 Answers5

1

I guess the simpliest solution would be to use Query of Query...

If the dataset is huge, try <cftree> and populate the leaves on demand.

Henry
  • 32,689
  • 19
  • 120
  • 221
1

I would use a query of queries to do this recursively.

Note: the code below is NOT tested, so please treat is as example psuedocode.

<cfquery query="qReadAllData">
  select * from your_table
</cfquery>

<!--- Read all roots (no parent ID) --->
<cfquery query="qReadRoots" dbtype="query">
  select nodeID from qReadAllData
  where parentID is null
</cfquery>

<ul>
  <cfloop query="qReadRoots">

    <cfset processNode(qReadRoots.nodeID) />

  </cfloop>
</ul>


<cffunction name="processNode" output="true">
  <cfargument name="nodeID" type="any" />

  <!--- Check for any nodes that have *this* node as a parent --->
  <cfquery query="LOCAL.qFindChildren" dbtype="query">
    select nodeID from qReadAllData
    where parentID = #ARGUMENTS.nodeID#
  </cfquery>

  <cfif LOCAL.qFindChildren.recordcount>

    <!--- We have another list! --->
    <li>
      <ul>
        <!--- We have children, so process these first --->
        <cfloop query="LOCAL.qFindChildren">

          <!--- Recursively call function --->
          <cfset processNode(LOCAL.qFindChildren.nodeID) />

        </cfloop>

      </ul>
    <li>


  <cfelse>

     <!--- We have no more children, so we just output the value --->
     <li>#nodeID#<li>

  </cfif>

</cffunction>

It's late. I'm tired. I hope this is right :)

Ciaran Archer
  • 12,316
  • 9
  • 38
  • 55
  • He is using `cfquery dbtype=query` – Leigh Jul 12 '11 at 03:19
  • I redact then, but still don't like looping over a query, even a QoQ – Nate Jul 12 '11 at 05:54
  • @Nate: just as well it's not you asking the question. There was no restriction on using QofQ, so this is a legitimate approach. This approach actually creates HTML lists - which is what the question writer asked for. – Ciaran Archer Jul 12 '11 at 06:53
  • Thank you! I ran into a problem though: my boss set up the database so the ParentID of all root photo galleries aren't null; their ParentID = PhotoGalleryID. So I changed the ReadRoots query to 'WHERE PhotoGalleryID = ParentID', and that works, however my problem occurs with the FindChildren query. I assumed I needed to change this to 'WHERE ParentID = #Arguments.PhotoGalleryID# AND ParentID <> PhotoGalleryID', but that only loops until the bottom one of the branches of the tree and then gives me a query of queries error: Encountered "ParentID = AND. Incorrect conditional expression... – Michael Jul 12 '11 at 19:48
1

This is a mix of both SQL and Coldfusion. It's probably not the best to format the label in SQL Server, but it does give the desired format.

SQL:

CREATE TABLE testTable(id int, parentID int)
INSERT INTO testTable(id, parentID) VALUES
    (1, NULL)
    , (2, 1)
    , (3, 1)
    , (4, NULL)
    , (5, 4)
    , (6, 5)
    , (7, 5)
    , (8, 4)

Coldfusion:

<cfscript>
    qs = new query();
    qs.setDatasource("datasource");
    qs.setSQL("
        ;WITH cte AS
        (
            SELECT t.ID, t.parentID, 1 AS level, 
                CAST(DENSE_RANK() OVER (PARTITION BY t.parentID ORDER BY t.ID) AS varchar(max)) AS label
            FROM testTable t
            WHERE parentID IS NULL

            UNION ALL 

            SELECT t.ID, t.parentID, cte.level + 1 AS level, 
                CAST(cte.label AS varchar(max)) + ' - ' + CAST(DENSE_RANK() OVER (PARTITION BY t.parentID ORDER BY t.ID) AS varchar(max)) AS label
            FROM testTable t
                INNER JOIN cte ON cte.ID = t.parentID
        )
        SELECT *,
            DENSE_RANK() OVER (PARTITION BY parentID ORDER BY ID) AS [order]
        FROM cte
        ORDER BY label
    ");
    qMenu = qs.execute().getResult();

    oldLevel = 0;

    for (i=1;i<=qMenu.recordCount;i++){        
        if (qMenu.level[i] > oldLevel) {
            WriteOutput("<ul>");
        }

        while (qMenu.level[i] < oldLevel) {
            WriteOutput("</ul>");
            oldLevel--;
        }

        WriteOutput("<li>" & qMenu.label[i] & "</li>");

        oldLevel = qMenu.level[i];

    }

    do {
        WriteOutput("</ul>");
        oldLevel--;
    } while (oldLevel > 0);
</cfscript>
Wes
  • 11
  • 1
0

Here's what you need :

T-SQL Stored Procedure for Looping a hierarchy with recursion

Let me know if you need help implementing it with your table

Update: See new answer with query to update with depth for easy select / output.

Community
  • 1
  • 1
Nate
  • 2,881
  • 1
  • 14
  • 14
  • Yipes. No need to resort to cursors. If recursion is *really* necessary, processing the data in sets would be more efficient. Though there may be simpler options like CTE's depending on the db and structure. – Leigh Jul 12 '11 at 03:14
  • "data in sets"? That hierarchy stack method is extremely fast and efficient and has no depth limitations (sp's called recursively do). – Nate Jul 12 '11 at 05:53
  • Huh? The procedure in your link *is* called recursively. It also uses procedural row by row processing (ie cursors). Refactoring to use set based processing is more efficient and would not require recursion. – Leigh Jul 12 '11 at 11:39
  • RE comment *Ignore the title of the question page, its not true recursion* It is. The proc calls itself ... row by row ;) – Leigh Jul 12 '11 at 14:48
  • Yeah, i missed the exec, you can do it without a call to the proc. I thought the question had was a standard stack implementation, which you can see here: http://support.microsoft.com/kb/248915 – Nate Jul 12 '11 at 14:52
  • Now that is set based processing ;) I remember using similar routines on ms sql 2000. Much better than cursors (aka the tool of the desperate IMO ;). – Leigh Jul 12 '11 at 14:59
  • Try not to delete whole chunks of answers when they already have related comments. If deleting content will cause multiple comments to "stop making sense", append the changes mark them as an update instead. – Leigh Jul 12 '11 at 15:15
0

Here you go, if you give the actual data structure I'll alter to match your actual fields :

You can add a depth and lineage(fullPath) column to your table, then you can just do one simple select to get the heirarchy and output it accordingly. One query, done.

WHILE EXISTS (SELECT * FROM Tree WHERE levelDeep Is Null) 
UPDATE T SET T.levelDeep = P.levelDeep + 1, 
T.fullPath= P.fullPath+ Ltrim(Str(T.ParentNode,6,0)) + '/' 
FROM Tree AS T 
INNER JOIN Tree AS P ON (T.ParentNode=P.Node) 
WHERE P.levelDeep >=0 
AND P.fullPathIs Not Null 
AND T.levelDeep Is Null
Nate
  • 2,881
  • 1
  • 14
  • 14