这里可以采取不同的方法,但我认为只有两种是正确的。但是让我们一步一步来。
我们有 Hierachy 表,其中有 Root 列。这一列告诉我们哪一行是当前的起点。如前所述,我们希望只有一个起点。
我们认为我们可以做到:
约束
在这种方法中,首先我们需要创建能够完成这项工作的函数。
CREATE FUNCTION [gt].[fnOnlyOneRoot]()
RETURNS BIT
BEGIN
DECLARE @rootAmount TINYINT
DECLARE @result BIT
SELECT @rootAmount=COUNT(1) FROM [gt].[Hierarchy] WHERE [Root]=1
IF @rootAmount=1
set @result=1
ELSE
set @result=0
RETURN @result
END
GO
然后是约束:
ALTER TABLE [gt].[Hierarchy] WITH CHECK ADD CONSTRAINT [ckOnlyOneRoot] CHECK (([gt].[fnOnlyOneRoot]()=(1)))
不幸的是,这种方法是错误的,因为这个约束不允许我们更改表中的任何值。它需要准确地标记一个根(使用 Root=1 插入会抛出异常,并使用 set Root=0 进行更新)
我们可以更改 fnOnyOneRoot 以允许选择 0 个根,但这不是我们想要的。
索引
索引将删除 where 子句中定义的所有行,其余数据将设置唯一约束。我们在这里有不同的选择:
- Root 可以为空,我们可以在 Root!=0 且 Root 不为空的地方添加
- Root 必须有值,我们只能在 Root!=0 的地方添加
- 以及不同的组合
CREATE UNIQUE INDEX ix_OnyOneRoot ON [gt].[Hierarchy](Root) WHERE Root !=0 and Root is not null
这种方法也不是完美的。最多一个 Root 将被强制,但最小不会。要更新数据,我们需要将之前的行设置为 null 或 0。
触发器
我们可以做两种不同的触发器
- 防止触发器 - 这不会让我们输入错误的数据
- DoTheJob 触发器 - 在后台会为我们更新数据
防止触发
这与约束基本相同,如果我们只想强制一个根而不是我们无法更新或插入。
CREATE TRIGGER tOnlyOneRoot
ON [gt].[Hierarchy]
AFTER INSERT, UPDATE
AS
DECLARE @rootAmount TINYINT
DECLARE @result BIT
SELECT @rootAmount=COUNT(1) FROM [gt].[Hierarchy] WHERE [Root]=1
IF @rootAmount=1
set @result=1
ELSE
set @result=0
IF @result=0
BEGIN
RAISERROR ('Only one root',0,0);
ROLLBACK TRANSACTION
RETURN
END
GO
DoTheJob 触发器
此触发器将检查所有插入/更新的行,如果将传递多个 Root,它将引发异常。在其他情况下,如果要更新或插入一个新的 Root,触发器将允许这样做,并且在操作后它将所有其他行的 Root 值更改为 0。
CREATE TRIGGER tOnlyOneRootDoTheJob
ON [gt].[Hierarchy]
AFTER INSERT, UPDATE
AS
DECLARE @insertedCount TINYINT
SELECT @insertedCount = COUNT(1) FROM inserted WHERE [Root]=1
if (@insertedCount > 1)
BEGIN
RAISERROR ('Only one root',0,0);
ROLLBACK TRANSACTION
RETURN
END
DECLARE @newRootId INT
SELECT @newRootId = [HierarchyId] FROM inserted WHERE [Root]=1
UPDATE [gt].[Hierarchy] SET [Root]=0 WHERE [HierarchyId] <> @newRootId
GO
这是我们试图实现的解决方案。只有一个根规则始终满足。 (应该做额外的删除触发)
不同的表和关系
这是一种更规范的方式。我们创建新表只允许有一行(使用上述选项)并且我们加入。
CREATE TABLE [gt].[HierarchyDefault](
[HierarchyId] INT PRIMARY KEY NOT NULL,
CONSTRAINT FK_HierarchyDefault_Hierarchy FOREIGN KEY (HierarchyId) REFERENCES [gt].[Hierarchy](HierarchyId)
)
会影响性能吗?
一栏
SET STATISTICS TIME ON;
SELECT [HierarchyId],[ParentHierarchyId],[Root]
FROM [gt].[Hierarchy] WHERE [root]=1
SET STATISTICS TIME OFF;
结果
CPU 时间 = 0 毫秒,经过的时间 = 0 毫秒。
加入:
SET STATISTICS TIME ON;
SELECT h.[HierarchyId],[ParentHierarchyId],[Root]
FROM [gt].[Hierarchy] h
INNER JOIN [gt].[HierarchyDefault] hd on h.[HierarchyId]=hd.[HierarchyId]
WHERE [root]=1
SET STATISTICS TIME OFF;
结果
CPU 时间 = 0 毫秒,经过的时间 = 0 毫秒。
总结
我将使用触发器。它是表中的一些魔法,但它在引擎盖下完成了所有工作。
轻松创建表:
CREATE TABLE [gt].[Hierarchy](
[HierarchyId] INT PRIMARY KEY IDENTITY(1,1),
[ParentHierarchyId] INT NULL,
[Root] BIT
CONSTRAINT FK_Hierarchy_Hierarchy FOREIGN KEY (ParentHierarchyId)
REFERENCES [gt].[Hierarchy](HierarchyId)
)