假设有如下架构:
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->...
答案 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或在触发器中使用显式锁定。