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')