在postgres中防止和/或检测周期

时间:2014-10-31 09:46:19

标签: postgresql triggers plpgsql recursive-query

假设有如下架构:

CREATE TABLE node (
  id       SERIAL PRIMARY KEY,
  name     VARCHAR,
  parentid INT REFERENCES node(id)
);

此外,我们假设存在以下数据:

INSERT INTO node (name,parentid) VALUES
('A',NULL),
('B',1),
('C',1);

有没有办法防止创建周期?例如:

UPDATE node SET parentid = 2 WHERE id = 1;

这会创建一个1->2->1->...

的循环

4 个答案:

答案 0 :(得分:11)

简化和优化您的触发器应该快得多:

CREATE OR REPLACE FUNCTION detect_cycle()
  RETURNS TRIGGER AS
$func$
BEGIN

IF EXISTS (
   WITH RECURSIVE search_graph(parentid, path, cycle) AS ( -- relevant columns
       -- check ahead, makes 1 step less
      SELECT g.parentid, ARRAY[g.id, g.parentid], (g.id = g.parentid)
      FROM   node g
      WHERE  g.id = NEW.id  -- only test starting from new row

      UNION ALL
      SELECT g.parentid, sg.path || g.parentid, g.parentid = ANY(sg.path)
      FROM   search_graph sg
      JOIN   node g ON g.id = sg.parentid
      WHERE  NOT sg.cycle
      )
   SELECT 1
   FROM   search_graph
   WHERE  cycle
   LIMIT  1  -- stop evalutation at first find
   )
THEN
   RAISE EXCEPTION 'Loop detected!';
ELSE
  RETURN NEW;
END IF;

END
$func$ LANGUAGE plpgsql;

您不需要动态SQL,您不需要计算,不需要所有列,并且您不需要为每一行测试整个表

CREATE TRIGGER detect_cycle_after_update
AFTER INSERT OR UPDATE ON node
FOR EACH ROW EXECUTE PROCEDURE detect_cycle();

这样的INSERT也必须被禁止:

INSERT INTO node (id, name,parentid) VALUES (8,'D',9), (9,'E',8);

答案 1 :(得分:2)

为了回答我自己的问题,我想出了一个阻止它的触发器:

CREATE OR REPLACE FUNCTION detect_cycle() RETURNS TRIGGER AS
$func$
DECLARE
  loops INTEGER;
BEGIN
   EXECUTE 'WITH RECURSIVE search_graph(id, parentid, name, depth, path, cycle) AS (
        SELECT g.id, g.parentid, g.name, 1,
          ARRAY[g.id],
          false
        FROM node g
      UNION ALL
        SELECT g.id, g.parentid, g.name, sg.depth + 1,
          path || g.id,
          g.id = ANY(path)
        FROM node g, search_graph sg
        WHERE g.id = sg.parentid AND NOT cycle
)
SELECT count(*) FROM search_graph where cycle = TRUE' INTO loops;
IF loops > 0 THEN
  RAISE EXCEPTION 'Loop detected!';
ELSE
  RETURN NEW;
END IF;
END
$func$ LANGUAGE plpgsql;

CREATE TRIGGER detect_cycle_after_update
AFTER UPDATE ON node
FOR EACH ROW EXECUTE PROCEDURE detect_cycle();

所以,如果你试图创建一个循环,就像在问题中那样:

UPDATE node SET parentid = 2 WHERE id = 1;

你得到EXCEPTION

ERROR:  Loop detected!

答案 2 :(得分:0)

CREATE OR REPLACE FUNCTION detect_cycle()
  RETURNS TRIGGER AS
$func$
DECLARE
  cycle int[];
BEGIN
EXECUTE format('WITH RECURSIVE search_graph(%4$I, path, cycle) AS (
  SELECT g.%4$I, ARRAY[g.%3$I, g.%4$I], (g.%3$I = g.%4$I)
    FROM %1$I.%2$I g
   WHERE g.%3$I = $1.%3$I
  UNION ALL
  SELECT g.%4$I, sg.path || g.%4$I, g.%4$I = ANY(sg.path)
    FROM search_graph  sg
    JOIN %1$I.%2$I g ON g.%3$I = sg.%4$I
   WHERE NOT sg.cycle)
SELECT path
  FROM search_graph
 WHERE cycle
 LIMIT 1', TG_TABLE_SCHEMA, TG_TABLE_NAME, quote_ident(TG_ARGV[0]), quote_ident(TG_ARGV[1]))
INTO cycle
USING NEW;
IF cycle IS NULL
THEN
  RETURN NEW;
ELSE
   RAISE EXCEPTION 'Loop in %.% detected: %', TG_TABLE_SCHEMA, TG_TABLE_NAME, array_to_string(cycle, ' -> ');
END IF;

END
$func$ LANGUAGE plpgsql;

CREATE TRIGGER detect_cycle_after_update
 AFTER INSERT OR UPDATE ON node
   FOR EACH ROW EXECUTE PROCEDURE detect_cycle('id', 'parent_id');

答案 3 :(得分:0)

虽然一次处理一个更新/插入时,@ Erwin Brandstetter接受的当前答案是可以的,但是考虑并发执行时,它仍然可能失败。

假设表内容由

定义
INSERT INTO node VALUES
(1, 'A', NULL),
(2, 'B', 1),
(3, 'C', NULL),
(4, 'D', 3);

然后在一次交易中执行

-- transaction A
UPDATE node SET parentid = 2 where id = 3;

和另一个

-- transaction B
UPDATE node SET parentid = 4 where id = 1;

两个UPDATE命令都将成功执行,然后您可以提交这两个事务。

-- transaction A
COMMIT;
-- transaction B
COMMIT;

然后您将在表中拥有循环1-> 4-> 3-> 2-> 1。 为了使其正常工作,您将不得不使用隔离级别SERIALIZABLE或在触发器中使用显式锁定。