SQL 和 Delphi:从表中创建树的递归机制

Posted

技术标签:

【中文标题】SQL 和 Delphi:从表中创建树的递归机制【英文标题】:SQL and Delphi: recursive mechanism for creating a tree from a table 【发布时间】:2013-03-31 23:22:36 【问题描述】:

我正在使用的 DBMS 是 mysql,编程环境是 Delphi 7(这对于本示例并不重要)。

我有一个名为“主题”的表,我将所有书籍主题存储在系统中。科目可以有亲子关系,比如科学可以分为数学和物理,而数学可以细分为微积分、代数、几何等等。

我想要的是创建一个填充了该表中日期的树。请帮我这样做。甚至你使用什么语言来进行说明都无关紧要,它可以是伪代码。

Subject 表的数据库图如下所示:

主题表定义:

DROP TABLE IF EXISTS subject;
CREATE TABLE IF NOT EXISTS subject (                  # Comment
    subject_id  INT UNSIGNED NOT NULL AUTO_INCREMENT, # Subject ID
    subject     VARCHAR(25)  NOT NULL,                # Subject name
    parent_id   INT UNSIGNED     NULL DEFAULT NULL,   # Parent ID as seen from
    PRIMARY KEY (subject_id),                         # the diagram refers to
    UNIQUE (subject),                                 # the subject_id field
    INDEX (parent_id),
    CONSTRAINT fk_subject_parent
    FOREIGN KEY (parent_id)
        REFERENCES subject (subject_id)
            ON DELETE RESTRICT
            ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

用一些虚拟数据填充主题表:

INSERT INTO subject (subject, parent_id) VALUES
                    ('Science',    NULL),
                    ('Mathematics',   1),
                    ('Calculus',      2),
                    ('Algebra',       2),
                    ('Geometry',      2),
                    ('Languages',  NULL),
                    ('English',       6),
                    ('Latin',         6);

SELECT 语句返回这个:

SELECT * FROM subject;
╔════════════╦═════════════╦═══════════╗
║ subject_id ║   subject   ║ parent_id ║
╠════════════╬═════════════╬═══════════╣
║          1 ║ Science     ║      NULL ║
║          2 ║ Mathematics ║         1 ║
║          3 ║ Calculus    ║         2 ║
║          4 ║ Algebra     ║         2 ║
║          5 ║ Geometry    ║         2 ║
║          6 ║ Languages   ║      NULL ║
║          7 ║ English     ║         6 ║
║          8 ║ Latin       ║         6 ║
╚════════════╩═════════════╩═══════════╝

存储过程:

DELIMITER$$

DROP PROCEDURE IF EXISTS get_parent_subject_list;
CREATE PROCEDURE get_parent_subject_list ()
BEGIN
    SELECT subject_id, subject
    FROM subject
    WHERE parent_id IS NULL
    ORDER BY subject ASC;
END$$


DROP PROCEDURE IF EXISTS get_child_subject_list;
CREATE PROCEDURE get_child_subject_list (IN parentID INT)
BEGIN
    SELECT subject_id, subject
    FROM subject
    WHERE parent_id = parentID
    ORDER BY subject ASC;
END$$

DELIMITER ;

接下来是我的 Delphi 程序,它试图用数据填充树视图,但可以进一步看出,它不能比第二层更深:

procedure TForm1.CreateSubjectTreeView(Sender: TObject);
var
    i : integer;
begin
    i := 0;

    q1.SQL.Clear;
    q1.SQL.Add('CALL get_parent_subject_list()');
    q1.Open;
    q1.First;

    while not q1.EOF do
    begin
        TreeView.Items.Add(nil, q1.Fields[1].Value);

        q2.SQL.Clear;
        q2.SQL.Add('CALL get_child_subject_list(' +
                    VarToStr(q1.Fields[0].Value) + ')');
        q2.Open;
        q2.First;

        while not q2.EOF do
        begin
            TreeView.Items.AddChild(TreeView.Items.Item[i], q2.Fields[1].Value);
            q2.Next;
        end;

        i := TreeView.Items.Count;
        q1.Next;
    end;
end;

这就是这段 sn-p 代码的作用:

+- Science
|   |
|   +- Mathematics
|
+- Languages
    |
    +- English
    +- Latin

但我希望它看起来像这样:

+- Science
|   |
|   +- Mathematics
|       |
|       +- Calculus
|       +- Algebra
|       +- Geometry
|
+- Languages
    |
    +- English
    +- Latin

【问题讨论】:

+1 表示格式正确的问题 mysql 解决方案我帮不了你,但你要找的是分层查询,可以在这里找到一个例子:explainextended.com/2009/03/17/hierarchical-queries-in-mysqlmysql 没有 connect by 子句,所以你必须手动完成。 【参考方案1】:

我建议你不要一次加载整棵树,为什么要加载?目前没有人可以查看一千个项目。它可能很长,您的程序看起来会冻结。而且它会在网络和服务器上产生巨大的负载。

您最好使用VirtualTreeView 方法,其中每个项目都根据请求加载其子项目。这将需要一个参数化的准备查询,例如

 Select ID, Title, This, That from TREE where Parent_ID = :ID
http://code.google.com/p/virtual-treeview/ http://www.lischke-online.de/index.php/controls/virtual-treeview-gallery Tree-like Datastructure (for use with VirtualTreeview) VirtualStringTree Correct/recommended use

是的,不要为每个项目都创建新的 SQL 文本。它既危险又缓慢(您需要删除为旧请求收集的所有数据并解析新请求)

您应该进行一个参数化查询,Prepare 它,然后关闭/更改参数值/打开。

在http://bobby-tables.com/查看原因和Delphi示例


“一次全部加载”rush 的一个例子是 dynamically create popup menu tree from sql server table in Delphi - 虽然我不认为 rush 对于或多或少的大树来说是个好方法。

注意这种方法:您填充根元素,然后找到一种或另一种方法来填充尚未填充但已被其他人引用的元素直到终于没有这样的元素了。

当然,您可以递归地执行此操作,将树遍历到其末端 - 但这需要许多嵌套的数据库查询。

您可以发出递归 SQL 请求,但它可能非常依赖服务器,并且 RDBMS 引擎通常会限制递归深度。

一种方法可能在树控制上稍差一些,但在 RDBMS 上更简洁、更容易,那就是为刚刚添加的树项创建一个专用的TQueue。在你加载了一些元素之后——最初都是根元素——你在队列中记住了它。然后你从队列中一个接一个地删除并填充(加载和入队)它的孩子。直到队列变空。

【讨论】:

【参考方案2】:

我喜欢使用哈希表来创建由 keyID 索引的所有节点的索引,并使用它来构建树。 它需要 2 次通过表。第一遍为每条记录创建一个根树节点 并针对树节点添加 keyID 的哈希条目。第二遍遍历表,在哈希中查找 parentId。如果找到它,则将当前节点移动到父节点下,否则忽略它。在第二遍结束时,您已经构建了完整的树。

    var i,imax,ikey,iParent : integer;
        aNode,aParentNode : TTreeNode;
        aData : TMyData;
        aContainer : TSparseObjectArray; // cDataStructs , delphi fundamentals
        aNodeIndex : TSparseObjectArray; // delphi 7
    begin
      try
        aContainer := TSparseObjectArray.Create(true);
        aNodeIndex := TSparseObjectArray.Create(False);
        imax := 10000;
        // create test data;
        for i := 1 to imax do
        begin
          aData := TMyData.Create;
          aData.iKey := i;
          aData.iParent := Random(imax); // random parent
          aData.Data := 'I:' + IntToStr(aData.iKey);
          aContainer.Item[i] := aData;
        end;

        tv1.Items.Clear;
        tv1.Items.BeginUpdate;
        // build tree
        // First Pass - build root tree nodes and create cross ref. index
        for i := 1 to imax do
        begin
          aData := TMYData(aContainer.Item[i]);
          aNode := tv1.Items.AddChild(nil,aData.Data);
          aNodeIndex.Item[aData.iKey] := aNode;
        end;
        // Second Pass - find parent node using index and move node
        for i := 1 to imax do
        begin
          aData := TMYData(aContainer.Item[i]);
          aNode := TTreeNode(aNodeIndex.Item[aData.iKey]);
          if aNodeIndex.HasItem(aData.iparent)
          then begin
                 aParentNode := TTreeNode(aNodeIndex.Item[aData.iparent]);
                 aNode.MoveTo(aParentNode,naAddChild);
               end;
        end;
        tv1.Items.EndUpdate;
        tv1.Select( tv1.Items.GetFirstNode);
      finally
        aContainer.Free;
        aNodeIndex.free;
      end;
  end;

【讨论】:

Windows 内置树控件中的“移动”有多快? 由于控件想要滚动到当前节点,一旦树节点超出窗口区域,MoveTo 就会很慢。我通常使用这种方法在自定义数据库中构建数据树。使用我构建的名为 getTreeBranch 的例程在较小的集合中公开实际的树视图,该例程已经构建了树结构,但只是遍历所有元素的当前分支。 我同意对于一些自定义的指针链接树数据结构,您的方法可能很容易成为最快的方法。但是 TS 想使用股票 TTreeView 并且您触摸它的次数越少,它的效果就越好;) 我使用我的方法和默认的 TTreeView 实现了一个包含 10000 个随机树元素的测试程序。在windows 7、Intel i5-2500 cpu下完成耗时8s或0.8ms/node。这还不错,但使用 VirtualTreeView 会快得多。有趣的是,将元素添加为根节点只需要 8 秒中的 0.5 秒,所以如果我可以先遍历内存树并将树节点添加到 TTreeView,理论上应该不到一秒。【参考方案3】:

