Foreign Key Constraint based on specific values in other table
Foreign Key Constraint based upon specific values in other table
在我的数据库中,我创建了以下 table:
CREATE TABLE [Category_Dim]
(
[Id] INT NOT NULL PRIMARY KEY
,[__ParentCategoryId] INT
,[Name] VARCHAR(250)
,CONSTRAINT [FK1] FOREIGN KEY ([__ParentCategoryId]) REFERENCES [dbo].[Category_Dim] ([Id])
)
这允许我存储多个不同类型的分类(嵌套)列表,其根具有 __ParentCategoryId = NULL
,然后按如下方式输入 children,例如:
INSERT INTO Category_Dim (Id, __ParentCategoryId, Name) VALUES
(1, NULL, 'Dog Breeds'),
(2, NULL, 'Bird Types'),
(3, 1, 'Chihuahua'),
(4, 1, 'Pug'),
(5, 1, 'Pit Bull'),
(6, 2, 'Macaw'),
(7, 2, 'Finch'),
... etc
换句话说,在这种情况下,ID 3、4 和 5 是 children 的 1(不同的狗品种),6 和 7 是 children 的 2(鸟类的类型)。
现在,假设我正在尝试创建第二个 table,我希望 只允许犬种(Id = 1
中的 children)作为值 在列中,否则为错误。
到目前为止,我有以下定义:
CREATE TABLE [Trainers]
(
[TrainerId] INT NOT NULL PRIMARY KEY IDENTITY (1, 1)
,[__DogBreedId] INT NOT NULL
, ...
,CONSTRAINT [FK_DogBreeds] FOREIGN KEY ([__DogBreedId]) REFERENCES [dbo].[Category_Dim] ([Id])
)
这有外键约束,但它允许 Category_Dim
中的任何 Id
值作为我的 __DogBreedId
,因此一个人可以输入数字,在这种情况下, 3-5 如我所愿。
有没有办法通过外键语句来实现?而且,如果不是,最好的方法是什么?或者这总体上是个坏主意吗?
谢谢!!
我最终为实现这一目标所做的是,我创建了一个返回 BIT
的函数,该函数基于 CategoryId
提供的是父 CategoryId 的子项:
CREATE FUNCTION [dbo].[IsChildOfCategory]
(
@__CategoryId INT
,@__ParentCategoryId INT
)
RETURNS BIT
AS
BEGIN
RETURN CASE
WHEN EXISTS
(
SELECT Id FROM Category_Dim
WHERE __ParentCategoryId = @__ParentCategoryId
AND Id = @__CategoryId
)
THEN 1
ELSE 0
END
END;
GO
然后,我将以下检查约束添加到我的 table 定义中:
,CONSTRAINT [CHK_IsDogBreed] CHECK ([dbo].[IsChildOfCategory]([__DogBreedId], 1) = 1)
这与外键约束一起,似乎完全符合我的要求。
但是我真的想知道这是否是一个糟糕的使用模式(所有内容都作为数据存储在一个类别中 table 而不是单独的数据库 tables for each type of data),因为这让我不得不硬编码 Category Ids
就像在这个检查约束中一样,它作为数据存在于数据库中而不是特定的数据库对象中(换句话说,它留下我需要用非常具体的值来播种我的数据库 - 在这种情况下确保 Category Id
1 = 'Dog Breeds').
所以,它确实有效,但它确实让我质疑这是否是个坏主意。
对于单级层次结构(节点深度 = 1),您最好使用 supertype-subtype。
如果您确实需要可变节点深度的树,那么请考虑使用 闭包 table,而不是将 parent_id
放在同一个 table.
闭包 table 将所有路径存储在树中,因此每个祖先-后代 link 都是单独的一行。这样,给定节点的所有 ancestor/descendants 都会被公开。闭包 table 很容易查询,但维护起来有点困难,所以这是一个权衡。
-- Category CAT exists.
--
category {CAT}
PK {CAT}
-- Data Sample
(CAT)
------------------------
('Dogs')
, ('Big Dogs')
, ('Small Dogs')
, ('Chihuahua')
, ('Pug')
, ('Pit Bull')
, ('Birds')
, ('Macaw')
, ('Finch')
-- Ancestor ANC has descendant DCS
--
category_tree {ANC, DCS}
PK {ANC, DCS}
FK1 {ANC} REFERENCES category {CAT}
FK2 {DCS} REFERENCES category {CAT}
-- Data Sample, includes ANC=DCS
(ANC, DCS)
------------------------
('Dogs' , 'Dogs')
, ('Birds' , 'Birds')
, ('Dogs' , 'Big Dogs')
, ('Dogs' , 'Small Dogs')
, ('Big Dogs' , 'Big Dogs')
, ('Small Dogs' , 'Small Dogs')
, ('Dogs' , 'Chihuahua')
, ('Small Dogs' , 'Chihuahua')
, ('Chihuahua' , 'Chihuahua')
, ('Dogs' , 'Pug')
, ('Small Dogs' , 'Pug')
, ('Pug' , 'Pug')
, ('Dogs' , 'Pit Bull')
, ('Big Dogs' , 'Pit Bull')
, ('Pit Bull' , 'Pit Bull')
, ('Birds' , 'Macaw')
, ('Macaw' , 'Macaw')
, ('Birds' , 'Finch')
, ('Finch' , 'Finch')
-- Trainer TRA trains all descendants of ancestor ANC.
--
trainer {TRA, ANC}
PK {TRA, ANC}
FK {ANC, ANC} REFERENCES category_tree {ANC, DCS}
-- Data Sample
(TRA, ANC)
------------------------
('Joe' , 'Dogs')
, ('Jane' , 'Small Dogs')
, ('Jane' , 'Finch')
, ('Jill' , 'Big Dogs')
, ('Jack' , 'Birds')
, ('John' , 'Pug')
-- Trainer TRA trains DCS, descendant of ANC.
-- (Resolved to leaf nodes.)
WITH
q_00 AS ( -- leaves only
select ANC, count(1) as cnt
from category_tree
group by ANC
having count(1) = 1
)
SELECT t.TRA, x.DCS, t.ANC
FROM trainer AS t
JOIN category_tree AS x ON x.ANC = t.ANC
JOIN q_00 as q ON q.ANC = x.DCS
ORDER BY TRA, t.ANC;
;
Returns:
TRA DCS ANC
----------------------------------
Jack' 'Finch' 'Birds'
Jack' 'Macaw' 'Birds'
Jane' 'Finch' 'Finch'
Jane' 'Pug' 'Small Dogs'
Jane' 'Chihuahua' 'Small Dogs'
Jill' 'Pit Bull' 'Big Dogs'
Joe' 'Pit Bull' 'Dogs'
Joe' 'Pug' 'Dogs'
Joe' 'Chihuahua' 'Dogs'
John' 'Pug' 'Pug'
注:
All attributes (columns) NOT NULL
PK = Primary Key
FK = Foreign Key
SQL 测试
CREATE TABLE category (
CAT VARCHAR(32) NOT NULL
, CONSTRAINT pk_cat PRIMARY KEY (CAT)
);
CREATE TABLE category_tree (
ANC VARCHAR(32) NOT NULL
, DCS VARCHAR(32) NOT NULL
, CONSTRAINT pk_ctre PRIMARY KEY (ANC, DCS)
, CONSTRAINT fk1_ctre FOREIGN KEY (ANC)
REFERENCES category (CAT)
, CONSTRAINT fk2_ctre FOREIGN KEY (DCS)
REFERENCES category (CAT)
);
CREATE TABLE trainer (
TRA VARCHAR(32) NOT NULL
, ANC VARCHAR(32) NOT NULL
, CONSTRAINT pk_tra PRIMARY KEY (TRA, ANC)
, CONSTRAINT fk1_tra FOREIGN KEY (ANC, ANC)
REFERENCES category_tree (ANC, DCS)
);
INSERT INTO category (CAT)
VALUES
('Dogs')
, ('Big Dogs')
, ('Small Dogs')
, ('Chihuahua')
, ('Pug')
, ('Pit Bull')
, ('Birds')
, ('Macaw')
, ('Finch')
;
INSERT INTO category_tree (ANC, DCS)
VALUES
('Dogs' , 'Dogs')
, ('Birds' , 'Birds')
, ('Dogs' , 'Big Dogs')
, ('Dogs' , 'Small Dogs')
, ('Big Dogs' , 'Big Dogs')
, ('Small Dogs' , 'Small Dogs')
, ('Dogs' , 'Chihuahua')
, ('Small Dogs' , 'Chihuahua')
, ('Chihuahua' , 'Chihuahua')
, ('Dogs' , 'Pug')
, ('Small Dogs' , 'Pug')
, ('Pug' , 'Pug')
, ('Dogs' , 'Pit Bull')
, ('Big Dogs' , 'Pit Bull')
, ('Pit Bull' , 'Pit Bull')
, ('Birds' , 'Macaw')
, ('Macaw' , 'Macaw')
, ('Birds' , 'Finch')
, ('Finch' , 'Finch')
;
INSERT INTO trainer (TRA, ANC)
VALUES
('Joe' , 'Dogs')
, ('Jane' , 'Small Dogs')
, ('Jane' , 'Finch')
, ('Jill' , 'Big Dogs')
, ('Jack' , 'Birds')
, ('John' , 'Pug')
;
编辑
如果整个 table 应该仅限于一个祖先,那么您可以:
-- Trainer TRA trains dog DCS; (ANC = 'Dogs').
--
dog_trainer {TRA, DSC, ANC}
PK {TRA, DSC}
FK {ANC, DSC} REFERENCES category_tree {ANC, DCS}
CHECK (ANC = 'Dogs')
在我的数据库中,我创建了以下 table:
CREATE TABLE [Category_Dim]
(
[Id] INT NOT NULL PRIMARY KEY
,[__ParentCategoryId] INT
,[Name] VARCHAR(250)
,CONSTRAINT [FK1] FOREIGN KEY ([__ParentCategoryId]) REFERENCES [dbo].[Category_Dim] ([Id])
)
这允许我存储多个不同类型的分类(嵌套)列表,其根具有 __ParentCategoryId = NULL
,然后按如下方式输入 children,例如:
INSERT INTO Category_Dim (Id, __ParentCategoryId, Name) VALUES
(1, NULL, 'Dog Breeds'),
(2, NULL, 'Bird Types'),
(3, 1, 'Chihuahua'),
(4, 1, 'Pug'),
(5, 1, 'Pit Bull'),
(6, 2, 'Macaw'),
(7, 2, 'Finch'),
... etc
换句话说,在这种情况下,ID 3、4 和 5 是 children 的 1(不同的狗品种),6 和 7 是 children 的 2(鸟类的类型)。
现在,假设我正在尝试创建第二个 table,我希望 只允许犬种(Id = 1
中的 children)作为值 在列中,否则为错误。
到目前为止,我有以下定义:
CREATE TABLE [Trainers]
(
[TrainerId] INT NOT NULL PRIMARY KEY IDENTITY (1, 1)
,[__DogBreedId] INT NOT NULL
, ...
,CONSTRAINT [FK_DogBreeds] FOREIGN KEY ([__DogBreedId]) REFERENCES [dbo].[Category_Dim] ([Id])
)
这有外键约束,但它允许 Category_Dim
中的任何 Id
值作为我的 __DogBreedId
,因此一个人可以输入数字,在这种情况下, 3-5 如我所愿。
有没有办法通过外键语句来实现?而且,如果不是,最好的方法是什么?或者这总体上是个坏主意吗?
谢谢!!
我最终为实现这一目标所做的是,我创建了一个返回 BIT
的函数,该函数基于 CategoryId
提供的是父 CategoryId 的子项:
CREATE FUNCTION [dbo].[IsChildOfCategory]
(
@__CategoryId INT
,@__ParentCategoryId INT
)
RETURNS BIT
AS
BEGIN
RETURN CASE
WHEN EXISTS
(
SELECT Id FROM Category_Dim
WHERE __ParentCategoryId = @__ParentCategoryId
AND Id = @__CategoryId
)
THEN 1
ELSE 0
END
END;
GO
然后,我将以下检查约束添加到我的 table 定义中:
,CONSTRAINT [CHK_IsDogBreed] CHECK ([dbo].[IsChildOfCategory]([__DogBreedId], 1) = 1)
这与外键约束一起,似乎完全符合我的要求。
但是我真的想知道这是否是一个糟糕的使用模式(所有内容都作为数据存储在一个类别中 table 而不是单独的数据库 tables for each type of data),因为这让我不得不硬编码 Category Ids
就像在这个检查约束中一样,它作为数据存在于数据库中而不是特定的数据库对象中(换句话说,它留下我需要用非常具体的值来播种我的数据库 - 在这种情况下确保 Category Id
1 = 'Dog Breeds').
所以,它确实有效,但它确实让我质疑这是否是个坏主意。
对于单级层次结构(节点深度 = 1),您最好使用 supertype-subtype。
如果您确实需要可变节点深度的树,那么请考虑使用 闭包 table,而不是将 parent_id
放在同一个 table.
闭包 table 将所有路径存储在树中,因此每个祖先-后代 link 都是单独的一行。这样,给定节点的所有 ancestor/descendants 都会被公开。闭包 table 很容易查询,但维护起来有点困难,所以这是一个权衡。
-- Category CAT exists.
--
category {CAT}
PK {CAT}
-- Data Sample
(CAT)
------------------------
('Dogs')
, ('Big Dogs')
, ('Small Dogs')
, ('Chihuahua')
, ('Pug')
, ('Pit Bull')
, ('Birds')
, ('Macaw')
, ('Finch')
-- Ancestor ANC has descendant DCS
--
category_tree {ANC, DCS}
PK {ANC, DCS}
FK1 {ANC} REFERENCES category {CAT}
FK2 {DCS} REFERENCES category {CAT}
-- Data Sample, includes ANC=DCS
(ANC, DCS)
------------------------
('Dogs' , 'Dogs')
, ('Birds' , 'Birds')
, ('Dogs' , 'Big Dogs')
, ('Dogs' , 'Small Dogs')
, ('Big Dogs' , 'Big Dogs')
, ('Small Dogs' , 'Small Dogs')
, ('Dogs' , 'Chihuahua')
, ('Small Dogs' , 'Chihuahua')
, ('Chihuahua' , 'Chihuahua')
, ('Dogs' , 'Pug')
, ('Small Dogs' , 'Pug')
, ('Pug' , 'Pug')
, ('Dogs' , 'Pit Bull')
, ('Big Dogs' , 'Pit Bull')
, ('Pit Bull' , 'Pit Bull')
, ('Birds' , 'Macaw')
, ('Macaw' , 'Macaw')
, ('Birds' , 'Finch')
, ('Finch' , 'Finch')
-- Trainer TRA trains all descendants of ancestor ANC.
--
trainer {TRA, ANC}
PK {TRA, ANC}
FK {ANC, ANC} REFERENCES category_tree {ANC, DCS}
-- Data Sample
(TRA, ANC)
------------------------
('Joe' , 'Dogs')
, ('Jane' , 'Small Dogs')
, ('Jane' , 'Finch')
, ('Jill' , 'Big Dogs')
, ('Jack' , 'Birds')
, ('John' , 'Pug')
-- Trainer TRA trains DCS, descendant of ANC.
-- (Resolved to leaf nodes.)
WITH
q_00 AS ( -- leaves only
select ANC, count(1) as cnt
from category_tree
group by ANC
having count(1) = 1
)
SELECT t.TRA, x.DCS, t.ANC
FROM trainer AS t
JOIN category_tree AS x ON x.ANC = t.ANC
JOIN q_00 as q ON q.ANC = x.DCS
ORDER BY TRA, t.ANC;
;
Returns:
TRA DCS ANC
----------------------------------
Jack' 'Finch' 'Birds'
Jack' 'Macaw' 'Birds'
Jane' 'Finch' 'Finch'
Jane' 'Pug' 'Small Dogs'
Jane' 'Chihuahua' 'Small Dogs'
Jill' 'Pit Bull' 'Big Dogs'
Joe' 'Pit Bull' 'Dogs'
Joe' 'Pug' 'Dogs'
Joe' 'Chihuahua' 'Dogs'
John' 'Pug' 'Pug'
注:
All attributes (columns) NOT NULL
PK = Primary Key
FK = Foreign Key
SQL 测试
CREATE TABLE category (
CAT VARCHAR(32) NOT NULL
, CONSTRAINT pk_cat PRIMARY KEY (CAT)
);
CREATE TABLE category_tree (
ANC VARCHAR(32) NOT NULL
, DCS VARCHAR(32) NOT NULL
, CONSTRAINT pk_ctre PRIMARY KEY (ANC, DCS)
, CONSTRAINT fk1_ctre FOREIGN KEY (ANC)
REFERENCES category (CAT)
, CONSTRAINT fk2_ctre FOREIGN KEY (DCS)
REFERENCES category (CAT)
);
CREATE TABLE trainer (
TRA VARCHAR(32) NOT NULL
, ANC VARCHAR(32) NOT NULL
, CONSTRAINT pk_tra PRIMARY KEY (TRA, ANC)
, CONSTRAINT fk1_tra FOREIGN KEY (ANC, ANC)
REFERENCES category_tree (ANC, DCS)
);
INSERT INTO category (CAT)
VALUES
('Dogs')
, ('Big Dogs')
, ('Small Dogs')
, ('Chihuahua')
, ('Pug')
, ('Pit Bull')
, ('Birds')
, ('Macaw')
, ('Finch')
;
INSERT INTO category_tree (ANC, DCS)
VALUES
('Dogs' , 'Dogs')
, ('Birds' , 'Birds')
, ('Dogs' , 'Big Dogs')
, ('Dogs' , 'Small Dogs')
, ('Big Dogs' , 'Big Dogs')
, ('Small Dogs' , 'Small Dogs')
, ('Dogs' , 'Chihuahua')
, ('Small Dogs' , 'Chihuahua')
, ('Chihuahua' , 'Chihuahua')
, ('Dogs' , 'Pug')
, ('Small Dogs' , 'Pug')
, ('Pug' , 'Pug')
, ('Dogs' , 'Pit Bull')
, ('Big Dogs' , 'Pit Bull')
, ('Pit Bull' , 'Pit Bull')
, ('Birds' , 'Macaw')
, ('Macaw' , 'Macaw')
, ('Birds' , 'Finch')
, ('Finch' , 'Finch')
;
INSERT INTO trainer (TRA, ANC)
VALUES
('Joe' , 'Dogs')
, ('Jane' , 'Small Dogs')
, ('Jane' , 'Finch')
, ('Jill' , 'Big Dogs')
, ('Jack' , 'Birds')
, ('John' , 'Pug')
;
编辑
如果整个 table 应该仅限于一个祖先,那么您可以:
-- Trainer TRA trains dog DCS; (ANC = 'Dogs').
--
dog_trainer {TRA, DSC, ANC}
PK {TRA, DSC}
FK {ANC, DSC} REFERENCES category_tree {ANC, DCS}
CHECK (ANC = 'Dogs')