防止循环加入,递归搜索

时间:2018-03-28 04:28:18

标签: mysql recursion

所以在我的情况下,我有三个表:listitemlist_relation

每个item都会通过list_id外键链接到列表。

list_relation看起来像这样:

CREATE TABLE list_relation
    (
        parent_id INT UNSIGNED NOT NULL,
        child_id INT UNSIGNED NOT NULL,

        UNIQUE(parent_id, child_id)

        FOREIGN KEY (parent_id)
            REFERENCES list (id)
                ON DELETE CASCADE,    

        FOREIGN KEY (child_id)
            REFERENCES list (id)
                ON DELETE CASCADE
    );

我希望能够从多个列表继承(包括相关项目)。

例如我有列表:1,2,3。

我想知道是否有任何SQL方法可以防止它成为循环关系。 E.g。

List 1继承自List 3,List 2继承自List 1,List 3继承自List 1.

1 -> 2 -> 3 -> 1

我目前的想法是,我必须首先通过验证所需的继承然后将其插入数据库来确定它是否为循环。

6 个答案:

答案 0 :(得分:5)

如果您使用 MySQL 8.0 MariaDB 10.2 (或更高版本),您可以尝试递归CTE(公用表表达式)

假设以下架构和数据:

CREATE TABLE `list_relation` (
  `child_id`  int unsigned NOT NULL,
  `parent_id` int unsigned NOT NULL,
  PRIMARY KEY (`child_id`,`parent_id`)
);
insert into list_relation (child_id, parent_id) values
    (2,1),
    (3,1),
    (4,2),
    (4,3),
    (5,3);

现在,您尝试使用child_id = 1parent_id = 4插入新行。但这会产生循环关系( 1-> 4->> 1 1-> 4-> 3-> 1 ),你想要防止。要查明反向关系是否已存在,您可以使用以下查询,该查询将显示 list 4 的所有父项(包括继承/传递父项):

set @new_child_id  = 1;
set @new_parent_id = 4;

with recursive rcte as (
  select *
  from list_relation r
  where r.child_id = @new_parent_id
  union all
  select r.*
  from rcte
  join list_relation r on r.child_id = rcte.parent_id
)
select * from rcte

结果将是:

child_id | parent_id
       4 |         2
       4 |         3
       2 |         1
       3 |         1

Demo

您可以在结果中看到列表1 list 4 的父项之一,并且您不会插入新记录。

由于您只想知道 list 1 是否在结果中,您可以将最后一行更改为

select * from rcte where parent_id = @new_child_id limit 1

select exists (select * from rcte where parent_id = @new_child_id)

BTW:您可以使用相同的查询来防止冗余关系。 假设您要使用child_id = 4parent_id = 1插入记录。这将是多余的,因为 list 4 已经在 list 2 list 3 上继承 list 1 。以下查询将向您显示:

set @new_child_id  = 4;
set @new_parent_id = 1;

with recursive rcte as (
  select *
  from list_relation r
  where r.child_id = @new_child_id
  union all
  select r.*
  from rcte
  join list_relation r on r.child_id = rcte.parent_id
)
select exists (select * from rcte where parent_id = @new_parent_id)

您可以使用类似的查询来获取所有继承的项目:

set @list = 4;

with recursive rcte (list_id) as (
  select @list
  union distinct
  select r.parent_id
  from rcte
  join list_relation r on r.child_id = rcte.list_id
)
select distinct i.*
from rcte
join item i on i.list_id = rcte.list_id

答案 1 :(得分:3)

对于那些没有MySQL 8.0 或Maria DB且想在MySQL 5.7中使用递归方法的人。 我希望你不必超过255 manual的最大rec.depth:)

MySQL不允许递归函数,但它确实允许递归过程。将它们组合起来你就可以拥有很好的小功能,你可以在任何选择命令中使用它。

递归sp将采用两个输入参数和一个输出。第一个输入是您在节点树中搜索的ID,sp使用第二个输入在执行期间保留结果。第三个参数是输出参数,它带有最终结果。

CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_list_relation_recursive`(
    in itemId       text,
    in iPreserve    text,
    out oResult     text

)
BEGIN

    DECLARE ChildId text default null;

    IF (coalesce(itemId,'') = '') then
        -- when no id received retun whatever we have in the preserve container
        set oResult = iPreserve;
    ELSE
        -- add the received id to the preserving container
        SET iPreserve = concat_ws(',',iPreserve,itemId);
        SET oResult = iPreserve;

        SET ChildId = 
        (
            coalesce(
            (
                Select 
                    group_concat(TNode.child_id separator ',') -- get all children
                from 
                    list_relation as TNode 
                WHERE 
                    not find_in_set(TNode.child_id,     iPreserve) -- if we don't already have'em
                    AND find_in_set(TNode.parent_id,    itemId) -- from these parents
            )
        ,'')
        );

        IF length(ChildId) >0 THEN
            -- one or more child found, recursively search again for further child elements
            CALL sp_list_relation_recursive(ChildId,iPreserve,oResult);
        END IF;

    END IF;

    -- uncomment this to see the progress looping steps
    -- select ChildId,iPreserve,oResult;
END

测试一下:

SET MAX_SP_RECURSION_DEPTH = 250;
set @list = '';
call test.sp_list_relation_recursive(1,'',@list);
select @list;

+----------------+
|     @list      |
+----------------+
| ,1,2,3,6,4,4,5 |
+----------------+

不介意重复的父母或额外的逗号,我们只是想知道节点中是否存在一个元素而没有太多的if和whens。

看起来很好,但SP不能在select命令中使用,所以我们只为这个sP创建包装函数。

CREATE DEFINER=`root`@`localhost` FUNCTION `fn_list_relation_recursive`(
    NodeId int
) RETURNS text CHARSET utf8
    READS SQL DATA
    DETERMINISTIC
BEGIN

    /*
        Returns a tree of nodes
        branches out all possible branches
    */
    DECLARE mTree mediumtext;
    SET MAX_SP_RECURSION_DEPTH = 250;


    call sp_list_relation_recursive(NodeId,'',mTree);

    RETURN mTree;
END

现在检查一下:

SELECT 
    *,
    FN_LIST_RELATION_RECURSIVE(parent_id) AS parents_children
FROM
    list_relation;

+----------+-----------+------------------+
| child_id | parent_id | parents_children |
+----------+-----------+------------------+
|        1 |         7 | ,7,1,2,3,6,4,4,5 |
|        2 |         1 |   ,1,2,3,6,4,4,5 |
|        3 |         1 |   ,1,2,3,6,4,4,5 |
|        4 |         2 |             ,2,4 |
|        4 |         3 |           ,3,4,5 |
|        5 |         3 |           ,3,4,5 |
|        6 |         1 |   ,1,2,3,6,4,4,5 |
|       51 |        50 |           ,50,51 |
+----------+-----------+------------------+

您的插页将如下所示:

insert into list_relation (child_id,parent_id)
select
    -- child, parent
    1,6
where 
    -- parent not to be foud in child's children node
    not find_in_set(6,fn_list_relation_recursive(1));

1,6应该添加0条记录。但是1,7应该有用。

与往常一样,我只是在证明这个概念,你们非常欢迎 调整sp以返回父节点的子节点或子节点的父节点。或者为每个节点树提供两个单独的SP,或者甚至是所有节点树组合在一起,因此从单个id中返回所有父节点和子节点。

试一试..这并不难:)

答案 2 :(得分:2)

问: [有]任何SQL方法来阻止循环关系

答:简短的回答

没有声明性约束会阻止INSERT或UPDATE创建循环关系(如问题中所述)。

但是BEFORE INSERTBEFORE UPDATE触发器的组合可能会阻止它,使用查询和/或过程逻辑来检测INSERT或UPDATE的成功完成是否会导致循环关系。

当检测到这种情况时,触发器需要引发错误以防止INSERT / UPDATE操作完成。

答案 3 :(得分:1)

将列parent_id放在列表中是不是更好?

然后,您可以通过列表中LEFT JOIN的查询获取列表树,将parent_idlist_id相匹配,例如:

SELECT t1.list_id, t2.list_id, t3.list_id
FROM list AS t1
LEFT JOIN list as t2 ON t2.parent_id = t1.list_id
LEFT JOIN list as t3 ON t3.parent_id = t2.list_id
WHERE t1.list_id = #your_list_id#

这是您案件的解决方案吗? 无论如何,我建议你阅读有关在mysql中管理分层数据的内容,你可以找到很多关于这个问题的内容!

答案 4 :(得分:1)

您介意是否需要添加额外的表格?

SQL方法和有效的方法是创建一个额外的表,其中包含每个子项的 ALL 父项。然后在建立继承之前检查潜在子节点是否存在于当前节点的父列表中。

parent_list表将是这样的:

CREATE TABLE parent_list (
  list_id INT UNSIGNED NOT NULL,
  parent_list_id INT UNSIGNED NOT NULL,
  PRIMARY KEY (list_id, parent_list_id)
);

现在,让我们从一开始就开始。

  1. 2从1和4继承。
    parent_list为空,这意味着1和4都没有父母。所以在这种情况下很好。
    完成此步骤后,parent_list应为:

      

    list_id,parent_list_id
      2,1   2,4,

  2. 3继承自2.
    2有两个父母,1和4. 3不是其中之一。所以再次没事了。
    现在parent_list变为(注意 2的父母也应该是3的父母):

      

    list_id,parent_list_id
      2,1   2,4   3,1   3,4   3,2

  3. 4继承自3.
    4存在于3的父列表中。这将导致一个循环。没办法!

  4. 要检查循环是否会发生,您只需要一个简单的SQL:

    SELECT * FROM parent_list 
    WHERE list_id = potential_parent_id AND parent_list_id = potential_child_id;
    

    想要通过一个电话完成所有这些事情吗?应用存储过程:

    CREATE PROCEDURE 'inherit'(
    IN in_parent_id INT UNSIGNED,
    IN in_child_id INT UNSIGNED
    )
    BEGIN
        DECLARE result INT DEFAULT 0;
    
        DECLARE EXIT HANDLER FOR SQLEXCEPTION 
        BEGIN
              ROLLBACK;
              SELECT -1;
        END;
    
        START TRANSACTION;
    
        IF EXISTS(SELECT * FROM parent_list WHERE list_id = in_parent_id AND parent_list_id = in_child_id) THEN
            SET result = 1; -- just some error code
        ELSE
            -- do your inserting here
    
            -- update parent_list
            INSERT INTO parent_list (SELECT in_child_id, parent_list_id FROM parent_list WHERE list_id = in_parent_id);
            INSERT INTO parent_list VALUES (in_child_id, in_parent_id);
        END IF;
    
        COMMIT;
        SELECT result;
    END
    

    说到多重继承,只需多次调用inherit

答案 5 :(得分:1)

在您提供的示例中,错误的关系很简单。这是3 - > 1和1-> 3关系。您可以在插入新行时查找反向关系。如果存在,请不要插入新行。

如果添加自动递增列,则可以专门识别有问题的行。

另一方面,如果您正在查看现有行,则可以使用简单的SQL语句识别错误行:

SELECT
    a.parent_id,
    a.child_id
FROM list_relation a
JOIN list_relation b
ON a.child_id = b.parent_id AND a.parent_id = b.child_id

如果添加自动递增列,则可以专门识别有问题的行。

您的问题标题包含“阻止”一词,因此我假设您要避免添加行。为此,您需要一个ON BEFORE INSERT触发器来检查现有行并阻止插入。您还可以使用ON BEFORE UPDATE触发器来防止将现有行更改为可能存在问题的值。