procedure TdfmMed.Button1Click(Sender: TObject);
var
    NodePai : TTreeNode;
         procedure MontaFilho(Node : TTreeNode; Cod : integer);
         var
            qry : TFDQuery;
            node1 : TTreeNode;
         begin
            qry := TFDQuery.Create( nil );
            qry.Connection := dm1.FDConnection1;
            qry.close;
            qry.SQL.Add('SELECT cod, nome_grupo FROM teste WHERE parent_cod = :cod ORDER BY nome_grupo ASC');
            qry.ParamByName('cod').AsInteger := cod;
            qry.Open();
            qry.First;
            while not qry.EOF do
            begin
                node1 := TreeView1.Items.AddChild(NODE, qry.Fields[1].Value);
                MontaFilho(node1, qry.Fields[0].Value );

                qry.Next;
            end;
         end;
begin
    TreeView1.Items.Clear;

    qryGrupoPai.close;    qryGrupoPai.Open;

    qryGrupoPai.First;
    while not qryGrupoPai.EOF do
    begin
        NodePai := TreeView1.Items.Add(nil, qryGrupoPai.Fields[1].Value);
        MontaFilho( NodePai, qryGrupoPai.Fields[0].Value); 

        qryGrupoPai.Next;
    end;
end;

【讨论】:

