ID-PID-Struktur abbilden mit CF9-ORM

Mischa Sameli, Geschäftsführer & Leiter Entwicklung

Zum Aufbau von Navigationsbäumen verwenden wir in der Regel Datenbank seitig eine ID-PID-Struktur. Wie lässt sich die Beziehung ID-ParentID mit dem Hibernate-ORM von ColdFusion 9 elegant abbilden?

Gehen wir von einer einfachen Datenstrukur aus, mit der wir unsere Navigation abbilden möchten. Dazu benötigen wir minimal und stark vereinfacht folgende Eigenschaften oder Datenbankfelder:

  • ID
  • PID
  • bezeichnung

Beginnen wir die Navigation auf der Startebene, auf welcher sich der Eintrag Porträt befindet. Dieser besitzt die ID=1 und PID=0. Der Eintrag besitzt kein Elternelemt und erhält deshalb die PID 0. Unterhalb des  Porträts setzen wir die Menüpunkte Lage, Geschichte und Kontakt. Dann erhalten wir folgende Tabelle:

ID PID bezeichnung
1 0 Porträt
2 1 Lage
3 1 Geschichte
4 1 Kontakt

Wenn wir nun eine Navigation ausgeben wollen, beginnen wir bei der PID=0

1....
2<!--- Aufruf der Navigations-Ausgabe --->
3#buildNavigation()#
4...
5<!--- Navigations-Aufbereitung --->
6<cffunction name="buildNavigation" output="true" access="public" returntype="void">
7 <cfargument name="PID" default="0" />
8    <cfset var local = StructNew() />
9    <cfquery name="local.qry" datasource="cms">
10        SELECT * FROM navigation
11        WHERE PID = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.PID#">
12    </cfquery>
13    <cfif local.qry.RecordCount>
14        <cfoutput>
15            <ul>
16                <cfloop query="local.qry">
17                <li><a href="/go.cfm?item=#local.qry.id#">#local.qry.bezeichnung#</a>#buildNavigation(pid=local.qry.id)#</li>
18                </cfloop>
19            </ul>
20        </cfoutput>
21    </cfif>
22</cffunction>

Die Navigation funktioniert, beinhaltet aber in dieser Version einen unter Umständen nicht unwesentlichen Nachteil: auf der aktuellen Ebene besitzt man noch keine Information über allfällig vorhandene Unterobjekte. Dazu müsste eine zweite Abfrage durchgeführt werden, wzu es natürlich verschiene Lösungsansätze gibt, auf die ich an dieser Stelle nicht näher eingehen möchte. Dafür aber auf die Version mit Hibernate von ColdFusion 9. Dafür legen wir zuerst ein Objekt an:

1<--- navigation.cfc --->
2
3<cfcomponent persistent="true" entityname="navigation">
4    <!--- Primary definieren --->
5    <cfproperty name="id" type="id" generator="uuid" />
6    <!--- alle Eigenschaften --->
7    <cfproperty name="pid" type="numeric" hint="Parent-ID bei mehrstufigen Navigationen" />
8    <cfproperty name="bezeichnung" type="string" />
9    <!--- Verknuepfung zu Unterelementen --->
10    <cfproperty name="subitems"
11                fieldtype="one-to-many"
12                fkcolumn="id"
13                id="pid"
14                cfc="navigation"
15                lazy="true"
16                insert="false"
17                update="false" />
    
18</cfcomponent>

Mit der property subitems haben wir die logische Beziehung ID-PID gleich als Eigenschaft definiert und können beim Auslesen der Element auch auf diese Untereigenschaften zugreifen:

1<!--- Alle Objekte der Startebene laden --->
2<cfset aryNavigation = EntityLoad("navigation", {pid=0}) />
3<!--- Navigation erstellen lassen mit den Objekten --->
4#buildNavigation(elements=aryNavigation)#
5...
6<!--- Funktion zur Erstellung der Navigation --->
7<cffunction name="buildNavigation" output="true" access="public" returntype="void">
8 <cfargument name="elements" default="#ArrayNew(1)#" />
9    <cfset var local = StructNew() />
10
11    <cfif ArrayLen(arguments.elements)>
12        <cfoutput>
13            <ul>
14                <cfloop array="#arguments.elements#" index="#local.navItem#">
15                <li><a href="/go.cfm?item=#local.navItem.getId()#">#local.navItem.getBezeichnung()#</a>
16                <cfif ArrayLen(local.navItem.getSubitems())>
17                    #buildNavigation(elements=local.navItem.getSubitems())#
18                </cfif>
19                </li>
20                </cfloop>
21            </ul>
22        </cfoutput>
23    </cfif>
24</cffunction>

Wie man sieht, konnte das Auslesen der Daten komplett aus der Funktion entfernt werden. Die Methode EntityLoad() wird nur einmal aufgerufen. Die Daten/Objekte, die wir dadurch enthalten, leiten wir der Funktion zur rekursiven Aufbereitung der Ausgabe weiter. Und wir haben mit dieser Variante immer die Möglichkeit, ein wenig vorausschauend zu agieren, da wir bereits Zugriff auf mögliche subitems haben.