Подтвердить что ты не робот

SQL и Delphi: рекурсивный механизм для создания дерева из таблицы

СУБД, с которой я работаю, - это MySQL, среда программирования - Delphi 7 (что в данном примере не имеет особого значения).

У меня есть таблица под названием "subject", где я храню все объекты книги в системе. Субъекты могут иметь отношения родитель-ребенок, например, наука может быть разделена, скажем, на математику и физику, тогда как математику можно подразделить на исчисление, алгебру, геометрию и по ходу.

Я бы хотел создать дерево, заполненное датой из этой таблицы. Пожалуйста, помогите мне сделать это. Даже неважно, какой язык вы используете для иллюстрации, он просто может быть псевдокодом.

Диаграмма базы данных для таблицы Subject выглядит следующим образом:

enter image description here

Определение таблицы темы:

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;

Заполнение таблицы Subject некоторыми фиктивными данными:

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;

Вот что делает этот фрагмент кода:

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

Но я бы хотел, чтобы это выглядело так:

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

Ответ 1

Я предлагаю вам не загружать все дерево сразу, зачем вам? ни один человек не может просмотреть на данный момент тысячу предметов. И это может быть долгим, и ваша программа будет выглядеть замороженной. И это создает огромную нагрузку на сеть и сервер.

Лучше использовать подход VirtualTreeView, где каждый элемент загружает свои дочерние элементы по запросу. Для этого потребуется один параметризованный подготовленный запрос, например

 Select ID, Title, This, That from TREE where Parent_ID = :ID

И да, не создавайте новый текст SQL для каждого элемента. Это опасно и медленно (вам нужно отбросить все данные, собранные для старого запроса, и проанализировать новый)

Вы должны сделать один параметризованный запрос, Prepare и просто закрыть/изменить значения параметра/открыть.

См. причины и образец Delphi в http://bobby-tables.com/


Один пример "загрузить все сразу сразу" - это время, когда он динамически создает всплывающее меню из таблицы sql-сервера в Delphi - хотя я не думаю, что пик является хорошим подходом для более или менее больших деревьев.

Обратите внимание на этот подход: вы заполняете корневые элементы, затем обнаруживаете, что так или иначе вы заполняете элементы, которые еще не заполнены, но уже упоминаются другими, пока нет таких элементов.

Вы можете сделать это рекурсивно, конечно, пройдя дерево до его конца - но это потребует много вложенных запросов к базе данных.

Вы можете сделать рекурсивный запрос SQL, но он, вероятно, будет очень зависящим от сервера, а механизмы RDBMS обычно налагают свои ограничения на глубину рекурсии.

Подход, возможно, немного худший в управлении деревьями, но более чистое и простое в RDBMS должно было бы сделать выделенный TQueue только что добавленный элемент дерева. После того, как вы загрузите какой-то элемент - изначально все корневые - вы запомните его в очереди. Затем вы удаляете один за другим из очереди и заполняете (загружать и ставить в очередь) его дочерние элементы. Пока очередь не станет пустой.

Ответ 2

Мне нравится использовать хэш-таблицу для создания индекса всех узлов, индексированных с помощью ключевого слова, и использовать это для построения дерева. Он требует 2 прохода таблицы. Первый проход создает дерево корней node для каждой записи и добавляет хэш-запись ключаID против дерева node. второй проход проходит по таблице, просматривая parentId в хеше. Если он найдет его, то он перемещает текущий node под родительский node, иначе игнорирует его. В конце второго прохода у вас есть полное дерево.

    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;

Ответ 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;

Ответ 4

Я написал пример на stackoverflow en español Consumir menu del sql server, может быть полезным для кого-то еще.

Используется несколько полей:

  • ID для идентификатора элемента
  • PID для идентификатора родителя
  • НАИМЕНОВАНИЕ для выполнения команды
  • CAPTION для заголовка TreeNode
  • НЕВОЗМОЖНО знать, будет ли этот элемент видимым для конечного пользователя (Д/Н).

Он работает для любых уровней меню и предназначен для использования с любой базой данных с использованием 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, чтобы избежать слишком большого количества обращений к серверу БД (для каждого шага записи/рекурсии). Наша встроенная СУБД NexusDB не допускает рекурсивные запросы, такие как oracle или MSSQL. Итак, вот что я придумал, объяснение в строке. Это позволяет загрузить дерево за 1 проход, но все же загружает все дерево, используя предоставленный корневой элемент в качестве отправной точки. Моя таблица БД называется 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;