一些解释会很好地伴随代码转储并解释为什么它解决了 OP 的问题。【参考方案4】:

我在 *** en español Consumir menu del sql server 上写了一个示例,可能对其他人有用。

它使用几个字段:

元素 ID 的 ID 父 ID 的 PID 要执行的命令的名称 树节点标题的CAPTION ISVISIBLE 以了解此元素是否对最终用户可见 (Y/N)。

它适用于任何菜单级别,旨在通过使用 TDataSource 作为参数与任何数据库一起使用

type
    tElementoMenu = Class(TObject)
      Comando : String;
      //Nombre : String;
      ID : String;
    End;
...
procedure TForm1.CrearMenuDeArbol(dsOrigen: TDataSource; CampoID, IDPadre,
  CampoComando, CampoCaption, CampoVisible: String; Raiz : TTreeNode = Nil);
var
  RamaActual, PrimeraRama : TTreeNode;
  ElementoMenu : TElementoMenu;
  iIndiceImagen : Integer;
begin
  RamaActual := Nil;
  PrimeraRama := Nil;
  if not assigned(Raiz) then
    VaciarArbol;

  with dsOrigen.DataSet do
  begin
    //For this example I use filter, however it can be use with WHERE sentence
    Filtered := False;
    IF not assigned(Raiz) then
      Filter := IdPadre + ' IS NULL '
    else
      Filter := IDPadre + ' = ' + TElementoMenu(Raiz.Data).ID;
    Filtered := True;

    First;
    while not Eof do
    begin
      if FieldByName(CampoVisible).AsString = 'Y' then
      begin
        ElementoMenu := TElementoMenu.Create;
        ElementoMenu.Comando := FieldByName(CampoComando).AsString;
        ElementoMenu.ID := FieldByName(CampoID).AsString;
        //ElementoMenu.Nombre := FieldByName(CampoName).AsString; //Otros datos para agregar al elemento del menu
        iIndiceImagen := 0;
        if Not Assigned(Raiz) then
          RamaActual := TreeView1.Items.AddObject(Nil, FieldByName(CampoCaption).AsString, ElementoMenu )
        else
        Begin
          RamaActual := TreeView1.Items.AddChildObject(Raiz, FieldByName(CampoCaption).AsString, ElementoMenu );
          iIndiceImagen := 1;
        End;

        RamaActual.ImageIndex := iIndiceImagen;
        RamaActual.SelectedIndex := iIndiceImagen;
      end;
      Next;
    end;

    if not Assigned(Raiz) then
      PrimeraRama := TreeView1.Items.GetFirstNode
    else
      PrimeraRama := Raiz.getFirstChild;

    while Assigned(PrimeraRama) do
    begin
      CrearMenuDeArbol(dsOrigen, CampoID, IDPadre, CampoComando, CampoCaption, CampoVisible, PrimeraRama);
      PrimeraRama := PrimeraRama.getNextSibling;
    end;    
  end;    
