Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
Home
Discussion GroupsFormsForms ProgrammingQueriesModules / DAO / VBAReports / PrintingMacrosDatabase DesignSecurityConversionImporting / LinkingSQL Server / ADPMultiuser / NetworkingReplicationSetup / ConfigurationDeveloper ToolkitsActiveX ControlsNew UsersGeneral 1General 2
Access DirectoryToolsTutorialsUser Groups
Related Topics
SQL ServerOther DB ProductsMS OfficeMore Topics ...

MS Access Forum / Queries / July 2006

Tip: Looking for answers? Try searching our database.

Joe Celko's Nested sets

Thread view: 
Enable EMail Alerts  Start New Thread
Thread rating: 
Paolo - 28 Jul 2006 14:18 GMT
Hi I've been banging my head with this for some time.

I'm converting Joe Celko's nested sets into Access, and I have it
working, but it's so inelegant, I'm convinced there's got to be a
better solution.  Basically a nested set is a hierarchical
representation for relational databases.  There's not much need to
understand other than each record has two field named 'left' and
'right' which determine where the record falls in a hierarchy and that
Element ID is a primary key.  Let's say I have 3 fields in tblElements:

ElementID     Left     Right

The top record alone would look like this:

Parent        1        2

If I were to add a child record, it would look like this:

Parent     1      4
Child     2     3

In SQL to add a record I can do this:

-----------------------------------------------------------------------------------------------------
1)  SELECT Left, Right
FROM tblElements
INTO tblIsolateElementAdd
WHERE ElementID= 'parent element as determined by the value of some
form control'

2)  UPDATE tblElements, tblIsolateElementAdd
SET tblElements.Left = tblElements.Left+2
WHERE tblElements.Left>tblIsolateElementAdd.Right

3)  UPDATE tblElements, tblIsolateElementAdd
SET tblElements.Right = tblElements.Right+2
WHERE tblElements.Right >= tblIsolateElementAdd.Right

4)  UPDATE tblIsolateElementAdd
SET ElementID ='value of some control', [Left] = [Right], [Right] =
[Right] + 1, [Right] = [Right] + 1,

5)  INSERT INTO tblElements (ElementID, [Left], [Right])
SELECT ElementID, [Left], [Right]
FROM tblIsolateElementAdd

6)  DROP TABLE tblIsolateElementAdd
------------------------------------------------------------------------------------------
As you can see I am using Cartesian joins and well as creating a
temporary table.  Indeed, I could have created a one record recordset
(snapshot) using DAO to do avoid the cartesian joins and the temporary
table (pseudocode):
--------------------------------------------------------------------------------------------
Set rs = Db.OpenRecordset("SELECT Left, Right FROM " & _
       "tblElements WHERE ElementID ='" & parent element as determined
by the value of
        some form control & "'", dbOpenSnapshot)

UPDATE tblElements
SET [Left] = [Left]+2
WHERE [Left]>rs!Left

UPDATE tblElements
SET [Right] = [Right]+2
WHERE [Right] >= rs!Right

INSERT INTO tblElements (ElementID, [Left], [Right])
VALUES 'value of some control', rs!Right, rs!Right+1
------------------------------------------------------------------------------------------------
Is the latter solution the way to go?  Or is there a simpler solution?
What about avoiding the SQL altogether and just using DAO?  Does that
sound like a better way to go?

Thanks in advance,

Paolo

paolo@NOSPAMcornacchia.net, eliminating NOSPAM
Michel Walsh - 30 Jul 2006 14:00 GMT
Hi,

Personnaly, I recreate the nested set representation from scratch when I
need it. Sure, my scenario is that there is no "light" changes, just major
ones, so it does not make sense to just add or delete one node (in my cases)
at a time. Here is a possible way to do it:

=======================================
Option Compare Database
Option Explicit

Private Const MyName As String = "NestedSets"

