我有下表:
create table dbo.Link
(
FromNodeId int not null,
ToNodeId int not null
)
此表中的行表示节点之间的链接。
我想阻止对此表的插入或更新在节点之间创建循环关系。
所以如果表包含:
(1,2)
(2,3)
不应该允许包含以下任何内容:
(1,1)
(2,1)
(3,1)
我很乐意单独处理(1,1)(例如使用CHECK CONSTRAINT),如果它使解决方案更直接。
我正在考虑使用递归CTE创建一个AFTER INSERT触发器(虽然可能有一种更简单的方法)。
假设这是要走的路,触发器定义是什么?如果有一种更优雅的方式,它是什么?
答案 0 :(得分:3)
首先请注意,最好在另一个环境中检测周期,因为递归CTE不是因其良好的性能而闻名,也不是每个插入语句都会运行的触发器。对于大型图表,基于以下解决方案的解决方案可能效率低下。
假设您按如下方式创建表:
CREATE TABLE dbo.lnk (
node_from INT NOT NULL,
node_to INT NOT NULL,
CONSTRAINT CHK_self_link CHECK (node_from<>node_to),
CONSTRAINT PK_lnk_node_from_node_to PRIMARY KEY(node_from,node_to)
);
这会阻止node_from
等于node_to
的插入,以及已存在的行。
如果检测到循环引用,则以下触发器应通过抛出异常来检测循环引用:
CREATE TRIGGER TRG_no_circulars_on_lnk ON dbo.lnk AFTER INSERT
AS
BEGIN
DECLARE @cd INT;
WITH det_path AS (
SELECT
anchor=i.node_from,
node_to=l.node_to,
is_cycle=CASE WHEN i.node_from/*anchor*/=l.node_to THEN 1 ELSE 0 END
FROM
inserted AS i
INNER JOIN dbo.lnk AS l ON
l.node_from=i.node_to
UNION ALL
SELECT
dp.anchor,
node_to=l.node_to,
is_cycle=CASE WHEN dp.anchor=l.node_to THEN 1 ELSE 0 END
FROM
det_path AS dp
INNER JOIN dbo.lnk AS l ON
l.node_from=dp.node_to
WHERE
dp.is_cycle=0
)
SELECT TOP 1
@cd=is_cycle
FROM
det_path
WHERE
is_cycle=1
OPTION
(MAXRECURSION 0);
IF @cd IS NOT NULL
THROW 67890, 'Insert would cause cyclic reference', 1;
END
我测试了这个数量有限的插页。
INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,2); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,4); -- OK
和
INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- PK violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,1); -- Check constraint violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,2); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,1); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,1); -- Exception: Insert would cause cyclic reference
如果一次插入多个行,或者如果在图中引入长于一个边的路径,它还会检测已插入行中已存在的循环引用。关于相同的初始插入:
INSERT INTO dbo.lnk(node_from,node_to)VALUES(8,9),(9,8); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,5),(5,6),(6,1); -- Exception: Insert would cause cyclic reference
答案 1 :(得分:0)
编辑:处理多记录插入,在单独的函数中移动逻辑
我考虑过一种程序方法,它非常快,几乎与链接表和图形“密度”中的记录数无关
我在一个有10'000个链接的表上测试了它,节点值从1到1000 这真的非常快,不会受到链接表维度或“密度”的影响
此外,该函数可用于在插入之前测试值,或者(例如)如果您不想在客户端的所有移动测试逻辑上使用触发器。
关于递归CTE的考虑:小心!
我已经在我的测试表(10k行)上测试了接受的答案但是在 25分钟之后,我已经取消了一行的插入操作,因为查询被挂起而没有结果......
将表格缩小到5k行,单个记录的插入可以持续 2-3分钟。
它非常依赖于图的“人口”。如果您插入一个新路径,或者您正在将一个节点添加到具有较低“分支”的路径,则它非常快,但您无法控制它。
当图表更加“密集”时,这个解决方案会在你脸上爆炸。
非常仔细地考虑您的需求。
所以,让我们看看如何......
首先,我已将表的// Not need to cast to `Button`, since all views can have an onClickListener
rootView.findViewById(R.id.enable).setOnClickListener(clickListener)
rootView.findViewById(R.id.enable).setOnClickListener(clickListener)
// Put this as a member of your Fragment class.
View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (v.getId() == R.id.enable) {
// Save your preference here
// ...
listener.themechanged(2);
enable.setVisibility(View.GONE);
disable.setVisibility(View.VISIBLE);
}
if (v.getId() == R.id.R.id.disable) {
// Save your preference here
// ...
listener.themechanged(2);
disable.setVisibility(View.GONE);
enable.setVisibility(View.VISIBLE);
}
}
}
设置为两列,并在第二列上添加了索引以进行完全覆盖。 (不需要FromNodeId&lt;&gt; ToNodeId上的PK
因为算法已经涵盖了这种情况。)
CHECK
然后我构建了一个函数来测试单个链接的有效性:
CREATE TABLE [dbo].[Link](
[FromNodeId] [int] NOT NULL,
[ToNodeId] [int] NOT NULL,
CONSTRAINT [PK_Link] PRIMARY KEY CLUSTERED ([FromNodeId],[ToNodeId])
)
GO
CREATE NONCLUSTERED INDEX [ToNodeId] ON [dbo].[Link] ([ToNodeId])
GO
现在让我们从触发器中调用它 如果将插入多行,则触发器将针对整个插入测试每个链接(旧表+新recs),如果所有这些都有效且最终表将是一致的,则插入将完成,如果其中一个无效,则插入将中止。
drop function fn_test_link
go
create function fn_test_link(@f int, @t int)
returns int
as
begin
--SET NOCOUNT ON
declare @p table (id int identity primary key, l int, t int, unique (l,t,id))
declare @r int = 0
declare @i int = 0
-- link is not self-referencing
if @f<>@t begin
-- there are links that starts from where new link wants to end (possible cycle)
if exists(select 1 from link where fromnodeid=@t) begin
-- PAY ATTENTION.. HERE LINK TABLE ALREADY HAVE ALL RECORDS ADDED (ALSO NEW ONES IF PROCEDURE IS CALLED FROM A TRIGGER AFTER INSERT)
-- LOAD ALL THE PATHS TOUCHED BY DESTINATION OF TEST NODE
set @i = 0
insert into @p
select distinct @i, ToNodeId
from link
where fromnodeid=@t
set @i = 1
-- THERE IS AT LEAST A STEP TO FOLLOW DOWN THE PATHS
while exists(select 1 from @p where l=@i-1) begin
-- LOAD THE NEXT STEP FOR ALL THE PATHS TOUCHED
insert into @p
select distinct @i, l.ToNodeId
from link l
join @p p on p.l = @i-1 and p.t = l.fromnodeid
-- CHECK IF THIS STEP HAVE REACHED THE TEST NODE START
if exists(select 1 from @p where l=@i and t=@f) begin
-- WE ARE EATING OUR OWN TAIL! CIRCULAR REFERENCE FOUND
set @r = -1
break
end
-- THE NODE IS STILL GOOD
-- DELETE FROM LIST DUPLICATED ALREADY TESTED PATHS
-- (THIS IS A BIG OPTIMIZATION, WHEN PATHS CROSSES EACH OTHER YOU RISK TO TEST MANY TIMES SAME PATHS)
delete p
from @p p
where l = @i
and (exists(select 1 from @p px where px.l < p.l and px.t = p.t))
set @i = @i + 1
end
if @r<0
-- a circular reference was found
set @r = 0
else
-- no circular reference was found
set @r = 1
end else begin
-- THERE ARE NO LINKS THAT STARTS FROM TESTED NODE DESTINATIO (CIRCULAR REFERENCE NOT POSSIBLE)
set @r = 1
end
end; -- link is not self-referencing
--select * from @p
return @r
end
GO
我希望这会有所帮助