end;


procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  VaciarArbol;
end;

procedure TForm1.TreeView1DblClick(Sender: TObject);
begin
  if Assigned(treeView1.Selected) then
    ShowMessage(TElementoMenu(treeView1.Selected.Data).Comando);
end;

procedure TForm1.VaciarArbol;
var
  itm : TTreeNode;
begin
  while TreeView1.Items.Count > 0 do
  begin
    itm := TreeView1.Items[TreeView1.Items.Count-1];
    TElementoMenu(itm.Data).Free;
    TreeView1.Items.Delete(itm);
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  CrearMenuDeArbol(ds1, 'ID', 'PID', 'NAME', 'CAPTION', 'ISVISIBLE');
  Treeview1.FullExpand;
end;

【讨论】:

【参考方案5】:

我遇到了同样的问题,想用 SQL 来修复它,以避免对数据库服务器的调用过多(对于每个记录/递归步骤)。我们的嵌入式 RDBMS NexusDB 不允许像 oracle 或 MSSQL 这样的递归查询。所以这就是我想出的,内联解释。它允许在 1 遍中加载树,但仍使用提供的 rootid 作为起点加载整个树。 我的数据库表名为OBJDAT,具有唯一整数ID,父链接通过字段TechPar

调用例程应该如下所示,您必须为 RootID 提供一个参数值。 NULL 将从所有根中选择所有对象(具有 TachPar=NULL)

   SELECT Obj.* FROM TABLE(RECURTABLE(:RootID)) AS Obj

结果将是一个首先使用根(也称为***)对象排序的表。 然后您可以遍历结果表并将对象添加到您的树控件(或内存结构中),如下所示:

//pseudodelphicode
ResultSet:=SQLQueryResult

ResultSet.First
while not ResultSet.EOF do
begin
  NewNode:=TreeNode.Create;
  NewNode.ID:=ResultSet.ID;
  NewNode.Name:=ResultSet.Name
  ... load more relevant stuff
  ParentID:=ResultSet.TechPar
  if ParentID<>nil then
    Tree.FIndNode(ParentID).AddChild(NewNode)
  else Tree.AddRoot(NewNode)

  ResultSet.Next;
end

实际完成工作的存储过程的实现是这样的:

-- NexusDB 变种的 SQL 存储过程从 -- 资产登记簿或图书馆。

DROP ROUTINE IF EXISTS RECURTABLE;

CREATE FUNCTION RECURTABLE(aRootID INTEGER)
RETURNS TABLE
MODIFIES SQL DATA
BEGIN

  -- pre-clean temporary tables
  CREATE LOCAL TEMPORARY TABLE #tmpsublayer
  (
    ID INTEGER,
    Name VARCHAR(50),
    UserID VARCHAR(50),
    ObjType INTEGER,
    TechPar INTEGER
  );
  CREATE LOCAL TEMPORARY TABLE #tmpobjparsublayer (LIKE #tmpsublayer);
  CREATE LOCAL TEMPORARY TABLE #tmpResultTable (LIKE #tmpsublayer);