Private Const errParentTable As String = "Parent Table in error."
Private Const errParentTableKey As String = "Specified 'node' field in
Parent Table has null and so can't be use for primary key."
Private Const errNoRoot As String = "The Parent table has no identifiable
root node; fix and submit again."
Private Const errNoUniqueRoot As String = "The Parent table has more than
one possible root; fix and submit again."
Private Const errCantCreate As String = "Cannot create the nested set table
("
Private Const errCantInsert As String = "Cannot insert the nodeID "
Private Const errUnknownParent As String = "At least one ParentID is unknown
as NodeID in the supplied table."
Private Const errUnusedRecords As String = "Not all the records from the
table have been used."

Private db As Database          ' To avoid using CurrentDb each time
Private OpeningString As String    ' string to open the recordset with all
children
Private InsertInto As String    ' string to insert a record
---------------------------
Private Sub RaiseError(ByVal Desc As String, Optional ErrNumber As Long =
513)
   Err.Raise vbObjectError + ErrNumber, MyName, Desc
End Sub
-----------------------------
Public Sub FromNestedToParent(ByVal NestedTable As String, _
       ByVal ParentTableName As String, _
       ByVal NodeFieldName As String, _
       ByVal ParentFieldName As String)

Dim db As Database:   Set db = CurrentDb
   On Error Resume Next
   db.Execute "DROP TABLE " & ParentTableName
   Err.Clear

   db.Execute "SELECT c.NodeID as " & NodeFieldName & _
       ", p.NodeID As " & ParentFieldName & _
       " INTO " & ParentTableName & _
       " FROM " & NestedTable & " AS c LEFT JOIN " & NestedTable & " AS p "
& _
       " ON (c.lft BETWEEN p.lft AND p.rgt)  AND  c.lvl = p.lvl+1 ",
dbFailOnError

   If 0 <> Err.Number Then
       RaiseError Err.Description, Err.Number
   End If
   Debug.Assert 0 = Err.Number
End Sub
---------------------------------
Public Sub FromParentToNested(ByVal ParentTable As String, _
       ByVal NodeID As String, _
       ByVal ParentID As String, _
       ByVal NestedSet As String)

Dim nCount As Long

   ' Check if the table exists, and if the fields NodeID and ParentID exist
   On Error Resume Next
   DCount "*", ParentTable, NodeID & "=" & ParentID
   If 0 <> Err.Number Then
       RaiseError errParentTable
       Exit Sub
   End If
   ' Check if there are NULL under the nodeID
   If 0 <> DCount("*", ParentTable, NodeID & " IS NULL") Then
       RaiseError errParentTableKey
       Exit Sub
   End If

   Set db = CurrentDb

   If 0 <> db.OpenRecordset("SELECT COUNT(*) FROM (SELECT * FROM " &
ParentTable & " AS a LEFT JOIN " & _
       ParentTable & " AS b ON a." & ParentID & "= b." & NodeID & _
       " WHERE (NOT a." & ParentID & " IS NULL)  AND b." & NodeID & " IS
NULL)").Fields(0).Value Then
       RaiseError errUnknownParent
       Exit Sub
   End If

   ' Create the nested set table. Its three fields are NodeID, lft and rgt.
   db.Execute "DROP TABLE " & NestedSet: Err.Clear
   ' We tried to drop a table, maybe it was not there... not important...

   db.Execute "CREATE TABLE " & NestedSet & _
           "(NodeID LONG CONSTRAINT PrimaryKey PRIMARY KEY," & _
           " lft LONG NOT NULL CONSTRAINT UniqueLft UNIQUE, " & _
           " rgt LONG NOT NULL CONSTRAINT UniqueRgt UNIQUE, " & _
           " lvl LONG NOT NULL );  "

       'In theory, we can compute the level, lvl, with a query, but there
is
       ' nothing wrong in storing it in the table too.

   If 0 <> Err.Number Then
       RaiseError errCantCreate & Err.Description & ")."
       Exit Sub
   End If

   ' Find the root, the node with a Null as ParentID, or the one with
itself.
Dim root As Long
   Select Case DCount("*", ParentTable, NodeID & "=" & ParentID)

   Case 0
       ' There is no node where NodeID=ParentID... look for a Parent Is
Null
       Select Case DCount("*", ParentTable, ParentID & " IS Null")
       Case 0
           ' There is no node where Parent Is Null, ... error...
           RaiseError errNoRoot
           Exit Sub

       Case 1
           ' there is just one such node, perfect...
           root = DLookup(NodeID, ParentTable, ParentID & " IS NULL")

       Case Else
           ' there is more than one node with a parent Is Null... what to
do?
           RaiseError errNoUniqueRoot
           Exit Sub
       End Select

   Case 1

       ' There is just one node where parentID=NodeID... what about
       ' a node with a NULL parent???
       If 0 <> DCount("*", ParentTable, ParentID & " Is Null") Then
           RaiseError errNoUniqueRoot
           Exit Sub
       End If

       root = DLookup(NodeID, ParentTable, ParentID & "=" & NodeID)

   Case Else

       ' We have more than one node with ParentID=NodeID, someone has to
fix it...
       RaiseError errNoUniqueRoot
       Exit Sub

   End Select

   If 0 <> Err.Number Then
       RaiseError errCantInsert & root
       Exit Sub
   End If

   '  Prepare the recursion
   InsertInto = "INSERT INTO " & NestedSet & "(NodeID, lft, rgt, lvl)
VALUES("
   OpeningString = "SELECT " & NodeID & " FROM " & ParentTable & " WHERE "
& ParentID & "="

Dim counting As Long
   counting = 2

   'Make some nasty recursion about calling for each of the children
   CallChildren root, counting, 2

   ' Append the root...
   db.Execute InsertInto & root & ", 1, " & counting & ", 1 ); "

   'Job is done... we can alter the rgt field so it doesn't accept NULLs
anymore
   ' but that is not 'strictly' required...

   db.Execute "CREATE INDEX level ON " & NestedSet & "(lvl)"

   If counting <> 2 * DCount("*", ParentTable) Then
       RaiseError errUnusedRecords
       Exit Sub
   End If

   If 0 <> Err.Number Then
       RaiseError Err.Description, Err.Number
       Exit Sub
   End If

End Sub
-------------------------------------------
Private Sub CallChildren(ByVal ParentNodeID As Long, ByRef counting As Long,
ByVal level As Long)

Dim rst As DAO.Recordset
Dim opening As Long ' the current worm counter, the lft value
                   ' for a given node
   On Error Resume Next
   ' Open a recordset will al the children of the given parent...
   Set rst = db.OpenRecordset(OpeningString & ParentNodeID,
dbOpenForwardOnly, dbReadOnly)

   ' For each, remember the lft, call the children, then having the rgt
value, insert the record.
   Do Until rst.EOF

       opening = counting  ' get a copy, that is our rgt value.
           ' we can't insert the whole record,now, since we ignore the rgt
value

       ' We need to increase the running worm counter...
       counting = counting + 1

       CallChildren rst.Fields(0).Value, counting, level + 1 ' recursive
call on all the actual children

       db.Execute InsertInto & rst.Fields(0).Value & ", " & opening & ", "
& counting & ", " & level & ") ;"

       ' Increase the running worm counter
       counting = counting + 1
       rst.MoveNext

   Loop
   Debug.Assert 0 = Err.Number
End Sub

==========================================

Hoping it may help,
Vanderghast, Access MVP

> Hi I've been banging my head with this for some time.
>
[quoted text clipped - 74 lines]
>
> paolo@NOSPAMcornacchia.net, eliminating NOSPAM
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.