--  for debugging purpose, ignore
--  DROP TABLE IF EXISTS #tmpobjparsublayer;
--  DROP TABLE IF EXISTS #tmpsublayer;
--  DROP TABLE IF EXISTS #tmpResultTable;


  DECLARE lRecursionCounter,lParentID INTEGER;
  DECLARE lRootPath TEXT;  
  START TRANSACTION;
  TRY
    IF (aRootID=0) OR (aRootID IS NULL) THEN
      --  No root provided: select all root records into the intermediate sublayer result set
      INSERT INTO #tmpsublayer
       SELECT
          ID,
          Name,
          UserID,
          ObjType,
          TechPar
        FROM OBJDAT
        WHERE (TechPar IS NULL) OR (TechPar=0); -- Match on TechPar in (Null,0)

    ELSE
       -- a root record was provided, select the root record into the result list

       SET lRootPath=NULL;
       SET lParentID=aRootID;
       SET lRecursionCounter=0;
       -- this loop resolves the path from the selected root object to the ultimate root object
       REPEAT
         SET lRecursionCounter=lRecursionCounter+1;
         -- avoid infinite loop by cyclical links here by usning a recursion counter watchdog
         IF lRecursionCounter>100 THEN
           SIGNAL 'Resolve root path for ('+ToStringLen(aRootID,10)+'): Maximum hierarchical depth reached.';
         END IF;
         SET lParentID=(SELECT TechPar FROM $AMOBJTABLENAME WHERE ID=lParentID);
         IF NullIf(lParentID,0) IS NULL THEN
            LEAVE;
         ELSE
           SET lRootPath=TOSTRINGLEN(lParentID,10)+COALESCE(';'+lRootPath,'');
         END IF;
         UNTIL FALSE
       END REPEAT;

      -- actually select the single root object into the intermediate sublayer result set
      INSERT INTO #tmpsublayer
      SELECT
        ID,
        Name,
        UserID,
        ObjType,
        TechPar
      FROM OBJDAT
      WHERE ID=aRootID;  // match on ID
     END IF;


    -- copy our rootlayer of results into out final output result set
    INSERT INTO #tmpResultTable
      SELECT
        *
      FROM #tmpsublayer;

    SET lRecursionCounter=0;
    -- this loop adds layers of sub objects to the result table
    REPEAT
      SET lRecursionCounter=lRecursionCounter+1;
      IF (SELECT TOP 1 ID FROM #tmpsublayer) IS NULL THEN
        LEAVE; -- empty result set, we are done get out of the loop
      END IF;

      -- watchdog for loop count to avoid infinite loops caused by cyclical links
      IF lRecursionCounter>100 THEN
        SIGNAL 'RecurSelect('+ToStringLen(aRootID,10)+'): Max hierarchical depth reached.';
      END IF;


      --  get a sublayer from the main table based on the current parent layer and technical parent field
      -- Not required DROP TABLE IF EXISTS #tmpobjparsublayer;
      DELETE FROM #tmpobjparsublayer;
      INSERT INTO #tmpobjparsublayer
        SELECT
          D.ID ID,
          D.Name Name,
          D.UserID UserID,
          D.ObjType TypeID,
          D.TechPar TechPar
      FROM #tmpsublayer P
      JOIN OBJDAT ON P.ID=D.TechPar;

      --  insert our sublayer of regular linked objects into the result table
      INSERT INTO #tmpResultTable
        SELECT
          *
        FROM #tmpobjparsublayer;

      -- clear current sublayer
      DELETE FROM #tmpsublayer;
      -- Move the newly selected objects layer to the sublayer set for the next iteration
      INSERT INTO #tmpsublayer
        SELECT
          *
        FROM #tmpobjparsublayer;

      UNTIL FALSE -- trust the LEAVE and SIGNAL statements
    END REPEAT;

    -- clean up temporary tables
    DELETE FROM #tmpobjparsublayer;
    DELETE FROM #tmpsublayer;
    COMMIT;
  CATCH TRUE 
    -- cleanup if something went wrong
    ROLLBACK;
    SIGNAL ERROR_MESSAGE;
  END;

  DROP TABLE IF EXISTS #tmpobjparsublayer;
  DROP TABLE IF EXISTS #tmpsublayer;

  -- return result
  RETURN SELECT * FROM #tmpResultTable;
END;

【讨论】:

我实际上想将代码部分设置为语法突出显示为 SQL,检查如何执行此操作...

以上是关于SQL 和 Delphi:从表中创建树的递归机制的主要内容,如果未能解决你的问题,请参考以下文章

二叉树--(建树,前序,中序,后序)--递归和非递归实现

UVa 699 The Falling Leaves(递归建树)

delphi创建表

Uva 839天平(二叉树dfs, 递归建树)

使用 LINQ 在 JSON 中创建树层次结构

delphi学习treeview中从表列名和数据添加为目录并双击自动选中